본문으로 건너뛰기
김신건의 로그

[Spring MVC] Thymeleaf: 서버 사이드 템플릿 엔진

· 수정 · 📖 약 2분 · 506자/단어 #spring #thymeleaf #template #view #mvc
Thymeleaf, spring thymeleaf, th:each, th:if, th:object, th:field, Thymeleaf fragment, th:fragment, th:replace, layout dialect

정의

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
EncodingUTF-8
Cachedev 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')
#temporalsJava 8 Date/Time
#calendarsCalendar
#objects#objects.nullSafe(obj, default)
#bools#bools.isTrue(value)
#arrays, #lists, #sets, #maps컬렉션
#fieldsform 에러
#httpServletRequest직접 request
#authenticationSpring 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

  1. th:cache: true (prod) 안 켜면 매 요청 파일 read. 운영 환경 반드시.
  2. th:text vs th:utext = text 는 escape, utext 는 raw HTML. XSS 주의.
  3. th:field 에러 = th:object 의 form 객체가 Model 에 없음. 컨트롤러에서 model.addAttribute("signupForm", new SignupForm()) 또는 빈 결과 시 다시 채움.
  4. *{}outside th:object = *{name}th:object 밖에서 사용 불가. ${signupForm.name} 으로.

Thymeleaf vs JSP vs Mustache

ThymeleafJSPMustache
Spring Boot 권장비추천옵션
Natural template아니오부분
로직표현식 + dialectscriptlet (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…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기