목표
1. controller에 게시판으로 이동하는 메소드를 추가한다.
2. handlebars와 bootstrap을 이용해 front 템플릿을 생성한다.
3. 서버에 api를 요청하여 하드코딩된 데이터를 받아와 화면에 뿌려준다.
이전에 만들어 두었던 HomeController에 게시판으로 이동하는 메소드를 추가한다.
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("msg", "Hello World!");
return "index";
}
@GetMapping("/anotherpage")
public ModelAndView buttonTest() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("index");
modelAndView.addObject("msg", "다른 페이지로 이동하였습니다!!!");
return modelAndView;
}
/**
* 추가한 메소드
*/
@GetMapping("/board")
public String board() {
return "board";
}
}
ViewResolver를 설정했었으므로 자동으로 WEB-INF/view/board.hbs를 찾아서 랜더링 할 것이다.
아래 구조로 만들어 질 것이다.
프론트 개발자도 아니고 Vue.js나 React.js를 자유자재로 다루지 못하기 때문에 익숙한 handlebars와 js, HTML, bootstrap을 이용하여 front는 간단하게 제작하도록 하겠다. UI/UX가 마음에 안드시는 분들은 적절히 변경하시길😔
또한 handlebars template에 대한 설명도 굳이 길게 적지 않을 것이니 필요하면 여기에서 자세히 확인하면 된다.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js"></script>
<!-- Load just-handlebar-helpers -->
<script src="https://unpkg.com/just-handlebars-helpers@1.0.16/dist/h.min.js"></script>
<script type="text/javascript"> // Register just-handlebars-helpers with handlebars
H.registerHelpers(Handlebars);
</script>
{{#block "header"}}{{/block}}
</head>
<body>
<div>
<div class="m-5">
{{#block "contents"}}{{/block}}
</div>
</div>
{{#block "js"}}{{/block}}
</body>
</html>
#block
은 #partial
과 한 쌍이다.
예를 들어, {{#partial "header"}} <title>Main Page</title> {{/partial}}
이라는 partial가 있을 때, 이는 {{#block "header"}}{{/block}}
이라고 명시된 block과 한 쌍이다. block으로 명시된 곳이 쌍이 맞는 partial 내부의 값으로 파싱된다.
따라서 해당 layout에는 header, contents, js라는 이름을 가진 partial가 필요하게되는데 이를 다른 .hbs
파일에서 정의하게 된다.
{{#partial "header"}}
<title>Main Page</title>
{{/partial}}
<!--body-->
{{#partial "contents"}}
<h1>어서오세요. 여기는 메인 페이지입니다.</h1>
<h1>{{msg}}</h1>
<button onclick='location.href="/freeboard01/anotherpage"' class='btn btn-secondary'>페이지 이동하기</button>
<button onclick='location.href="/freeboard01/board"' class='btn btn-primary'>게시판으로 이동하기</button>
{{/partial}}
<!--body-->
<!--js-->
{{#partial "js"}}
<script>
function test() {
console.log("test");
}
</script>
{{/partial}}
<!--js-->
{{> layout/layout}}
방금 이야기한 header, contents, js라는 이름을 가진 partial이 모두 나타나있고, 마지막에 layout/layout.hbs (.hbs
는 생략한다.)를 삽입하고 있는 모습이다. 정반대로 layout/layout.hbs에 index.hbs가 삽입되는 형식이 적절할 것 같은 기분이 들더라도🤔 모든 파일을 다 불러온 다음에, block과 partial을 쌍으로 파싱한다고 생각하면 된다.
{{#partial "header"}}
<title>Main Page</title>
{{/partial}}
<!--body-->
{{#partial "contents"}}
<h1>이곳은 게시판입니다.</h1>
<div id="tableSpace"></div>
<div id="pageMarkerSpace"></div>
<button onclick='location.href="/freeboard01"' class='btn btn-primary'>메인으로 이동하기</button>
{{/partial}}
<!--body-->
<!--js-->
{{#partial "js"}}
<!-- handlebars template 삽입 코드가 추가될 영역 -->
<script>
/* javascript가 추가될 영역 */
</script>
{{#block "helper"}}{{/block}}
{{/partial}}
<!--js-->
{{> static/helper/helper}}
{{> layout/layout}}
index.hbs
와 비교하여도 크게 달라진 것은 없다.
<!-- handlebars template 삽입 코드가 추가될 영역 -->
과 바로 아래 script 영역은 조금 있다가 다시 설명하도록 하겠다.
layout.hbs에만 #block
을 사용하는 것은 아니다. #block
은 얼마든지 중첩해서 사용할 수 있다.
위에선 static/helper/helper
를 가져와 #partial "helper"
와 #block "helper"
를 매칭한다.
✔️ 주입하는 방식
#partial
을 선언한 곳에#block
을 가진 파일을 주입하는 것 (board.hbs
에layout.hbs
를 주입)#block
을 선언한 곳에#partial
을 가진 파일을 주입하는 것 (board.hbs
에helper.hbs
를 주입)이 두 가지가 어떻게 다르고 왜 다르게 쓰는건지 헷갈릴 수가 있다.
layout(#block
을 선언한 곳)에 board를 주입해도 상관없다. 하지만 layout은 모든 페이지에서 사용될 것이고, 여기에 모든 페이지(index, board, login ... )를 주입하면 한 페이지가 가지는 코드의 양이 계속해서 늘어날 것이다.##layout.hbs {{> index}} {{> board}} {{> login}} ...
또한, 이런 경우에 만약 실수로 index.hbs에
{{> login}}
을 선언한다면 중복 선언으로 오류가 발생한다.
따라서 해당 페이지의 성격에 따라서 적절한 방식으로 선언하면 된다.
{{#partial "helper"}}
<script>
Handlebars.registerHelper('ifGreaterDateThanNow', function(v1, options) {
if(new Date(v1) > new Date()) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('for', function(from, to, incr, block) {
var accum = '';
for(var i = from; i <= to; i += incr)
accum += block.fn(i);
return accum;
});
Handlebars.registerHelper("math", function(lvalue, operator, rvalue, options) {
lvalue = parseFloat(lvalue);
rvalue = parseFloat(rvalue);
return {
"+": lvalue + rvalue,
"-": lvalue - rvalue,
"*": lvalue * rvalue,
"/": lvalue / rvalue,
"%": lvalue % rvalue
}[operator];
});
Handlebars.registerHelper("dateFormatter", function (date, options) {
var convertedDate = new Date(date);
var month = convertedDate.getMonth()+1;
var date = convertedDate.getDate();
var hours = convertedDate.getHours();
var minutes = convertedDate.getMinutes();
if(minutes == 0) {
minutes = '00';
}
return month+"월"+date+"일 "+hours+":"+minutes;
})
Handlebars.registerHelper("timeFormatter", function (date, options) {
var convertedDate = new Date(date);
var hours = convertedDate.getHours();
var minutes = convertedDate.getMinutes();
if(minutes == 0) {
minutes = '00';
}
return hours+":"+minutes;
})
</script>
{{/partial}}
커스텀 헬퍼이다.
핸들바는 언어나 프레임웍, 라이브러리가 아니기 때문에 템플릿으로써 아주 사소한 기능만 제공한다. 따라서 필요한 기능이 있다면 helper
를 사용해야하고 이는 오픈 소스로 공유되는 것도 많고, 커스텀 생성도 가능하다.
위는 프론트 사이드 랜더링에서만 사용할 수 있는 js로 만들어진 헬퍼이며, 핸들바는 서버 사이드 랜더링 또한 지원하므로 서버 사이드 랜더링으로 사용하기위해 java 코드로 작성하여 쓸 수도 있다.
board.hbs
에서 비워둔 부분을 채울 차례이다. 아래와 같이 채울 것이다.
<!--js-->
{{#partial "js"}}
<!-- handlebars template 삽입 코드가 추가될 영역 -->
{{> template/table}}
{{> template/pageMarker}}
<script>
/* javascript가 추가될 영역 */
var data = {
contents : [
{ user : "user1", title : "제목입니다." },
{ user : "user2", title : "안녕하세요." },
{ user : "user3", title : "테이블 완성~!" }
]
};
var page = {
startPage : 1,
endPage: 5,
totalPages: 5
}
window.onload = function () {
var template = Handlebars.compile($("#tableList").html());
if(typeof data === 'undefined') {
$("#tableSpace").html(template());
}else{
$("#tableSpace").html(template(data));
}
var template = Handlebars.compile($("#pageMarker").html());
$("#pageMarkerSpace").html(template(page));
}
</script>
{{#block "helper"}}{{/block}}
{{/partial}}
<!--js-->
handlebars에는 특이한 기능이 하나 있는데 바로 template
이다. 정확히는 <script id="템플릿 아이디" type="text/x-handlebars-template"></script>
으로 표기한다.
미리 handlebars로 쓰여진 HTML template을 만들어두고 데이터를 받아 랜더링 하는 기능이다.
따라서 이는 데이터보다 우선 읽어져야하므로, {{#partial "js"}}
의 script보다 윗쪽에 주입하도록 한다 .
var template = Handlebars.compile($("#tableList").html());
if(typeof data === 'undefined') {
$("#tableSpace").html(template());
}else{
$("#tableSpace").html(template(data));
}
위 코드가 돌아가는 순서는 다음과 같다.
1. 템플릿 아이디가 tableList
인 text/x-handlebars-template
을 컴파일하여 template
이라는 이름의 변수에 저장한다.
2. data가 정의되어 있다면 위에서 정의된 template
에 data를 넘겨 랜더링하여 반환한다.
3. 반환 받은 값을 tableSpace
이라는 아이디를 가진 태그에 html타입으로 붙인다.
template/table.hbs를 보자
<script id="tableList" type="text/x-handlebars-template">
<table class="table table-striped" style="width: 50% !important;">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">제목</th>
<th scope="col">글쓴이</th>
</tr>
</thead>
<tbody>
\{{#contents}}
<tr>
<th scope="row">\{{math @index '+' 1}}</th>
<td>\{{title}}</td>
<td>\{{user}}</td>
</tr>
\{{/contents}}
</tbody>
</table>
</script>
스크립트 type은 반드시 설정해주어야한다.
#contents
는 핸들바가 정의한 메소드가 아니며 단순한 key 혹은 Object name 정도로 생각하면 된다.
var data = {
contents : [
{ user : "user1", title : "제목입니다." },
{ user : "user2", title : "안녕하세요." },
{ user : "user3", title : "테이블 완성~!" }
]
};
이 데이터가 table.hbs 로 넘어가는데, 보다시피 key-value 형태이며, contents라는 key를 가지고 있다. value가 배열이므로 배열 수 만큼 <tr>...</tr>이 반복된다.
다음은 pageMarker.hbs이다.
<script id="pageMarker" type="text/x-handlebars-template">
\{{#if this}}
<div aria-label="Page navigation example">
<ul class="pagination mt-3">
<li class="page-item \{{#if (eqw startPage 1)}}disabled\{{/if}}">
<a class="page-link" id='prevPageBtn' \{{#if (eqw startPage 1)}}disabled="disabled"\{{/if}} data-pagenumber='\{{startPage}}' href="#">Previous</a>
</li>
\{{#for startPage endPage 1}}
<li class="page-item">
<a class="page-link pageBar" data-pagenumber='\{{this}}' href="#">\{{this}}</a>
</li>
\{{/for}}
<li class="page-item \{{#if (eqw endPage totalPages)}}disabled\{{/if}}">
<a class="page-link" id='nextPageBtn' \{{#if (eqw endPage totalPages)}}disabled="disabled"\{{/if}} data-pagenumber='\{{endPage}}' href="#">Next</a>
</li>
</ul>
</div>
\{{/if}}
</script>
for
와 eqw
는 헬퍼의 일종이며, script 선언 바로 아래의 this
는 받은 데이터 자체를 의미한다.
var page = {
startPage : 1,
endPage: 5,
totalPages: 5
}
위 템플릿에 넘어가는 데이터는 page이다. 따라서 startPage 등을 접근자 없이 {{#if this}}
내부에서 바로 접근 할 수 있다. (this.startPage
처럼 쓰지 않도록 주의한다.)
이제 톰캣 서버를 실행하면 다음과 같은 화면을 볼 수 있을 것이다.
게시판으로 이동하기
버튼을 클릭하면 아래의 페이지로 바뀔 것이다. 현재까지는 HTML 마크업이 제대로 됐는지 확인하기 위해 javascript에 하드코딩된 데이터를 사용하였다.
이제 비동기식으로 서버에 API 요청을 보내 데이터를 받아와 랜더링하도록 바꾸어 보겠다.
plugins {
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
/***
* test dependencies
* */
// https://mvnrepository.com/artifact/org.springframework/spring-test
testCompile group: 'org.springframework', name: 'spring-test', version: '5.2.5.RELEASE'
// https://mvnrepository.com/artifact/org.mockito/mockito-core
testCompile group: 'org.mockito', name: 'mockito-core', version: '3.1.0'
// https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
testCompile group: 'org.mockito', name: 'mockito-junit-jupiter', version: '3.1.0'
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.4.2'
// https://mvnrepository.com/artifact/org.hamcrest/hamcrest-all
testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'
/***
* web dependencies
* */
// https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
// https://mvnrepository.com/artifact/org.springframework/spring-webmvc
compile group: 'org.springframework', name: 'spring-webmvc', version: '5.2.5.RELEASE'
/**
* JSON converter
* */
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.10.3'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.3'
/***
* front template dependencies
*/
// https://mvnrepository.com/artifact/com.github.jknack/handlebars
compile group: 'com.github.jknack', name: 'handlebars', version: '4.0.6'
/*// https://mvnrepository.com/artifact/com.github.jknack/handlebars-jackson2
compile group: 'com.github.jknack', name: 'handlebars-jackson2', version: '4.0.6'*/
// https://mvnrepository.com/artifact/com.github.jknack/handlebars-springmvc
compile group: 'com.github.jknack', name: 'handlebars-springmvc', version: '4.0.6'
/***
* datebase dependencies
* */
// https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa
compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '2.2.6.RELEASE'
// https://mvnrepository.com/artifact/org.hibernate/hibernate-entitymanager
compile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '5.4.10.Final'
// https://mvnrepository.com/artifact/mysql/mysql-connector-java
compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.15'
/***
* lombok annotation dependencies
* */
// https://mvnrepository.com/artifact/org.projectlombok/lombok
compile group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
testCompileOnly("org.projectlombok:lombok:1.18.12")
annotationProcessor("org.projectlombok:lombok:1.18.12")
testAnnotationProcessor("org.projectlombok:lombok:1.18.12")
/***
* logger dependencies
* */
// https://mvnrepository.com/artifact/org.slf4j/slf4j-api
compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.30'
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
testCompile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
}
사실 꽤 많은 변동사항이 있었는데, Junit4를 Junit5
으로 변경하고 servlet-api
버전을 2.x에서 3.x로 변경하였다.
추가적으로 mockito
, hamcrest
등 테스트를 위한 종속들과 JSON 컨버팅을 위한 jackson
종속이 들어갔다.
각 버전에 따라 사용하는 어노테이션과 지원하는 메소드 등이 차이가 있으므로 꼭 버전 호환을 체크한 뒤에 사용하기를 바란다.
예를 들어, 이전 글에서 Junit4를 사용하면서 테스트 클래스에 @RunWith(SpringJUnit4ClassRunner.class)
를 사용했으나 Junit5로 업그레이드하면서 해당 어노테이션이 @ExtendWith(SpringExtension.class)
로 변경되었다.
본격적으로 서버 개발을 위해 api를 추가하도록 하겠다.
com/freeboard01
하위에 api
패키지를 추가하고, BoardApiController
, BoardDto
클래스를 추가한다.
BoardDto
는 BoardEntity를 가공하여 Client로 보내주는 객체이다.
com/freeboard01/domain
하위에 BoardService
를 추가한다. 이는 Controller에서 Repository에 직접 접근하지 않고 Service에서 필요한 로직을 수행하기위한 까닭이다.
즉 Controller - Service - Repository의 3-tier 아키텍처를 차용한다.
@RestController
@RequestMapping("/api/boards")
@RequiredArgsConstructor
public class BoardApiController {
private final BoardService boardService;
@GetMapping
public ResponseEntity<List<BoardDto>> get(){
List<BoardEntity> boardEntityList = boardService.get();
return ResponseEntity.ok(boardEntityList.stream().map(boardEntity -> BoardDto.of(boardEntity)).collect(Collectors.toList()));
}
}
@RestController : @Controller와 @RequestBody를 합친 어노테이션이며, @Controller 어노테이션을 포함하기 때문에 당연히 component-scan에서 빈으로 자동 등록된다.
@RequiredArgsConstructor : final 이 선언된 멤버변수를 파라미터로 받는 생성자를 자동 생성하는 어노테이션이다. 즉, 빈인 BoardApiController
는 파라미터로 또 다른 빈인 BoardService
를 가지는데 이를 생성자 주입 방식으로 자동 주입 받는 것이다. 이는 아래와 동일한 코드이다.
/** (참고용)
* 생성자 자동 생성 어노테이션을 빼고, 직접 생성자를 만들어 @Autowired로 연결하였다.
*/
@RestController
@RequestMapping("/api/boards")
public class BoardApiController {
private BoardService boardService;
@Autowired
public BoardApiController(BoardService boardService){
this.boardService = boardService;
}
@GetMapping
public ResponseEntity<List<BoardDto>> get(){
List<BoardEntity> boardEntityList = boardService.get();
return ResponseEntity.ok(boardEntityList.stream().map(boardEntity -> BoardDto.of(boardEntity)).collect(Collectors.toList()));
}
}
package com.freeboard01.api.board;
import com.freeboard01.domain.board.BoardEntity;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Getter
public class BoardDto {
private String user;
private String password;
private String contents;
private String title;
public BoardDto(BoardEntity board) {
this.user = board.getUser();
this.password = board.getPassword();
this.contents = board.getContents();
this.title = board.getTitle();
}
public static BoardDto of(BoardEntity board) {
return new BoardDto(board);
}
}
package com.freeboard01.domain.board;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional
public class BoardService {
public List<BoardEntity> get() {
List<BoardEntity> boardEntityList = new ArrayList<>();
for (int i = 1; i <= 5; ++i) {
BoardEntity boardEntity = BoardEntity.builder().user("user"+i+i+i).title("title"+i+i+i).contents("contents"+i+i+i).password("1234").build();
boardEntityList.add(boardEntity);
}
return boardEntityList;
}
}
원래대로라면 Service는 Repository 빈을 파라미터로 받아야 했으나, 지금은 ApiController의 작동을 확인하고 있기 때문에 위와 같이 하드코딩된 값을 사용하도록 하겠다.
우선 아래 코드를 추가한다.
<context:component-scan base-package="com.freeboard01.api" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
새로 만든 api 패키지 내의 @Controller
어노테이션을 가진 클래스만 빈으로 등록하겠다는 설정이다.
이전에 <mvc:annotation-driven />
으로 설정이 끝나있던 곳을 아래와 같이 변경한다.
<mvc:annotation-driven>
<mvc:path-matching trailing-slash="false"/>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
spring-webmvc 라이브러리의 spring-mvc.xsd
파일에 들어가보면 다양한 설정을 사용할 수 있게 만들어 두었는데, 아래와 같다.
이는 WebMvcConfigurer
를 상속받아 @Configuration
를 이용한 자바 어노테이션 스프링 설정 방식에 사용하는 메소드들과 일맥 상통하며 xml
에서도 동일하게 구현할 수 있다.
위의 코드에서는 path-matching
(trailing-slash : uri 마지막이 /
로 끝나면 이를 제거한 uri로 redirect한다.)과 message-converters
(JSON converting 설정)를 사용하였다.
ref. [Spring] <mvc:annotation-driven>
BoardApiController에서 shitf
+command
+t
를 이용해 테스트 클래스를 두개 만들었다. BoardApiControllerTest
는 @MockMvc를 이용해 Api요청 테스트를 할 것이고, 아래 BoardApiControllerUnitTest
는 @Mock을 이용해 BoardApiController 내의 메소드에 대한 유닛테스트를 할 것이다.
package com.freeboard01.api.board;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations={"file:src/main/webapp/WEB-INF/applicationContext.xml", "file:src/main/webapp/WEB-INF/dispatcher-servlet.xml"})
@Transactional
public class BoardApiControllerUnitTest {
@Autowired
private BoardApiController sut;
@Test
public void getTest(){
List<BoardDto> list = sut.get().getBody();
assertThat(list.size(), equalTo(5));
}
}
마치 테스트 코드가 안 돌아갈 것처럼 이렇게 붉은 줄이 그이겠지만 잘 돌아가니 신경쓰지말자.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/applicationContext.xml", "file:src/main/webapp/WEB-INF/dispatcher-servlet.xml"})
@Transactional
@WebAppConfiguration
public class BoardApiControllerTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mvc;
@BeforeEach
public void initMvc() {
mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void getTest() throws Exception {
mvc.perform(get("/api/boards")).andExpect(status().isOk());
}
}
ref. [Spring-mvc] standaloneSetup vs webAppContextSetup
두 개의 테스트 코드가 잘 돌아간다면 Postman으로도 확인해보자.
그럼 진짜 Client에서 Server로 데이터를 요청하여 받아온 뒤, 이를 화면에 뿌려주도록 하자!!
표시한 부분이 새로 추가되는 코드이다. Ajax를 이용해 비동기식으로 받아올 것이다.
테스트를 위해 콘솔에 데이터를 찍어본다.
var apiTest = () => {
$.ajax({
method : 'GET',
url : 'api/boards'
}).done(function (response) {
console.log(response);
})
}
버튼을 추가하여, 이를 클릭하면 API 요청을 보낸다.
실제로 서버에서 받아온 데이터를 이용해서 바로 테이블에 랜더링 해보겠다.
#tableList
template을 랜더링하는 작업을 attachBoard
함수 내부로 옮겨준다. 이 함수는 response
라는 파라미터를 가지는데 이를 이용해 data
라는 Object를 생성하고 있다.apiTest
였던 함수는 apiRequest
로 이름을 바꿔주었다. 자세히 보면 이전에 파라미터가 없던 곳에 callback = null
이라는 값이 들어갔음을 알 수 있다.response
를 담아 호출한다.attachBoard
는 비로소 이 때 호출된다.{{#partial "header"}}
<title>Main Page</title>
{{/partial}}
<!--body-->
{{#partial "contents"}}
<h1>이곳은 게시판입니다.</h1>
<div id="tableSpace"></div>
<div id="pageMarkerSpace"></div>
<button onclick='location.href="/freeboard01"' class='btn btn-primary'>메인으로 이동하기</button>
{{/partial}}
<!--body-->
<!--js-->
{{#partial "js"}}
{{> template/table}}
{{> template/pageMarker}}
<script>
var page = {
startPage : 1,
endPage: 5,
totalPages: 5
}
window.onload = () => {
apiRequest(attachBoard);
var template = Handlebars.compile($("#pageMarker").html());
$("#pageMarkerSpace").html(template(page));
}
var apiRequest = (callback = null) => {
$.ajax({
method : 'GET',
url : 'api/boards'
}).done(function (response) {
if(typeof callback != 'undefined'){
callback(response);
}
})
}
var attachBoard = (response) => {
var data = { contents : response }
var template = Handlebars.compile($("#tableList").html());
if(typeof data === 'undefined') {
$("#tableSpace").html(template());
}else{
$("#tableSpace").html(template(data));
}
}
</script>
{{#block "helper"}}{{/block}}
{{/partial}}
<!--js-->
{{> static/helper/helper}}
{{> layout/layout}}
톰캣을 다시 띄워준 뒤 게시판으로 이동하면 server의 BoardService$get()
에 하드코딩 해 둔 데이터를 이용해 테이블이 생성됐음을 확인할 수 있다.
java.lang.NoClassDefFoundError: javax/servlet/SessionCookieConfig
serlvet-api 버전이 너무 낮아서 발생 👉 2.5 → 3.x로 변경
Preferences - Build, Excution, Deployment - Build Tools - Gradle 👉 Run tests using (Default)Gradle → intelliJ IDEA로 변경해준다.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.freeboard01.api.board.BoardDto and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ArrayList[0])
Jackson converter는 getter를 이용해서 시리얼라이징하게 되어있음.
BoardDto를 보니 @getter
어노테이션이 빠져있어서 추가해주니 성공했다.