[Spring MVC] Thymeleaf: 서버 사이드 템플릿 엔진
Thymeleaf, spring thymeleaf, th:each, th:if, th:object, th:field, Thymeleaf fragment, th:fragment, th:replace, layout dialect
URL 표현식 (
정의
Thymeleaf = Java 서버 사이드 HTML 템플릿 엔진. natural template 철학 (브라우저에서 그대로 열어도 동작). Spring Boot 가 기본 권장 (Tiles, JSP, Velocity 후계).
Spring Boot 설정
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
| 위치 | 기본 |
|---|---|
templates/ | 템플릿 (resolve 시 자동 prefix) |
static/ | 정적 (CSS, JS, 이미지) |
| Suffix | .html |
| Encoding | UTF-8 |
| Cache | dev false, prod true |
기본 문법
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${pageTitle}">기본 제목</title>
</head>
<body>
<h1 th:text="${user.name}">홍길동</h1>
<p>나이: <span th:text="${user.age}">30</span>세</p>
<ul>
<li th:each="item, stat : ${items}"
th:class="${stat.first} ? 'first' : 'normal'">
<span th:text="${stat.count}">1</span>:
<span th:text="${item.name}">상품</span>
</li>
</ul>
</body>
</html>
th:text="${...}"가 가장 핵심. 기본 텍스트 (“홍길동”, “30”) 는 브라우저에서 직접 열었을 때만 보이는 자리표시자.
표현식 4종
| 종류 | 예 | 의미 |
|---|---|---|
${...} | ${user.name} | Variable (Model attribute) |
*{...} | *{name} | Selection (th:object 안의 필드) |
#{...} | #{login.title} | Message (i18n) |
@{...} | @{/users/{id}(id=${u.id})} | URL (context path 자동) |
~{...} | ~{fragments :: header} | Fragment |
URL 표현식 (@{...})
<!-- context path 자동 prefix -->
<a th:href="@{/users}">목록</a>
<a th:href="@{/users/{id}(id=${u.id})}">상세</a>
<!-- query string -->
<a th:href="@{/search(q=${query}, page=${page+1})}">다음</a>
<!-- 절대 URL -->
<img th:src="@{~/img/logo.png}" />
조건문
<div th:if="${user.admin}">관리자 메뉴</div>
<div th:unless="${user.admin}">일반 사용자</div>
<!-- switch -->
<div th:switch="${user.role}">
<p th:case="'admin'">관리자</p>
<p th:case="'editor'">에디터</p>
<p th:case="*">일반</p>
</div>
<!-- 삼항 -->
<span th:text="${user.active ? 'O' : 'X'}"></span>
<span th:class="${user.active ? 'badge-success' : 'badge-muted'}"></span>
반복 + iteration status
<tr th:each="u, stat : ${users}">
<td th:text="${stat.index}">0</td> <!-- 0부터 -->
<td th:text="${stat.count}">1</td> <!-- 1부터 -->
<td th:text="${stat.size}">100</td> <!-- 전체 크기 -->
<td th:text="${stat.even ? 'even' : 'odd'}"></td>
<td th:text="${stat.first ? '첫번째' : ''}"></td>
<td th:text="${stat.last ? '마지막' : ''}"></td>
<td th:text="${u.name}"></td>
</tr>
Spring 폼 통합
<form th:action="@{/signup}" th:object="${signupForm}" method="post">
<!-- th:field 가 자동으로 id, name, value 박음 -->
<input type="text" th:field="*{email}" />
<input type="password" th:field="*{password}" />
<input type="checkbox" th:field="*{newsletter}" />
<select th:field="*{country}">
<option th:each="c : ${countries}"
th:value="${c.code}"
th:text="${c.name}">한국</option>
</select>
<!-- 에러 표시 -->
<div class="error" th:if="${#fields.hasErrors('email')}"
th:errors="*{email}">에러 메시지</div>
<button type="submit">가입</button>
</form>
자세한 form binding 은 spring-mvc-form-handling.
Fragment (재사용)
<!-- templates/fragments/header.html -->
<header th:fragment="topbar(title)">
<h1 th:text="${title}">제목</h1>
<nav>...</nav>
</header>
<!-- 다른 페이지 -->
<body>
<div th:replace="~{fragments/header :: topbar('대시보드')}"></div>
<!-- 또는 th:insert (이 div 안에 fragment 삽입) -->
<div th:insert="~{fragments/header :: topbar(${pageTitle})}"></div>
</body>
| 디렉티브 | 의미 |
|---|---|
th:replace | 대체 (현재 태그가 fragment 로 교체) |
th:insert | 삽입 (현재 태그 안에 fragment) |
th:include (deprecated) | replace 권장 |
Layout (template inheritance)
implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect")
<!-- templates/layout/main.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<title layout:title-pattern="$CONTENT_TITLE - 사이트명">기본</title>
</head>
<body>
<div th:replace="~{fragments/header :: topbar(${pageTitle})}"></div>
<main layout:fragment="content">
여기에 페이지별 내용
</main>
<div th:replace="~{fragments/footer :: footer}"></div>
</body>
</html>
<!-- templates/users/detail.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/main}">
<head>
<title>사용자 상세</title>
</head>
<body>
<main layout:fragment="content">
<h2 th:text="${user.name}"></h2>
<p th:text="${user.email}"></p>
</main>
</body>
</html>
표준 객체 (Utility Objects)
| 객체 | 용도 |
|---|---|
#strings | #strings.toUpperCase(name), #strings.isEmpty(...) |
#numbers | #numbers.formatDecimal(price, 1, 2) |
#dates | #dates.format(date, 'yyyy-MM-dd') |
#temporals | Java 8 Date/Time |
#calendars | Calendar |
#objects | #objects.nullSafe(obj, default) |
#bools | #bools.isTrue(value) |
#arrays, #lists, #sets, #maps | 컬렉션 |
#fields | form 에러 |
#httpServletRequest | 직접 request |
#authentication | Spring Security |
Security 통합
<div xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<div sec:authorize="isAuthenticated()">
<span sec:authentication="name">사용자</span>
<a th:href="@{/logout}">로그아웃</a>
</div>
<div sec:authorize="hasRole('ADMIN')">
<a th:href="@{/admin}">관리자</a>
</div>
<div sec:authorize="!isAuthenticated()">
<a th:href="@{/login}">로그인</a>
</div>
</div>
Inline (script + JSON 데이터)
<script th:inline="javascript">
const user = /*[[${user}]]*/ {};
const pageData = /*[[${pageData}]]*/ null;
console.log(user.name);
</script>
Thymeleaf 가 JSON 변환 + JS 객체로 주입. SPA 의 초기 데이터 hydration 에 활용.
Live Reload (dev)
spring:
thymeleaf:
cache: false # dev only
devtools:
restart:
enabled: true
흔한 함정
WARNING
th:cache: true(prod) 안 켜면 매 요청 파일 read. 운영 환경 반드시.th:textvsth:utext= text 는 escape, utext 는 raw HTML. XSS 주의.th:field에러 =th:object의 form 객체가 Model 에 없음. 컨트롤러에서model.addAttribute("signupForm", new SignupForm())또는 빈 결과 시 다시 채움.*{}의 outsideth:object=*{name}은 th:object 밖에서 사용 불가.${signupForm.name}으로.
Thymeleaf vs JSP vs Mustache
| Thymeleaf | JSP | Mustache | |
|---|---|---|---|
| Spring Boot 권장 | 예 | 비추천 | 옵션 |
| Natural template | 예 | 아니오 | 부분 |
| 로직 | 표현식 + dialect | scriptlet (X) | logicless |
| 모더니즘 | 2026 표준 | legacy | 가볍지만 제한적 |
| 학습 곡선 | 중간 | 낮음 | 매우 낮음 |
관련 위키
이 글의 용어 (5개)
- [Spring MVC] Form Handling: @ModelAttribute, DataBinder, conversionspring
- 정의 Form Handling = HTML form 의 요청 파라미터 → Java 객체 변환 + 검증 + 에러 처리. 핵심: + + . 기본 흐름 자세한 BindingResult…
- [Spring MVC] Model + BindingResult: 뷰 모델과 바인딩 오류 처리spring
- 정의 Model = 컨트롤러 → 뷰 (Thymeleaf, JSP) 로 데이터 전달. BindingResult = 요청 파라미터 → 객체 바인딩 결과 + 오류. 둘 다 Spring…
- [Spring] Bean Validation: @Valid, @NotBlank, customspring
- 정의 Spring은 Jakarta Bean Validation (JSR-380)을 표준으로 사용. , , 등 어노테이션으로 선언적 검증. Spring MVC가 로 자동 통합. 설…
- [Spring] Security: Filter Chain, JWT, OAuth2spring
- 정의 Spring Security는 인증·인가의 사실상 표준. 서블릿 필터 체인 위에 다양한 인증/권한 처리를 모듈식으로. 6.0+ 부터 Lambda DSL 표준, Bean 방식…
- [Spring] Spring MVC: @RestController, @RequestMappingspring
- 정의 Spring MVC는 Servlet 기반 동기 웹 프레임워크. 이 요청을 받아 핸들러(컨트롤러 메서드)에 라우팅. REST API와 전통 MVC(HTML) 모두 지원. We…
💬 댓글