Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[비트캠프] 85일차(18주차3일) - Spring Framework: myapp-64 중간(Client-rendering: Front-end / back-end 분리) 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Spring Framework, Spring Boot

[비트캠프] 85일차(18주차3일) - Spring Framework: myapp-64 중간(Client-rendering: Front-end / back-end 분리)

끈기JK 2023. 3. 8. 12:27

 

64. Front-end / Back-end

 

① Server-rendering → 클라이언트가 출력할 UI 를 서버에서 생성

Web Browser 가 App. Server 로 ① 요청한다. 여기서 Controller 를 ② 실행한다. 여기서 Service 를 ③ 실행 하고 여기서 DAO 를 ④ 실행 한다. 여기서 DBMS 로 ⑤ 질의하고 리턴 받는다. 리턴을 계속 받아서 App. Server 까지 온다. 여기서 JSP 페이지를 ⑥ 요청하고 ⑦ HTML 생성 코드를 응답 받는다. ⑧ 응답 (HTML + CSS + JavaScript)을 Web Browser 로 해서 ⑨ HTML 출력한다.

"Communication Diagram"

 

 

② Client-rendering → 서버에서 받은 데이터를 가지고 클라이언트에서 동적으로 UI 생성

Web Browser 에서 Application Server 로 ① list.html 요청 한다. 여기에서 list.html 을 리턴하고 Web Browser 에서 list.html 출력한다.

Web Browser 에서 Application Server 로 ② list 데이터 AJAX 요청한다. 여기서 list() 를 Controller 에서 실행한다. 여기서 Service 로 요청하고 여기서 DAO 로 요청한다. DAO 에서 DBMS 에 요청해서 결과 리턴을 계속한다. Contnroller 에서 List 또는 배열 객체를 Application 으로 리턴한다. 여기서 JSON 포맷 문자열을 Web Browser 로 리턴한다. 여기서 JSON 파싱해서 JavaScript 객체로 변환해서 이 객체로 HTML 태그 생성한다. 이걸 가지고 list.html 화면 갱신한다.

 

 

 

 

executor 실행시 resolve 또는 reject 통지를 Promise 로 한다. Promise는 작업 실행 상태를 통지받는 객체로 작업 상태에 따라 약속된 함수를 호출한다.

resolve 통지받으면 .then(onFullfillment) 에서 onFullfillment 함수가 호출된다.

reject 를 통지받으면 .then(onFullfillment, onRejection) 에서 onRejection 함수가 호출된다. 또는 .catch(onRejection) 하면 onRejection 함수가 호출된다.

 

 

executor 실행시 resolve 또는 reject 통지를 Promise 로 한다. resolve 통지받으면 .then(onFulfillment) 에서 onFulfillment 함수가 호출되는데 executor 역할이다. 실행이 완료되면 다른 Promise 에 통지한다.

Promise 객체에 실행이 완료됐다 통지 받으면 .then(onFulfillment2) 의 onFulfillment2 가 실행되고 executor 역할이다. 실행이 완료되면 또 다른 Promise 에 통지한다. 여기서 실행 완료를 통지 받으면 .then(onFulfillment3) 에서 onFulfillment 를 실행한다.

 

*작업들을 체인으로 엮어 순서대로 실행시킨다!

executor → onFulfillment → onFulfillment2 → onFulfillment3

 

 

executor 실행시 resolve(값) 통지를 Promise 로 한다. resolve 통지받으면 값 전달하며 .then(listener1), .then(listener2), .then(listerner3) 을 순서대로 호출한다.

한 개의 promise 객체에 여러 개의 리스너를 붙일 수있다. resolve 를 통지 받으면 순서대로 호출 하며 모든 리스너에게 값을 전달한다.

 

 

executor 실행시 resolve(값) 통지를 Promise 로 한다. resolve 통지받으면 값 전달을 .then(onFulfillment) 로 한다. 여기서 Promise 객체를 리턴한다. 여기서 실행이 완료됐다 통지받으면 .then(onFulfillment2) 의 onFulfillment2 를 실행하지만 값 전달은 안된다. 여기서 Promise 객체를 생성한다. 실행 완료를 통지받으면 .then(onFulfillment3) 의 onFulfillmenet3 를 실행하지만 값 전달은 안된다.

 

 

executor 실행시 resolve(값) 통지를 Promise 로 한다. resolve 통지받으면 값 전달을 .then(onFulfillment) 로 한다. 여기서 return 값2; 하고 Promise 객체를 생성한다. 실행이 완료됐다 통지받으면 값2 전달을 .then(onFulfillmenet2) 로 한다. 여기서 return 값3; 하고 Promise 객체를 생성한다. 실행 완료를 통지받으면 값3 전달을 .then(onFulfillment3) 로 한다.

리스너가 다음 체인에 연결된 리스너에게 값을 전달하려면 return 문을 사용해야 한다.

 

 

각각의 객체에 .then 과 .catch 가 있는 상황이다.

executor 실행시 예외발생을 Promise 에 통지한다. 여기서 예외가 발생하거나 reject 를 통지받으면

.then(x, listener) 에서 listener 가 호출되어 Promise 객체가 리턴된다.

그리고 .catch(listener) 에서 listener 가 호출되어 Promise 객체가 리턴된다.

 

 

.then 에 .catch 가 체인으로 연결된 상황이다.

executor 실행시 예외발생을 Promise 에 통지한다. 여기서 예외가 발생하거나 reject 를 통지받으면 .then(onFulfillment) 로 간다. 예외 처리 핸들러가 없으면 다음 promise 객체에 전달한다. Promise 객체가 리턴되고 예외 통지한다. .catch(onRejection) 에서 onRejection 이 호출된다.

 

 

.then 4개 뒤에 .catch 가 체인으로 연결된 상황이다.

executor 실행시 예외발생을 Promise 에 통지한다. 여기서 예외가 발생하거나 reject 를 통지받으면 .then(onFulfillment) 로 전달한다. 여기에 예외 처리 핸들러가 없으면 리턴한 Promise 객체에 전달한다. 여기서 예외 통지를 .then(onFulfillment2) 로 한다. Promise 객체를 리턴하고 예외를 전달한다. 여기서 .then(onFulfillment3) 로 예외를 통지한다. 여기서 Promise 객체를 리턴하고 예외를 전달한다. 여기서 .catch(listener) 에 예외를 통지한다. listener 가 호출된다.

 

 

.then 2개 뒤에 .catch 2개 연결된 상황이다.

executor 실행시 예외 발생을 Promise 로 통지한다. 여기서 예외가 발생하거나 reject 를 통지받으면 .then(onFulfillment) 로 전달한다. 여기에 예외 처리 핸들러가 없으면 리턴한 Promise 객체에 전달한다. 여기서 예외 통지를 .then(onFulfillment2) 로 한다. Promise 객체를 리턴하고 예외를 전달한다. 여기서 .catch(listener) 의 listener 가 호출되어 Promise 객체가 리턴된다. 이미 여기서 예외를 처리했기 대문에 fulfill 통지 = 끝까지 간다! 여기서 .catch(listener) 는 호출되지 않는다.

 

 

.then 2개 뒤에 .catch 2개 뒤에 .then 2개 연결된 상황이다.

executor 실행시 예외 발생을 Promise 로 통지한다. 여기서 예외를 .then(cb) 로 전달하는데 Promise 객체를 리턴하고 예외를 전달한다. 여기서 .then(cb) 로 전달한다. Promise 객체를 리턴하고 예외를 전달한다. 여기서 .catch(cb) 로 전달한다. cb 가 호출되고 Promise 객체 리턴되며 fulfill 통지한다. 여기서 .catch(cb) 에서 cb 호출되지 않고 Promise 객체 리턴해 fulfill 전달한다. 여기서 .then(cb) 로 전달하며 cb 호출된다. Promise 객체 리턴하여 fulfill 전달한다. 여기서 .then(cb) 의 cb 호출된다.

 

 

 

jackson 라이브러리 추가

 

DispatcherServlet call Controller, Controller 가 값 리턴을 DispatcherServlet 에게 한다.

요청 파라미터 값 변환 ━변환→ request handler 의 파라미터 타입의 값

응답 형식에 맞는 타입의 값 ←변환━ 리턴 값

(변환 : HttpMessageConverter)

 

 

MappingJackson2HttpMessageConverter 사용하려면 아래 라이브러리 추가해야 한다.

central.sonatype.com 에서 'jackson-databind' 검색 해서 아래 코드 build.gradle 에 붙여넣고 $ gradle eclipse 한다.

JSON 라이브러리 설명이다.

    // JSON 형식을 다루는 라이브러리
    // @Controller가 붙은 일반적인 페이지 컨트롤러의 요청 핸들러를 실행할 때
    // 요청 파라미터의 문자열을 int나 boolean 등으로 바꾸기 위해
    // 기본으로 장착된 변환기를 사용한다.
    // 그 변환기는 HttpMessageConverter 규칙에 따라 만든 변환기이다.
    //
    // 또한 요청 핸들러가 리턴한 값을 문자열로 만들어 클라이언트로 출력할 때도
    // 이 HttpMessageConverter를 사용한다.
    // 즉 클라인트가 보낸 파라미터 값을 핸들러의 아규먼트 타입으로 바꿀 때도 이 변환기를 사용하고
    // 핸들러의 리턴 값을 클라이언트로 보내기 위해 문자열로 바꿀 때도 이 변환기를 사용한다.
    //
    // 스프링이 사용하는 기본 데이터 변환기는 MappingJackson2HttpMessageConverter 이다.
    // 만약 이 변환기가 없다면 Google의 Gson 변환기를 사용한다.
    // 구글의 Gson 변환기 마저 없다면 컨버터가 없다는 예외를 발생시킨다.
    // 컨버터가 하는 일은 JSON 데이터로 변환하는 것이다.
    //      클라이언트가 보낸 JSON 요청 파라미터 ===> 자바 객체
    //      핸들러가 리턴하는 자바 객체 ===> JSON 형식의 문자열
    //
    // MappingJackson2HttpMessageConverter?
    // => 요청 파라미터로 JSON 문자열을 받으면 요청 핸들러를 호출할 때 자바 객체로 변환시킨다.
    // => 요청 핸들러가 자바 객체를 리턴할 때 JSON 문자열로 변환한다.
    //
    // 주의!
    // => MappingJackson2HttpMessageConverter를 사용하려면
    //    다음과 같이 의존하는 라이브러리를 추가해야 한다.
    //
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'

    // => 그런데 JSON 데이터를 처리할 때
    //    MappingJackson2HttpMessageConverter 대신 GsonHttpMessageConverter 를 사용할 수 있다.
    //    단 GsonHttpMessageConverter를 사용하려면
    //    다음과 같이 이 클래스가 들어있는 의존 라이브러리를 추가해야 한다.
    // => 만약 동시에 추가한다면 기본으로 Jackson 라이브러리를 사용한다.
    //
    implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.9'

 

 

### 64. Back-end 와 Front-end 분리하기: 클라이언트 렌더링 방식으로 전환 
- 요청 핸들러에서 JSON을 리턴하는 방법 
- 웹 페이지에서 JSON을 받아 처리하는 방법

 

HelloController.java 생성한다.

@ResponseBody 애노테이션 설정해서 /web/app/hello 요청이 들어오면 message-body 에 바로 응답을 보낸다.

package bitcamp.myapp.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {

  @GetMapping(value = "/hello", produces = "text/plain;charset=UTF-8")
  @ResponseBody
  public String hello() throws Exception {
    Thread.sleep(5000);
    return "Hello, world! (안녕!)";
  }
}

 

Board.java 파일의 @JsonFormat 설정해서 Date 를 JSON 문자열로 변환할 때 사용할 규칙을 설정한다.

Member.java 의 createdDate 도 동일하게 적용한다.

package bitcamp.myapp.vo;

import java.sql.Date;
import java.util.List;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;

public class Board implements java.io.Serializable {
  private static final long serialVersionUID = 1L;

  private int no;
  private String title;
  private String content;
  private String password;

  // Jackson 라이브러리가 Date 타입 값을 JSON 문자열로 변환할 때 사용할 규칙을 설정한다.
  @JsonFormat(
      shape = Shape.STRING,
      pattern = "yyyy-MM-dd")
  private Date createdDate;

  private int viewCount;
  private int writerNo;
  private String writerName;
  private Member writer;
  private List<BoardFile> attachedFiles;
  
/* 후략 */


PageController 는 추후 HTML 에서 AJAX 로 요청하면 객체를 리턴하도록 변경한다.

/board 요청 아래의

/list 요청이 들어오면 List<Board> 객체를 리턴하도록 바꾼다.

/view 요청이 들어오면 Board 객체를 리턴하도록 바꾼다.

/insert 요청이 들어오면 게시물을 insert 하고 status : success 를 담은 Map 객체를 리턴한다.

package bitcamp.myapp.controller;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import bitcamp.myapp.service.BoardService;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
import bitcamp.myapp.vo.Member;

@Controller
@RequestMapping("/board")
public class BoardController {

  Logger log = LogManager.getLogger(getClass());

  {
    log.trace("BoardController 생성됨!");
  }

  // ServletContext 는 요청 핸들러의 파라미터로 주입 받을 수 없다.
  // 객체의 필드로만 주입 받을 수 있다.
  @Autowired private ServletContext servletContext;
  @Autowired private BoardService boardService;

  @GetMapping("form")
  public void form() {
  }

  @PostMapping("insert")
  @ResponseBody
  public Object insert(
      Board board,
      List<MultipartFile> files,
      Model model,
      HttpSession session) throws Exception{

    Member loginUser = (Member) session.getAttribute("loginUser");

    Member writer = new Member();
    writer.setNo(loginUser.getNo());
    board.setWriter(writer);

    List<BoardFile> boardFiles = new ArrayList<>();
    for (MultipartFile file : files) {
      if (file.isEmpty()) {
        continue;
      }

      String filename = UUID.randomUUID().toString();
      file.transferTo(new File(servletContext.getRealPath("/board/upload/" + filename)));

      BoardFile boardFile = new BoardFile();
      boardFile.setOriginalFilename(file.getOriginalFilename());
      boardFile.setFilepath(filename);
      boardFile.setMimeType(file.getContentType());
      boardFiles.add(boardFile);
    }
    board.setAttachedFiles(boardFiles);

    boardService.add(board);

    Map<String,Object> result = new HashMap<>();
    result.put("status", "success");

    return result;
  }

  @GetMapping("list")
  @ResponseBody
  public Object list(String keyword, Model model) {
    log.debug("BoardController.list() 호출됨!");
    List<Board> boards = boardService.list(keyword);

    // MappingJackson2HttpMessageConverter 가 jackson 라이브러리를 이용해
    // 자바 객체를 JSON 문자열로 변환하여 클라이언트로 보낸다.
    // 이 컨버터를 사용하면 굳이 UTF-8 변환을 설정할 필요가 없다.
    // 즉 produces = "application/json;charset=UTF-8" 를 설정하지 않아도 된다.
    return boards;
  }

  @GetMapping("view")
  @ResponseBody
  public Object view(int no, Model model) {
    return boardService.get(no);
  }
  
/* 후략 */

 

추후 게시판 URL은 /web/app/board/list 에서 /web/board/list.html 로 변경한다.

게시물 목록을 AJAX 이용해 /web/app/board/list 로 요청해서 가져온다.

view.html, form.html 도 동일하게 변경한다.

<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판</h1>

<div><a href='form.html'>새 글</a></div>

<table id="board-table" border='1'>
<thead>
  <tr>
    <th>번호</th> <th>제목</th> <th>작성자</th> <th>작성일</th> <th>조회수</th>
  </tr>
</thead>
<tbody></tbody>
</table>

<form action='list' method='get'>
  <input type='text' name='keyword' value="">
  <button>검색</button>
</form>

<script>
fetch("../app/board/list")
.then(response => {
  return response.json();
  // json()은 Promise 객체를 리턴한다.
  // Promise 객체가 하는 일:
  //   - 서버에서 응답 콘텐트를 받는 일을 한다.
  //   - 서버에서 받은 JSON 포맷의 문자열을 JavaScript 객체로 변환한다.
  //   - resolve()를 호출하여 다음 Promise 객체에 작업이 완료됐음을 통지한다.
  //     이때 변환된 JavaScript를 객체를 파라미터로 전달한다.
})
.then(boards => {
  let tbody = "";
  boards.forEach(board => {
    let html = `
  	  <tr>
  	     <td>${board.no}</td> 
  	     <td><a href='view.html?no=${board.no}'>${board.title == "" ? "제목없음" : board.title}</a></td> 
  	     <td>${board.writer.name}</td>
  	     <td>${board.createdDate}</td> 
  	     <td>${board.viewCount}</td>
  	  </tr>
  	`;
  	tbody += html;
  });
  document.querySelector("#board-table > tbody").innerHTML = tbody;
});
</script>
</body>
</html>

 

view.html

<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판</h1>

<div>
  <form id='board-form' action='update' method='post' enctype="multipart/form-data">
  <table border='1'>
  <tr>
    <th>번호</th>
    <td><input type='text' name='no' readonly></td>
  </tr>
  <tr>
    <th>제목</th>
    <td><input type='text' name='title'></td>
  </tr>
  <tr>
    <th>내용</th>
    <td><textarea name='content' rows='10' cols='60'></textarea></td>
  </tr>
  <tr>
    <th>작성자</th>
    <td><span id="f-writer-name"></span></td>
  </tr>
  <tr>
    <th>등록일</th>
    <td><span id="f-created-date"></span></td>
  </tr>
  <tr>
    <th>조회수</th>
    <td><span id="f-view-count"></span></td>
  </tr>
  <tr>
    <th>첨부파일</th>
    <td>
      <input type="file" name='files' multiple>
      <ul id="f-files"></ul>
    </td>
  </tr>
  </table>
  
  <div>
    <button id='btn-list' type='button'>목록</button>
    <button>변경</button>
    <button id='btn-delete' type='button'>삭제</button>
  </div>
  </form>
</div>


<script>
// http://localhost:8080/web/board/view.html?no=100
const values = location.href.split('?');
if (values.length == 1) {
  alert("URL이 옳지 않습니다.");
  location.href = "list.html";
}

// no=100
const param = values[1].split("=")
if (param.length == 1 || param[0] != 'no') {
  alert("URL이 옳지 않습니다.");
  location.href = "list.html";
}

let no = parseInt(param[1]);
if (isNaN(no)) {
  alert("URL이 옳지 않습니다.");
  location.href = "list.html";
}

fetch("../app/board/view?no=" + no)
.then(response => {
  return response.json();
})
.then(board => {
	console.log(board);
  document.querySelector("input[name='no']").value = board.no;
  document.querySelector("input[name='title']").value = board.title;
  document.querySelector("textarea[name='content']").value = board.content;
  document.querySelector("#f-writer-name").innerHTML = board.writer.name;
  document.querySelector("#f-created-date").innerHTML = board.createdDate;
  document.querySelector("#f-view-count").innerHTML = board.viewCount;
  
  let ul = "";
  board.attachedFiles.forEach(file => {
    console.log(file);
    if (file.no == 0) return;
    let html = `
      <li>
        <a href="../download/boardfile?fileNo=${file.no}">${file.originalFilename}</a>
        [<a href="filedelete?boardNo=${file.boardNo}&fileNo=${file.no}">삭제</a>]
      </li>`;
    ul += html;
  });
  document.querySelector("#f-files").innerHTML = ul;  
});

document.querySelector('#btn-list').onclick = function() {
  location.href = 'list.html';
}

document.querySelector('#btn-delete').onclick = function() {
  var form = document.querySelector('#board-form');
  form.action = 'delete';
  form.submit();
}
</script>

</body>
</html>

 

form.html

<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판</h1>

<div>
  <form id='board-form' method='post' enctype="multipart/form-data">
  <table border='1'>
  <tr>
    <th>제목</th>
    <td><input type='text' name='title'></td>
  </tr>
  <tr>
    <th>내용</th>
    <td><textarea name='content' rows='10' cols='60'></textarea></td>
  </tr>
  <tr>
    <th>첨부파일</th>
    <td>
      <input type="file" name='files' multiple>
    </td>
  </tr>
  </table>
  
  <div>
    <button id="btn-insert" type="button">등록</button>
    <button id='btn-cancel' type='button'>취소</button>
  </div>
  </form>
</div>


<script>
document.querySelector('#btn-insert').onclick = function() {
  const form = document.querySelector('#board-form');
  const formData = new FormData(form);
  
  fetch("../app/board/insert", {
    method: "post",
    body: formData
  })
  .then(response => {
    return response.json();
  })
  .then(result => {
    if (result.status == 'success') {
      location.href = 'list.html';
    } else {
      alert('입력 실패!');
    }
  });
  
}

document.querySelector('#btn-cancel').onclick = function() {
  location.href = 'list.html';
}
</script>

</body>
</html>

 

 

 


 

 

조언

 

*

 

 


 

과제

 

학습

- eomcs-java\eomcs-web