개발자입니다
[비트캠프] 85일차(18주차3일) - Spring Framework: myapp-64 중간(Client-rendering: Front-end / back-end 분리) 본문
[비트캠프] 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