템플릿 엔진이란, 지정된 템플릿 양식과 데이터가 합쳐져 HTML문서를 출력하는 소프트웨어를 이야기한다.
머스테치란
수많은 언어를 지원하는 가장 심플한 템플릿 엔진이다.
템플릿 엔진들의 단점
머스테치의 장점
머스테치를 사용하기 위해서는 플러그인을 설치해야 한다.
build.gradle에 의존성을 등록하자
dependencies {
implementation('org.springframework.boot:spring-boot-starter-mustache')
}
머스테치의 파일 위치는 기본적으로 src/main/resources/templates 이다.
src/main/resources/templates에 index.mustache를 생성하자
코드는 생략한다.
이 머스테치를 URL 매핑을 하기 위해 web 패키지 안에 IndexController를 생성하자
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public calss IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
머스테치 스타터 덕분에 컨트롤러에 문자열을 반환할 때 앞의 경로와 뒤의 파일 확자자는 자동으로 지정된다. 앞의 경로는 src/main/resources/templates로, 뒤의 파일 확장자는 .mustache가 붙는다.
IndexControllerTest를 생성하자
package com.jojoldu.book.springboot.web;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class IndexControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void 메인페이지_로딩() {
//when
String body = this.restTemplate.getForObject("/", String.class);
//then
assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");
}
}
이 테스트는 실제로 URL 호출 시 페이지의 내용이 제대로 호출되는지에 대한 테스트다.
header.mustache
와 footer.mustache
를 src/main/resources/templates/layout 경로에 생성한다.
참고
페이지 로딩 속도를 높이기 위해 css는 header에, js는 footer에 위치한다. HTML은 위에서부터 코드가 실행되기 때문에 head가 다 실행되고서야 body가 실행된다.
js는 body 하단에 두어 화면이 다 그려진 뒤에 호출하는 것이 좋다.
header와 footer가 있기 때문에 index.mustache의 코드가 변경된다.
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스</h1>
{{>layout/footer}}
-> 레이아웃으로 파일을 분리했으니 index.mustache에 글 등록 버튼을 추가하자.
코드생략
글 등록 버튼을 누르면 이동하는 페이지 주소는 /posts/save 이다.
이 주소에 해당하는 코드를 IndexController에 추가하자.
@GetMapping("/posts/save")
public String postsSave() {
return "posts-save";
}
posts-save.mustache를 추가해야한다. 파일의 위치는 index.mustache와 동일하다.
코드생략
현재 게시글 등록 화면에 등록 버튼은 기능이 없다.
src/main/resources에 static/js/app 디렉토리를 생성하고 index.js를 생성하자.
코드생략
index.js를 머스테치 파일이 사용할 수 있게 footer.mustache에 추가하자.
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<!--index.js 추가-->
<script src="/js/app/index.js"></script>
</body>
</html>
index.js 호출 코드를 보면 절대경로(/)로 바로 시작한다. 스프링 부트는 기본적으로 src/main/resources/static에 위치한 자바스크립트, CSS, 이미지 등 정적 파일들을 URL에서 /로 설정한다.
전체 조회를 위해 index.mustache의 UI를 변경한다.
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스 Ver.2</h1>
<div class="col-md-12">
<!-- 로그인 기능 영역 -->
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
</div>
</div>
<br>
<!-- 목록 출력 영역 -->
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
{{#posts}}
<tr>
<td>{{id}}</td>
<td>{{title}}</a></td>
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
</tbody>
</table>
</div>
{{>layout/footer}}
머스테치의 문법
{{#posts}}
{{id}}등의 {{변수명}}
기존에 있던 PostsRepository
인터페이스에 쿼리가 추가된다.
package com.jojoldu.book.springboot.domain.posts;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface PostsRepository extends JpaRepository<Posts, Long> {
@Query("select p from Posts p order by p.id desc")
List<Posts> findAllDesc();
}
참고
Querydsl을 추천하는 이유
- 타입 안정성이 보장된다.
: 단순한 문자열로 쿼리를 생성하는 것이 아니라, 메소드를 기반으로 쿼리를 생성하기 때문에 오타나 존재하지 않는 컬럼명을 명시할 경우 IDE에서 자동으로 검출해준다.- 래퍼런스가 많다.
PostsService에 코드를 추가하자
@Transactional(readOnly = true)
public List<PostsListResponseDto> findAllDesc() {
return postsRepository.findAllDesc().stream().map(PostsListResponseDto::new)
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
PostsListResponseDto 를 추가하자.
package com.jojoldu.book.springboot.web.dto;
import com.jojoldu.book.springboot.domain.posts.Posts;
import java.time.LocalDateTime;
import lombok.Getter;
@Getter
public class PostsListResponseDto {
private Long id;
private String title;
private String author;
private LocalDateTime modifiedDate;
public PostsListResponseDto(Posts entity) {
id = entity.getId();
title = entity.getTitle();
author = entity.getAuthor();
modifiedDate = entity.getModifiedDate();
}
}
마지막으로 IndexController를 수정하자
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.config.auth.LoginUser;
import com.jojoldu.book.springboot.config.auth.dto.SessionUser;
import com.jojoldu.book.springboot.service.posts.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequiredArgsConstructor
public class IndexController {
private final PostsService postsService;
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user) {
model.addAttribute("posts", postsService.findAllDesc());
return "index";
}
}
Model
게시글 수정
posts-update.mustache 를 생성하자
경로는 src/main/resources/templates이다.
코드생략
{{post.id}}
readonly
btn-update 버튼을 클릭하면 update 기능을 호출할 수 있게 index.js 파일에 update function을 추가하자.
코드생략
$('#btn-update').on('click')
전체 목록에서 수정페이지로 이동할 수 있게 index.mustache 코드를 수정하자.
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스 Ver.2</h1>
<div class="col-md-12">
<!-- 로그인 기능 영역 -->
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
</div>
</div>
<br>
<!-- 목록 출력 영역 -->
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
{{#posts}}
<tr>
<td>{{id}}</td>
<td><a href="/posts/update/{{id}}">{{title}}</a></td> // <- 수정 부분
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
</tbody>
</table>
</div>
{{>layout/footer}}
<a href="/posts/update/{{id}}></a>
IndexController에 메서드를 추가하자.
@GetMapping("/posts/update/{id}")
public String postsUpdate(@PathVariable Long id, Model model) {
PostsResponseDto dto = postsService.findById(id);
model.addAttribute("post", dto);
return "posts-update";
}
게시글 삭제
posts-update.mustache을 수정하자
코드생략
삭제 이벤트를 진행하기 위해 index.js를 수정하자
코드생략
PostsService 코드 추가
@Transactional
public void delete(Long id) {
Posts posts = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
postsRepository.delete(posts);
}
postsRepository.delete(posts)
PostsApiController 수정
@DeleteMapping("/api/v1/posts/{id}")
public Long delete(@PathVariable Long id) {
postsService.delete(id);
return id;
}
이로써 수정/삭제 기능까지 완성되었다.
전 이번장에서 버그가 제대로 걸려서 더이상 진행이 안되네요..
깃헙을 끼고살긴 했지만 혹시나해서 해당블로그 글을 처음부터 해당부분까지 코드비교해가며 다 읽어보고 해도 안되는거보면 여기까지인가 싶네요...후.....ㅠ