템플릿 엔진이란?
지정된 템플릿 양식과 데이터 합쳐 HTML 문서 출력
서버 템플릿 엔진과 클라이언트 템플릿 엔진
서버 템플릿 엔진: JSP, Freemarker
클라이언트 템플릿 엔진: 리액트(React), 뷰(Vue)의 View 파일
React.js, Vue.js
Single Page Application SPA
라고 함
Json 혹은 XML이 클라이언트로 전달 됨
최근 진화 되어 자바스크립트 프레임워크에서 서버 사이드 렌더링(Server Side Rendering) 도 가능
자바 진영 템플릿 엔진
다양한 서버 템플릿 엔진 이 존재
- JSP, Velocity: 스프링 부트에서 권장 X
- Freemarker: 과하게 많은 기능 지원
- Thymealeaf: 스프링 진영에서 밀고 있음. HTML 태크에 속성 문법 어려울수 있음 단 이런 방식을 쓰는 Vue.js 에 익숙한 개발자에겐 편함
많은 언어 지원 하는 가장 심플한 템플릿 엔진
- 문법이 심플
- 로직 코드 사용 할수 없어 View역할만
- Mustache.js 와 Mustache.java 2가지 있어, 하나의 문법으로 클라이언트/서버 템플릿 사용 가능
인텔리제이의 Plugin 설치
mustache 로 검색 하여 설치, 문법체크, HTML 문법 지원, 자동완성 됨
의존성
spring boot 에서 공식 지원 하는 것임으로 알수 있음
implementation 'org.springframework.boot:spring-boot-starter-mustache'
첫 예제, index.html
resources/templates/index.mustache
1 2 3 4 5 6 7 8 9 10
| <!DOCTYPE HTML> <html> <head> <title> 스프링 부트 웹서비스 </title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> </head> <body> <h1>스프링 부트로 시작하는 웹 서비스</h1> </body> </html>
|
1 2 3 4 5 6 7 8 9
| @Controller public class IndexController {
@GetMapping("/") String index() { return "index"; } }
|
게시판 화면 만들기
HTML 만 사용하기엔 멋이 없으니 부트 스트랩, 제이쿼리등 프로트엔드 라이브러리 사용
부트 스트랩, 제이쿼리 사용 방식
2가지 방식 존재: 외부 CDN 사용
, 직접 라이브러리 받아서 사용
외부 CDN 사용 활용 해보기로 함, 실제 서비스는 외부 의존이 생김으로 자주 안 쓰임
구조 및 파일 작성
- 레이아웃 방식: 공통 영역을 별도의 파일로 분리 하여 필요한 곳에서 가져다 쓰는 방식
- resources/templates 에 layout 디렉토리 추가
- footer.mustache, header.mustache 파일 생성
웹페이지는 위에서 아래로 로딩 되니 header에 CSS, body 제일 하단에 자바 스크립트.
css 가 적용 안된 깨진 화면을 사용자에게 안 보여주기 위함임
index.mustache 의 코드 변경
1 2 3
| {{>laytout/header}} <h1> 스프링 부트로 시작 하는 웹서비스 </h1> {{>laytout/footer}}
|
header.mustache 파일 생성
1 2 3 4 5 6 7 8 9
| <!DOCTYPE HTML> <html> <head> <title>스프링부트 웹서비스</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"> </head> <body>
|
footer.mustache 파일 생성
1 2 3 4 5 6 7
| <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>
<script src="/js/app/index.js"></script> </body> </html>
|
게시글 등록 HTML 생성
posts-save.mustache 파일 생성
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
| {{>layout/header}}
<h1>게시글 등록</h1>
<div class="col-md-12"> <div class="col-md-4"> <form> <div class="form-group"> <label for="title">제목</label> <input type="text" class="form-control" id="title" placeholder="제목을 입력하세요"> </div> <div class="form-group"> <label for="author"> 작성자 </label> <input type="text" class="form-control" id="author" placeholder="작성자를 입력하세요"> </div> <div class="form-group"> <label for="content"> 내용 </label> <textarea class="form-control" id="content" placeholder="내용을 입력하세요"></textarea> </div> </form> <a href="/" role="button" class="btn btn-secondary">취소</a> <button type="button" class="btn btn-primary" id="btn-save">등록</button> </div> </div>
{{>layout/footer}}
|
static/js/app 폴더에 생성
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 31 32
| var index = {
init : function () { var _this = this; $('#btn-save').on('click', function() { _this.save(); }); },
save : function () { var data = { title: $('#title').val(), author: $('#author').val(), content: $('#content').val() };
$.ajax({ type: 'POST', url: '/api/v1/posts', dataType: 'json', contentType: 'application/json; charset=utf-8', data: JSON.stringify(data) }).done(function () { alert('글이 등록 되었습니다.'); window.location.href = '/'; }).fail(function (error) { alert(JSON.stringify(error)); }); } };
index.init();
|
Controller에 URL mapping
URL 요청의 뷰 지정은 전부 IndexController.java 파일에 추가
1 2 3 4
| @GetMapping("/posts/save") String postsSave() { return "posts-save"; }
|
목록 List 나오게 수정
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 31
| {{>layout/header}} <h1>스프링 부트로 시작하는 웹 서비스</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> <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}}</td> <td>{{author}}</td> <td>{{modifiedDate}}</td> </tr> {{/posts}} </tbody> </table> </div> {{>layout/footer}}
|
DB Query 사용
FK 조인 및 복잡한 조건 일때 조회용 프레임워크 사용을 하기도 한다.
querydsl(타입 안정성 보장), jooq, myBatis
여기서는 간단히 JPA가 제공 하는 @Query 사용
1 2 3 4 5 6 7 8
| 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();
}
|
readOnly는 조회만 하는 메서드 에 넣어 주면 속도 개선
.map(PostsListResponseDto::new)
은 .map(posts -> new PostsListResponseDto(posts))
와 동일
1 2 3 4 5 6 7 8
| @Transactional(readOnly = true) public List<PostsListResponseDto> findAllDesc() { return postsRepository.findAllDesc().stream() .map(PostsListResponseDto::new) .collect(Collectors.toList()); }
|
이제 Controller 부분에 목록을 넘겨주면 된다.
Model model
은 템플릿 엔진 에서 사용하게 되는 객체이고 여기에 posts 에 목록을 담아서 넘겨주게 된다.
1 2 3 4 5 6 7 8 9 10 11
| @RequiredArgsConstructor @Controller public class IndexController {
private final PostsService postsService;
@GetMapping("/") String index(Model model) { model.addAttribute("posts", postsService.findAllDesc()); return "index"; }
|
Update 기능 추가
posts-update.mustache view 파일 추가
- readonly는 input 태크에 수정 안되게 해주는 속성
- btn-update 버튼 추가
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 31
| {{>layout/header}}
<h1>게시글 수정</h1>
<div class="col-md-12"> <div class="col-md-4"> <form> <div class="form-group"> <label for="title">글 번호</label> <input type="text" class="form-control" id="id" value="{{post.id}}" readonly> </div> <div class="form-group"> <label for="title">제목</label> <input type="text" class="form-control" id="title" value="{{post.title}}"> </div> <div class="form-group"> <label for="author"> 작성자 </label> <input type="text" class="form-control" id="author" value="{{post.author}}" readonly> </div> <div class="form-group"> <label for="content"> 내용 </label> <textarea class="form-control" id="content">{{post.content}}</textarea> </div> </form> <a href="/" role="button" class="btn btn-secondary">취소</a> <button type="button" class="btn btn-primary" id="btn-update">수정 완료</button> <button type="button" class="btn btn-danger" id="btn-delete">삭제</button> </div> </div>
{{>layout/footer}}
|
js file 에 btn-update 기능 추가
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
| ...
$('#btn-update').on('click', function () { _this.update(); });
... update : function () { var data = { title: $('#title').val(), content: $('#content').val() };
var id = $('#id').val();
$.ajax({ type: 'PUT', url: '/api/v1/posts/'+id, dataType: 'json', contentType:'application/json; charset=utf-8', data: JSON.stringify(data) }).done(function() { alert('글이 수정되었습니다.'); window.location.href = '/'; }).fail(function (error) { alert(JSON.stringify(error)); }); },
|
Index 뷰파일 수정
1 2 3 4 5 6
| <tr> <td>{{id}}</td> <td><a href="/posts/update/{{id}}">{{title}}</a></td> <td>{{author}}</td> <td>{{modifiedDate}}</td> </tr>
|