๊ธฐ๋ณธ ํํ์(${...}, @{...}), th:text, th:each ๋ฐ๋ณต๋ฌธ, th:if ์กฐ๊ฑด๋ฌธ, ํผ ๋ฐ์ธ๋ฉ, ํ
ํ๋ฆฟ ์กฐ๊ฐ(th:replace), Spring Security ๋ฐ ์ค๋ฌด ํ ๋ ํผ๋ฐ์ค
## 1. ๊ธฐ๋ณธ ์ ์ธ & 5๋ ํํ์ ๋ฌธ๋ฒ (Syntax & Standard Expressions)
```html
<!DOCTYPE html>
<!-- Thymeleaf ๋ค์์คํ์ด์ค ์ ์ธ (IDE ์๋์์ฑ ๋ฐ ๋ฌธ๋ฒ ์ง์) -->
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ํ์๋ฆฌํ ๊ธฐ๋ณธ</title>
</head>
<body>
<!-- [1] ๋ณ์ ์ ${...} : ์ปจํ
์คํธ(Model)์ ๋ด๊ธด ๋ณ์ ์ ๊ทผ -->
<p th:text="${user.name}">ํ๊ธธ๋</p>
<!-- [2] ์ ํ ๋ณ์ ์ *{...} : th:object๋ก ์ง์ ๋ ๊ฐ์ฒด์ ๋ด๋ถ ํ๋ ์ถ์ฝ ์ ๊ทผ -->
<div th:object="${user}">
<p th:text="*{name}">์ด๋ฆ</p>
<p th:text="*{email}">์ด๋ฉ์ผ</p>
</div>
<!-- [3] ๋ฉ์์ง ์ #{...} : messages.properties ๋ค๊ตญ์ด ๋ฉ์์ง ํค ์กฐํ -->
<p th:text="#{welcome.message}">ํ์ํฉ๋๋ค</p>
<!-- [4] ๋งํฌ URL ์ @{...} : ์ปจํ
์คํธ ํจ์ค ๋ฐ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์๋ ์ธ์ฝ๋ฉ ๊ฒฐํฉ -->
<a th:href="@{/users/{id}(id=${user.id}, tab='profile')}">ํ๋กํ ๋ณด๊ธฐ</a>
<!-- [5] ์กฐ๊ฐ ์ ~{...} : ํ
ํ๋ฆฟ ๋ ์ด์์ ์ปดํฌ๋ํธ(fragment) ์ฝ์
-->
<div th:replace="~{fragments/header :: headerFragment}"></div>
</body>
</html>
```
## 2. ํ
์คํธ ์ถ๋ ฅ & ์์ฑ ๋ฐ์ธ๋ฉ (Text & Attribute Binding)
```html
<!-- [1] HTML ์ด์ค์ผ์ดํ ์ถ๋ ฅ (th:text) vs ์ด์ค์ผ์ดํ ํด์ (th:utext - ํ๊ทธ ํด์) -->
<p th:text="${rawHtml}"><b>๊ตต์ ๊ธ์จ</b></p> <!-- <b>๊ตต์ ๊ธ์จ</b> ๋ก ์ถ๋ ฅ -->
<p th:utext="${rawHtml}"><b>๊ตต์ ๊ธ์จ</b></p> <!-- ์ค์ ๋ณผ๋์ฒด HTML๋ก ๋ ๋๋ง (XSS ์ฃผ์!) -->
<!-- [2] ๋ฆฌํฐ๋ด ๋์ฒด ๋ฌธ๋ฒ (|...|) : ๋ฌธ์์ด ๊ฒฐํฉ์ ๋ฐ์ดํ์ + ์์ด ๊น๋ํ๊ฒ ์์ฑ -->
<span th:text="|์๋
ํ์ธ์, ${user.name}๋! ์ ์๊ถํ: ${user.role}|">์ธ์ฌ๋ง</span>
<!-- [3] HTML ํ์ค ์์ฑ ๋์ ๋ฐ์ธ๋ฉ -->
<input type="text" th:value="${user.name}" />
<img th:src="@{/images/{file}(file=${user.profileImg})}" alt="ํ๋กํ" />
<button th:disabled="${!user.isActive}">์ ์ก</button> <!-- false๋ฉด disabled ์์ฑ ์์ฒด ์ ๊ฑฐ -->
<!-- [4] ํด๋์ค ์ถ๊ฐ (th:classappend) -->
<div class="card" th:classappend="${user.isAdmin} ? 'border-primary' : ''">์นด๋</div>
```
## 3. ์กฐ๊ฑด๋ฌธ & ๋ถ๊ธฐ ์ฒ๋ฆฌ (Conditionals: th:if & th:switch)
```html
<!-- [1] th:if / th:unless (if๋ ์ฐธ์ผ ๋ ํ์, unless๋ ๊ฑฐ์ง์ผ ๋ ํ์) -->
<!-- null, false, 0, ๋น ์ปฌ๋ ์
๋ฑ์ ๋ชจ๋ false๋ก ํ๊ฐ๋จ -->
<span th:if="${user.age >= 20}" class="badge">์ฑ์ธ</span>
<span th:unless="${user.age >= 20}" class="badge">๋ฏธ์ฑ๋
์</span>
<!-- [2] ๋ค์ค ์กฐ๊ฑด ๋
ผ๋ฆฌ ์ฐ์ฐ (and, or, not / !) -->
<div th:if="${user.isActive and not user.isBlocked}">์ ์ ํ์</div>
<!-- [3] th:switch / th:case (๋ค์ค ๋ถ๊ธฐ ์ฒ๋ฆฌ) -->
<div th:switch="${user.role}">
<span th:case="'ADMIN'" class="badge-admin">์ต๊ณ ๊ด๋ฆฌ์</span>
<span th:case="'MANAGER'" class="badge-manager">์ด์์</span>
<span th:case="*" class="badge-user">์ผ๋ฐ ํ์</span> <!-- *๋ default(๊ธฐ๋ณธ๊ฐ) -->
</div>
```
## 4. ๋ฐ๋ณต๋ฌธ (Iteration: th:each & Status)
```html
<!-- th:each="๋จ์ผ์์๋ณ์, ์ํ๋ณ์ : ${์ปฌ๋ ์
}" -->
<table>
<thead>
<tr>
<th>No</th>
<th>์๋ฒ(Count)</th>
<th>์ด๋ฆ</th>
<th>์ํ</th>
</tr>
</thead>
<tbody>
<tr th:each="user, stat : ${users}" th:class="${stat.even} ? 'table-even' : 'table-odd'">
<td th:text="${stat.index}">0</td> <!-- 0๋ถํฐ ์์ํ๋ ์ธ๋ฑ์ค -->
<td th:text="${stat.count}">1</td> <!-- 1๋ถํฐ ์์ํ๋ ์นด์ดํธ -->
<td th:text="${user.name}">ํ๊ธธ๋</td>
<td>
<!-- stat ์ํ ํ๋กํผํฐ -->
<span th:if="${stat.first}">[์ฒซ๋ฒ์งธ]</span>
<span th:if="${stat.last}">[๋ง์ง๋ง]</span>
<span th:text="|์ด ${stat.size}๋ช
์ค|">์ด ์ธ์</span>
</td>
</tr>
</tbody>
</table>
```
## 5. ๋งํฌ & URL ์์ฑ (Link Expressions: @{...})
```html
<!-- [1] ๋จ์ ์ ๋/์๋ ๊ฒฝ๋ก (์ปจํ
์คํธ ํจ์ค ์๋ ๋ถ์ฐฉ) -->
<a th:href="@{/css/main.css}">์คํ์ผ์ํธ</a>
<!-- [2] ์ฟผ๋ฆฌ ์คํธ๋ง ํ๋ผ๋ฏธํฐ: /search?keyword=spring&page=2 -->
<a th:href="@{/search(keyword=${keyword}, page=2)}">๊ฒ์ ๊ฒฐ๊ณผ</a>
<!-- [3] ๊ฒฝ๋ก ๋ณ์ (Path Variable): /users/10/edit?mode=full -->
<a th:href="@{/users/{id}/edit(id=${user.id}, mode='full')}">์์ </a>
<!-- [4] ํ๋กํ ์ฝ ํฌํจ ์ธ๋ถ ์ ๋ URL -->
<a th:href="@{https://luckytechworld.com/docs}">๊ณต์ ๋ฌธ์</a>
```
## 6. ํผ ๋ฐ์ธ๋ฉ & ์ ํจ์ฑ ๊ฒ์ฆ (Form Binding & Validation)
```html
<!-- Spring MVC @ModelAttribute("userForm")์ ์ฐ๋ -->
<form th:action="@{/users/new}" th:object="${userForm}" method="post">
<!-- 1. th:field="*{ํ๋๋ช
}" : id, name, value๋ฅผ ์๋ ๋๊ธฐํ -->
<div class="form-group">
<label for="username">์ฌ์ฉ์๋ช
</label>
<input type="text" th:field="*{username}" class="form-control"
th:errorclass="field-error" /> <!-- ์๋ฌ ์กด์ฌ ์ field-error ํด๋์ค ์๋ ์ถ๊ฐ -->
<!-- Bean Validation ์๋ฌ ๋ฉ์์ง ์ถ๋ ฅ -->
<p th:if="${#fields.hasErrors('username')}"
th:errors="*{username}" class="error-msg">์ด๋ฆ ์ค๋ฅ ๋ฉ์์ง</p>
</div>
<!-- 2. ๋ผ๋์ค ๋ฒํผ / ์ฒดํฌ๋ฐ์ค ๋ฐ์ธ๋ฉ (์ ํ ์ํ ์๋ ์ฒดํฌ) -->
<div class="form-group">
<label th:each="role : ${roles}">
<input type="radio" th:field="*{role}" th:value="${role.name()}" />
<span th:text="${role.description}">์ญํ </span>
</label>
</div>
<button type="submit">๊ฐ์
ํ๊ธฐ</button>
</form>
```
## 7. ๋ ์ด์์ ์กฐ๊ฐ ๋ถํ & ๋ชจ๋ํ (Fragments: th:replace)
```html
<!-- [1] ์กฐ๊ฐ ์ ์ ํ์ผ: templates/fragments/common.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<!-- ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ๋ ํค๋ ์กฐ๊ฐ ์ ์ -->
<header th:fragment="mainHeader(pageTitle)">
<h1 th:text="${pageTitle}">๊ธฐ๋ณธ ํ์ดํ</h1>
<nav><a th:href="@{/}">ํ</a> | <a th:href="@{/about}">์๊ฐ</a></nav>
</header>
<!-- ํธํฐ ์กฐ๊ฐ ์ ์ -->
<footer th:fragment="mainFooter">
<p>© 2026 LuckyTechWorld. All rights reserved.</p>
</footer>
</body>
</html>
```
```html
<!-- [2] ์กฐ๊ฐ ์ฌ์ฉ ํ์ผ: templates/index.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<!-- th:replace: ํ์ฌ ํ๊ทธ(div)๋ฅผ ์กฐ๊ฐ ๋ด์ฉ์ผ๋ก ์์ ํ ๊ต์ฒด -->
<div th:replace="~{fragments/common :: mainHeader('๋ฉ์ธ ํ์ด์ง')}"></div>
<main>
<h2>ํ์ํฉ๋๋ค!</h2>
</main>
<!-- th:insert: ํ์ฌ ํ๊ทธ(div) ๋ด๋ถ์ ์์ ์์๋ก ์กฐ๊ฐ์ ์ฝ์
-->
<div th:insert="~{fragments/common :: mainFooter}"></div>
</body>
</html>
```
## 8. Spring Security ์ฐ๋ (Securing Thymeleaf)
```html
<!-- build.gradle: implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' -->
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!-- [1] ๋ก๊ทธ์ธ ์ฌ๋ถ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ -->
<div sec:authorize="isAuthenticated()">
<!-- ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์ ์ด๋ฆ ์ถ๋ ฅ -->
<span sec:authentication="name">ํ๊ธธ๋</span>๋ ํ์ํฉ๋๋ค!
<a th:href="@{/logout}">๋ก๊ทธ์์</a>
</div>
<div sec:authorize="isAnonymous()">
<a th:href="@{/login}">๋ก๊ทธ์ธ</a>
<a th:href="@{/register}">ํ์๊ฐ์
</a>
</div>
<!-- [2] ์ญํ (Role) ๋ฐ ๊ถํ(Authority) ์ธ๊ฐ ์ ์ด -->
<div sec:authorize="hasRole('ADMIN')">
<a th:href="@{/admin/dashboard}">๊ด๋ฆฌ์ ์ฝ์ ๋ฐ๋ก๊ฐ๊ธฐ</a>
</div>
</html>
```
## 9. ๊ธฐ๋ณธ ๋ด์ฅ ์ ํธ๋ฆฌํฐ ๊ฐ์ฒด (Standard Utility Objects)
```html
<!-- [1] #strings : ๋ฌธ์์ด ์กฐ์ -->
<p th:if="${#strings.isEmpty(user.bio)}">์๊ธฐ์๊ฐ๊ฐ ์์ต๋๋ค.</p>
<p th:text="${#strings.toUpperCase(user.name)}">LUCKY</p>
<p th:text="${#strings.abbreviate(user.description, 20)}">๊ธด ๊ธ ์ค์๋ง...</p>
<!-- [2] #temporals : Java 8 LocalDateTime ๋ ์ง/์๊ฐ ์์ํ -->
<!-- thymeleaf-extras-java8time ํ์ (Boot 3 ๊ธฐ๋ณธ ๋ด์ฅ) -->
<p th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm:ss')}">2026-09-27 12:00:00</p>
<p th:text="${#temporals.day(post.createdAt)}">27</p>
<!-- [3] #numbers : ์ซ์ ๋ฐ ํํ ํฌ๋งทํ
-->
<p th:text="${#numbers.formatDecimal(price, 0, 'COMMA', 2, 'POINT')}">1,250.00</p>
<p th:text="${#numbers.formatInteger(1234567, 3, 'COMMA')}">1,234,567</p>
<!-- [4] #lists : ์ปฌ๋ ์
ํฌ๊ธฐ ๋ฐ ํฌํจ ์ฌ๋ถ -->
<span th:text="|์ด ${#lists.size(items)}๊ฐ์ ์ํ|">์ด 5๊ฐ</span>
<div th:if="${#lists.contains(user.roles, 'VIP')}">VIP ํํ ์ ๊ณต</div>
```
## 10. ์๋ฐ์คํฌ๋ฆฝํธ ์ธ๋ผ์ธ (th:inline="javascript")
```html
<script th:inline="javascript">
/* [1] ์๋ฐ ๊ฐ์ฒด๋ฅผ ๋ธ๋ผ์ฐ์ JS ๊ฐ์ฒด/JSON์ผ๋ก ์์ ํ๊ฒ ์๋ ์ง๋ ฌํ */
const currentUser = /*[[${user}]]*/ null;
console.log("๋ก๊ทธ์ธ ์ ์ :", currentUser.name, currentUser.id);
/* [2] ๋ฌธ์์ด ์ด์ค์ผ์ดํ ๋ฐ ๊ฐ ํ ๋น */
const serverMessage = /*[[${message}]]*/ "๊ธฐ๋ณธ ๋ฉ์์ง";
/* [3] ์ธ๋ผ์ธ ์กฐ๊ฑด์ ํ์ฉ */
/*[# th:if="${user.isAdmin}"]*/
console.log("๊ด๋ฆฌ์ ๊ถํ ์คํฌ๋ฆฝํธ ์คํ");
/*[/]*/
</script>
```
## 11. Spring Boot application.yml ์ค์ (Configuration)
```yaml
spring:
thymeleaf:
prefix: classpath:/templates/ # ํ
ํ๋ฆฟ ํ์ผ ์ ์ฅ ๊ฒฝ๋ก
suffix: .html # ํ์ผ ํ์ฅ์
mode: HTML # ํ
ํ๋ฆฟ ๋ชจ๋ (HTML5)
encoding: UTF-8 # ํ์ผ ์ธ์ฝ๋ฉ
cache: false # โ ๏ธ ๋ก์ปฌ ๊ฐ๋ฐ ์ ํ
ํ๋ฆฟ ์ค์๊ฐ ๋ฐ์ (๋ฐฐํฌ ์์๋ true)
check-template-location: true # ํ
ํ๋ฆฟ ๋๋ ํ ๋ฆฌ ์กด์ฌ ์ฌ๋ถ ์์ ์ ํ์ธ
servlet:
content-type: text/html
```
## 12. ์ค๋ฌด ํ & ์ฃผ์์ฌํญ (Best Practices)
```text
# [1] ๋ด์ถ๋ด ํ
ํ๋ฆฟ (Natural Templates) ์์น
- Thymeleaf ํ์ผ์ ์์ HTML ๋ฌธ๋ฒ์ ์ ์งํ๋ฏ๋ก ๋ธ๋ผ์ฐ์ ์์ ์๋ฒ ์์ด ์ง์ ์ด์ด๋ ๊นจ์ง์ง ์๊ณ UI ํผ๋ธ๋ฆฌ์ฑ ๊ฒ์๊ฐ ๊ฐ๋ฅํจ.
- ์ ์ ๋์์ด๋ ์์
๋ฌผ๊ณผ ๋ฐฑ์๋ ํ
ํ๋ฆฟ์ ๊ฐ๊ทน์ ํ๊ธฐ์ ์ผ๋ก ์ค์ฌ์ค.
# [2] ํ์๋ฆฌํ ์ ์ฉ ์ฃผ์ ๋ฌธ๋ฒ
<!--/* ์ด ์ฃผ์์ ์๋ฒ ๋ ๋๋ง ์ ๋ธ๋ผ์ฐ์ HTML ์์ค์์ ์์ ํ ์ญ์ ๋จ (๋ณด์ ๋ฐ ๋ฏผ๊ฐ ์ ๋ณด ๋ณดํธ) */-->
<!-- ์ด ์ฃผ์์ ์ผ๋ฐ HTML ์ฃผ์์ผ๋ก ๋ธ๋ผ์ฐ์ [ํ์ด์ง ์์ค ๋ณด๊ธฐ]์ ๊ทธ๋๋ก ๋
ธ์ถ๋จ -->
# [3] ํ์๋ฆฌํ SpEL ์ธ์ ์
๋ณด์ ์ฃผ์
- ์ฌ์ฉ์๊ฐ ์ง์ ์
๋ ฅํ ๋ฌธ์์ด์ th:inline์ด๋ th:text ํํ์ ํ์ฑ ๋์์ผ๋ก ๋์ ์กฐ๋ฆฝํ์ง ๋ง ๊ฒ.
- ํญ์ ์ฌ์ ๊ฒ์ฆ๋ Model Attribute ๋ฐ์ธ๋ฉ์ ์ฌ์ฉํ ๊ฒ.
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์