첫 번째 시간에 말했듯이 thymeleaf 버전은 2.1이다. Spring Boot1.4인 경우에는 thymeleaf3 도 지원 하지만 thymeleaf 서드파트 라이브러리가 아직 많이 지원하지 않는거 같아서 일단 2.1로 했다. 그리고 다른거 templates 보다 자료들이 더 많은거 같아서 thymeleaf로 정했다.
Spring Boot 경우에는 따로 설정할 필요 없다. 클래스패스에 라이브러리만 있다면 boot의 자동설정이 알아서 기본설정을 해준다.
아래와 같이 spring-boot-starter-thymeleaf 를 디펜더시 받자.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
간단하게 hello world를 찍어보자.
public class IndexController {
@GetMapping("/")
public String home(Model model){
model.addAttribute("message", "hello world");
return "index";
}
message라는 키에 hello world를 넣었다.
spring boot의 경우에 resources/templates/ 아래의 경로에는 사용할 view templates 파일들을 넣으면 되고 resources/static 아래에는 정적 파일들을 넣으면 된다. 우리는 thymeleaf라는 view templates을 사용하므로 resources/templates에 html을 넣고 나머지 css나 js, 폰트 경우에는 static아래에 넣으면 된다.
resources/templates/ 아래 index.html만들어서 아래와 같이 넣어 보자.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
</head>
<body>
<div th:text="${message}">
</div>
</body>
</html>
실행 시켜 http://localhost:8080으로 접속하면 hello world 라는 페이지를 볼 수 있다. templates2.1인 경우에는 태그가 정확해야 된다. 만약 닫는 태그가 없다면 에러가 발생한다. 그런데 아마도 3.0부터는 그렇게 엄격하지 않았던거 같다. 대충 쓰는 법을 알았으니 이제 만들어보자.
첫번째 시간에도 말했듯이 clean blog 테마를 이용할 것이다.
https://startbootstrap.com/template-overviews/clean-blog/에 가서 다운로드 받자.
다운받은 파일을 압축을 푼후에 html 파일 경우에는 templates 아래에 넣고 나머지 css, img, js, vender 폴더은 static 아래에 넣자.
그리고 index.html 파일을 열어서 태크들을 잘 정리하자. 닫히지 않은 태크들을 다 닫아주고 특히 link 태그와 hr 태그 를 유심히 보자.
<link th:href="@{/vendor/bootstrap/css/bootstrap.min.css}" rel="stylesheet" />
와 같이 th 네임스페이스를 넣어도 되고 아니면 그냥 해도 상관은 없을 듯하다. 경로만 잘 맞으면 상관 없지 않을까? 얼추 태그들 정리가 다 되었다면 다시 실행 시켜보자. 만약 안될 경우에는 로그에 어떤 어떤 태그들을 닫히지 않았다고 나오니 로그를 잘 보자.
다시 서버를 실행시켜서 화면을 띄어보자. 그럼 아래와 같이 이쁜 화면이 나올 것이다.
이제 화면도 띄었으니 post를 가져와 보자.
아까 만든 IndexController 를 살짝 수정해보자.
@Controller
@RequiredArgsConstructor
public class IndexController {
private final PostRepository postRepository;
@GetMapping("/")
public String home(Model model, Pageable pageable){
model.addAttribute("posts", postRepository.findAll(pageable));
return "index";
}
}
블로그이니 페이징도 처리 해야 하므로 Pageable을 파라미터로 받으면 알아서 페이징을 해준다. thymeleaf 서드파트 라이브러리중 페이징처리를 해주는 라이브러리가 있는데 그걸 사용할 것이다.
<dependency>
<groupId>io.github.jpenren</groupId>
<artifactId>thymeleaf-spring-data-dialect</artifactId>
<version>2.1.1</version>
</dependency>
위와 같이 라이브러리를 디펜더시 받고 설정만 해주면 된다.
@Bean
public SpringDataDialect springDataDialect() {
return new SpringDataDialect();
}
이제 html을 보자. html에 보면 Main Content 라는 주석이 있다. 거기에 있던 내용을 아래와 같이 변경하자.
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<div class="post-preview" th:each="post : ${posts.content}">
<a th:href="@{/posts/ + ${post.id}}">
<h2 class="post-title" th:text="${post.title}">
</h2>
</a>
<p class="post-meta">Posted by <a href="#">Start Bootstrap</a> <strong th:text="${#temporals.format(post.regDate, yyyy-MM-dd)}"> </strong> category <strong th:text="${post.category.name}"></strong></p>
</div>
</div>
</div>
<div class="row" style="margin-top: 5%">
<div class="col-lg-3 col-lg-offset-1 col-md-10 col-md-offset-1">
<div sd:pagination-summary="">info</div>
</div>
<div class="col-lg-5 col-lg-offset-2 col-md-10 col-md-offset-1">
<nav class="pull-right">
<ul class="pagination" sd:pagination="full">
<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true"></span></a></li>
<li class="active"><a href="#">1 <span class="sr-only">(current)</span></a></li>
</ul>
</nav>
</div>
</div>
container 아래의 row는 실제 post의 제목이 나오는 부분이고 다음 row는 페이징 처리를 하는 부분이다. 페이징 처리부분은 문서에 나온 그대로 했다. 딱히 뭘 안해줘도 되는 듯 하다. 포스팅은 여러개가 나와야 되므로 each문 통해 loop를 돌리면 된다. thymeleaf서드파트 라이브러리중에 java8 datetime을 지원해주는 것이 또 있다. thymeleaf 서드파트 라이브러리가 많은듯하다.
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
위와 같이 메이븐을 디펜더시 받으면 된다. 딱히 설정은 하지 않아도 spring boot가 클래스 패스에 해당 라이브러리가 존재하면 알아서 설정해준다.
위와 같이
${#temporals.format(post.regDate, yyyy-MM-dd)}
이렇게 사용하면 된다.
jpa를 전혀 모른다면 딱히 의문점이 없는데 jpa를 아주 살짝 안다면 약간 의문점이 들 수 있다. 바로
${post.category.name}
이부분이다. post의 category를 접근 하는 이부분이 조금 의문점이 들 수도 있다.
다시 jpa로 넘어가보자.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CATEGORY_ID")
private Category category;
Post 도메인 중 category라는 필드가 있다. 저번에도 잠깐 언급했었지만 fetch = FetchType.LAZY 일 경우에는 지연로딩을 한다고 했다. 지연로딩은 해당 프로퍼티가 사용될 때 실제 쿼리를 날린다. 하지만 조건이 있는데 트랜잭션 안에서만 그게 가능하다. 트랜잭션 안에서는 마음껏 사용해도 되지만 트랜잭션 밖에서 사용하면 에러가 발생한다. 처음 jpa를 사용하면 자주 나오는 exception인 lazyinitializationexception이다. 이방법을 해결하기 위한것은 opensessioninview 이하 osiv 이라는 것인데 이거 또한 설명할 양이 꽤 된다. 간단하게 설명하자면 view까지 영속성을 확장 한다는 의미이다. 하지만 여기서 중요한것은 확장을 한다고 해도 트랜잭션안에서만 변경이 가능하다.
우리는 아무 설정도 하지 않았는데 lazyinitializationexception이 나오지 않았다. 그것은 기본적으로 Spring boot는 opensessioninview를 사용하고 있다. 그래서 에러가 나오지 않았던 것이다.
기본적인 데이터를 넣기위해서 아래와 같이 insert 쿼리를 만들자.
insert into category(ID, NAME, REG_DATE) values(1, spring, CURRENT_TIMESTAMP());
insert into category(ID, NAME, REG_DATE) values(2, java, CURRENT_TIMESTAMP());
insert into post(ID, TITLE, CODE, CONTENT, STATUS, REG_DATE, CATEGORY_ID) values(1, 테스트, 지금 포스팅은 테스트 포스팅 입니다., 지금 포스팅은 테스트 포스팅 입니다., Y,CURRENT_TIMESTAMP(), 1);
해당 sql을 resources 아래의 import.sql 파일을 만들면 서버를 실행할 때 해당 sql이 자동으로 실행 된다.
그럼 서버가 실행 되었다면 다시 웹페이지를 띄워 확인해보자. 우리가 원하는 테스트라는 제목으로 포스팅이 한개 되었다. 서버도 완벽하고 뷰도 완벽하다. 그런데 가만보면 조금 불필요한 코드들이 많다. 현재 있는 html 파일을 보면 비슷한 구석이 많다. 헤더와 푸터는 동일하다. 우리는 Main Content만 살짝 살짝 바꾸어 주면된다.
Thymeleaf Layout Dialect 이라는 기능있다. 공통된 부분은 따로 빼고 해당 body만 교체해주는 그런 기능이다. 예전에 필자는 tiles를 많이썼는데 거기에도 이런 기능이 존재 한다.
우리는 spring-boot-starter-thymeleaf 안에는 우리가 사용할 thymeleaf-layout-dialect 가 포함되어 있다. 그래서 따로 디펜더시를 받을 필요는 없다.
/resources/templates/layouts/ 아래 폴더를 만들고 그 아래 main.html을 만들자. 그리고 공통된 부분 헤더와 푸터를 넣어주자. 더 세밀하게 나눈다면 header footer 파일도 나누어도 되지만 여기선 간단하게 헤더 푸터를 main에 다 넣었다. 그리고 content만 바꾸는 방법으로 하였다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
...// 공통 스크립트
... //header html
<div layout:fragment="content">
</div>
... //footer html
위와 같이 xmlns:layout 네임 스페이스를 추가 하고 공통 부분을 제외한 body 부분은 layout:fragment 속성을 사용 하면 된다.
다음은 content 부분이다. 다시 index.html로 가서 공통부분(스크립트, 헤더, 푸터)은 다 삭제하고 우리가 원하는 post부분만 있으면 된다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sd="http://www.thymeleaf.org/spring-data"
layout:decorator="layouts/main" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
...//기타 meta/title 설정
<body>
<div class="container" layout:fragment="content">
... // 기존 post html
</div>
</body>
이렇게 완성되었다. 다시 서버를 재시작해보자. 그럼 아까와 동일한 화면이 나올 것이다.
post의 단건 화면도 만들어보자. 기본이 잘 되어 있어 아주 쉽게 만들 수있다. /resources/templates/post 폴더를 만들어서 post.html 파일을 아래와 같이 작성하자.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
layout:decorator="layouts/main" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
...//기타 meta/title 설정
<body>
<div class="container" layout:fragment="content">
<div class="row">
<h1 class="col-lg-10 col-lg-offset-1 col-md-10 col-md-offset-1" style="margin-bottom:3%" th:text="${post.title}">
</h1>
<div class="col-lg-10 col-lg-offset-1 col-md-10 col-md-offset-1" th:utext="${post.content}">
</div>
</div>
</div>
</body>
</html>
thymeleaf를 사용해 뷰를 만들어 보았다. 화면이 나오니까 이제 조금 뭘 한듯 하다.
다음시간에는 글을 쓸수있는 markdown을 만들어 보자. 오늘 소스는
여기에 올라가 있다.
1.
[spring-boot] 블로그를 만들자 (1)
[spring-boot] 블로그를 만들자. (2) JPA
[spring-boot] 블로그를 만들자. (3) Category 와 Comment
[spring-boot] 블로그를 만들자. (4) thymeleaf
[spring-boot] 블로그를 만들자. (5) markdown, catetory
[spring-boot] 블로그를 만들자. (6) 댓글과 Navigation
[spring-boot] 블로그를 만들자. (7) 캐시와 에러페이지
[spring-boot] 블로그를 만들자. (8) GitHub login
[spring-boot] 블로그를 만들자. (9) CI 와 배포