학습노트4 - 머스테치로 화면 구성 하기

템플릿 엔진이란?

지정된 템플릿 양식과 데이터 합쳐 HTML 문서 출력

서버 템플릿 엔진과 클라이언트 템플릿 엔진

서버 템플릿 엔진: JSP, Freemarker
클라이언트 템플릿 엔진: 리액트(React), 뷰(Vue)의 View 파일

React.js, Vue.js

Single Page Application SPA 라고 함
Json 혹은 XML이 클라이언트로 전달 됨
최근 진화 되어 자바스크립트 프레임워크에서 서버 사이드 렌더링(Server Side Rendering) 도 가능

자바 진영 템플릿 엔진

다양한 서버 템플릿 엔진 이 존재

  1. JSP, Velocity: 스프링 부트에서 권장 X
  2. Freemarker: 과하게 많은 기능 지원
  3. Thymealeaf: 스프링 진영에서 밀고 있음. HTML 태크에 속성 문법 어려울수 있음 단 이런 방식을 쓰는 Vue.js 에 익숙한 개발자에겐 편함

머스테치 http://mustache.github.io

많은 언어 지원 하는 가장 심플한 템플릿 엔진

  1. 문법이 심플
  2. 로직 코드 사용 할수 없어 View역할만
  3. 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 사용 활용 해보기로 함, 실제 서비스는 외부 의존이 생김으로 자주 안 쓰임

구조 및 파일 작성

  1. 레이아웃 방식: 공통 영역을 별도의 파일로 분리 하여 필요한 곳에서 가져다 쓰는 방식
  2. resources/templates 에 layout 디렉토리 추가
  3. 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>

<!--index.js 추가-->
<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}}

Footer에 넣은 index.js 파일 생성

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 파일 추가

  1. readonly는 input 태크에 수정 안되게 해주는 속성
  2. 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', // PUT 사용
url: '/api/v1/posts/'+id, // 만든 api 을 호출
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>