spring boot 에 친화적이라 하여 thymeleaf 를 사용했던 경험을 정리해보고자 한다.
thymeleaf 란?
서버사이드렌더링 방식의 템플릿 엔진 중의 하나로 JSP 역시 동일한 SSR 방식이다.
서버사이드렌더링은 클라이언트에서 요청 시 서버에서 사용자에게 표출할 페이지를 완전히 구성하여 전체 페이지를 렌더링하는 것을 뜻하며 페이지를 이동할 때마다 요청이 이루어진다.
thymeleaf 의 가장 큰 특징은 순수 HTML로 유지되는 Natural Template 이라는 점이다. JSP의 경우 전용 문법이 있기 때문에 화면을 보기 위해서 서버가 필요하지만 thymeleaf 는 HTML 형태로 되어 있어 서버의 도움 없이도 프로토 타입의 화면을 볼 수 있다. 웹 브라우저가 th 태그 같은 속성은 무시하고 HTML로 구성하여 띄워주기 때문이다.
thymeleaf layout 이란?
보통 페이지를 구성할 때 공통 영역으로 분류되는 Header, Footer, Navigation, Sidebar 에 대한 코드의 재사용이 가능할 수 있도록 layout을 구성해주는 라이브러리이다.
다음은 spring boot에서 thymeleaf 를 사용하기 위한 설정이다. maven 프로젝트 기준이다.
1. 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Maven Repository에서 버전별로 선택할 수 있는데 spring boot starter를 사용하면 버전 관리를 알아서 해주기 때문에 편하다.
더불어 layout 을 사용할 것이기 때문에 이에 해당하는 dependency도 추가해준다.
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
2. 기본경로 확인 및 생성
별로도 설정하지 않으면 기본경로는 src/main/resources/templates 이다. 디렉토리를 생성해준다.
만약 기본경로를 변경하고 싶다면 application.properties 파일에 아래 설정을 추가해준다.
spring.thymeleaf.prefix = classpath:/other/
classpath 는 src/main/resources 를 가리킨다.
이후 controller 에서 String으로 .html 파일의 파일명만 return 해주면 앞에 경로가 붙어서 해당 페이지로 이동하게 된다.
ModelAndView를 사용하여 setViewName에 파일명을 파라미터로 준 후 ModelAndView 객체를 return해도 동일하게 적용된다.
@GetMapping("/list")
public ModelAndView list(ModelAndView mav) {
/* src/main/resources/templates/pages 디렉토리의 list.html로 이동 */
mav.setViewName("/pages/list");
return mav;
}
3. 폴더 구성 및 html 파일 생성
기본경로 아래에 layout 구성을 위한 폴더를 생성한다.
- fragments : Header, Footer, Navigation, Sidebar와 같은 공통 영역에 들어갈 내용으로 구성된 파일을 넣는 폴더
- layouts : fragments 를 하나로 묶어주는 기본 파일을 넣는 폴더
- pages : 페이지마다 다르게 들어갈 content 파일을 넣는 폴더
pages 폴더를 제외하고 일단 공통 부분만 생성해두었다.
fragments
<!DOCTYPE html>
<html lang="ko" xmlns:th="httpl://www.thymeleaf.org">
<footer th:fragment="footerFragment" class="footer">
<ul class="list-inline">
<li class="list-inline-item">
<a class="text-muted" href="#">Support</a>
</li>
<li class="list-inline-item">
<a class="text-muted" href="#">Help Center</a>
</li>
<li class="list-inline-item">
<a class="text-muted" href="#">Privacy</a>
</li>
<li class="list-inline-item">
<a class="text-muted" href="#">Terms of Service</a>
</li>
</ul>
</footer>
</html>
footer.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="httpl://www.thymeleaf.org">
<nav th:fragment="navFragment" class="navbar">
<div class="navbar-collapse">
<ul class="navbar-nav navbar-align">
<li class="nav-item dropdown">
<a class="nav-icon dropdown-toggle" href="#" data-toggle="dropdown">
<i class="align-middle" data-feather="settings"></i>
</a>
<a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown">
<span class="text-dark" th:text="${session.user}"></span>
</a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/logout">Sign out</a>
</div>
</li>
</ul>
</div>
</nav>
</html>
nav.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="httpl://www.thymeleaf.org">
<nav th:fragment="sideFragment" id="sidebar" class="sidebar">
<div class="sidebar-content js-simplebar">
<a class="sidebar-brand" href="/">
<span class="align-middle mr-3">system</span>
</a>
<ul class="sidebar-nav">
<li class="sidebar-header separator">
System Manager
</li>
<li class="sidebar-item" th:classappend="${menu}=='a'?'active'">
<a href="/list_a" class="sidebar-link">
<i class="align-middle" data-feather="list"></i>
<span class="align-middle">A List</span>
</a>
</li>
<li class="sidebar-item" th:classappend="${menu}=='b'?'active'">
<a href="/list_b" class="sidebar-link">
<i class="align-middle" data-feather="folder"></i>
<span class="align-middle">B List</span>
</a>
</li>
</ul>
</div>
</nav>
</html>
side.html
세 개의 html에 공통으로 들어가는 부분은 다음과 같다.
<html lang="ko" xmlns:th="httpl://www.thymeleaf.org">
해당 html이 thymeleaf를 사용한다고 인식할 수 있도록 상단에 태그를 추가해준다.
<div th:fragment="fragment명">
</div>
해당 부분을 fragment로 사용하겠다는 태그를 추가해준다.
layouts
<!DOCTYPE html>
<html xmlns:th="httpl://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title th:text="${title}"></title>
<link rel="shortcut icon" th:href="@{/img/favicon.ico}">
<link href="/css/light.css" rel="stylesheet">
</head>
<body data-theme="default" data-layout="fluid" data-sidebar-position="left" data-sidebar-behavior="sticky">
<div class="wrapper">
<!-- Navigation -->
<nav th:replace="~{fragments/side :: sideFragment}"></nav>
<div class="main">
<!-- Search -->
<nav th:replace="~{fragments/nav :: navFragment}"></nav>
<!-- Main Content -->
<main layout:fragment="content"></main>
<!-- Footer -->
<footer th:replace="~{fragments/footer :: footerFragment}"></footer>
</div>
</div>
</body>
</html>
default.html
<html xmlns:th="httpl://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
마찬가지로 thymeleaf 사용 선언을 해주면서 layout 도 함께 추가한다.
<div th:replace="~{파일경로/파일명 :: fragment명}"></div>
그리고 body 부분에 각 fragment를 배치해준다. 위의 표현식은 파일 내부의 fragment 를 가져와서 replace 한다는 의미다.
여기까지 구성하면 기본 골격이 완성된 상태이다.
<!DOCTYPE html>
<html xmlns:th="httpl://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/default}">
<head>
<style type="text/css">
/* CSS */
</style>
</head>
<body>
<main layout:fragment="content" class="content">
<h1>A LIST</h1>
</main>
<script th:inline="javascript">
/* javascript */
</script>
</body>
</html>
/pages/list.html
<html xmlns:th="httpl://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/default}">
content 에 해당하는 html 파일에는 layout:decorate 를 추가하여 구성한 레이아웃을 불러와서 사용한다.
그리고 본문에 default.html 에 작성했던 layout:fragment="content" 를 태그에 추가해주면 해당 영역에 표출되는 것을 확인할 수 있다.
thymeleaf 에서 사용하는 표현식을 좀 더 공부하면 더 자유롭고 편리하게 코드를 작성할 수 있을 것 같다!