<!DOCTYPE html>
<html lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/default}">
<head>
<title>페이지 제목</title>
</head>
<body>
<main layout:fragment="content">
<!-- 페이지 콘텐츠 -->
</main>
<!-- 페이지 전용 스크립트가 있을 때만 추가 -->
<th:block layout:fragment="page-script">
<script>
// 이 페이지 전용 JS
</script>
</th:block>
</body>
</html>
일반 페이지 → layout:decorate="~{layout/default}"
어드민 페이지 → layout:decorate="~{layout/blank}"
공통 CSS/JS는 레이아웃이 자동 삽입 — 페이지에서 중복 선언 금지
데이터 바인딩
<!-- 올바른 예 — th:text로 자동 이스케이프 -->
<span th:text="${user.name}"></span>
<span th:text="${#strings.abbreviate(content, 100)}"></span>
<!-- 잘못된 예 — th:utext는 XSS 위험 -->
<span th:utext="${user.name}"></span>
<!-- URL 바인딩 -->
<a th:href="@{/posts/{id}(id=${post.id})}">보기</a>
<a th:href="@{/posts(page=${page}, category=${category})}">목록</a>
<!-- 조건부 렌더링 -->
<div th:if="${list != null and !list.isEmpty()}">...</div>
<div th:unless="${user.admin}">일반 사용자 전용</div>
<!-- 반복 -->
<li th:each="item, stat : ${list}" th:text="${stat.index + 1} + '. ' + ${item.title}"></li>
<!-- 속성 조건부 추가 -->
<li th:classappend="${item.active} ? 'active'"></li>
<button th:disabled="${!canEdit}">수정</button>
th:utext 사용 기준
상황
사용 여부
사용자 입력 데이터
금지
관리자가 등록한 내부 콘텐츠
허용 (주석 필수)
서버에서 생성한 HTML
허용
<!-- th:utext 불가피 사용 시 — 신뢰된 내부 콘텐츠임을 주석으로 명시 -->
<!-- 관리자 입력 콘텐츠, 사용자 입력 아님 -->
<div th:utext="${post.htmlContent}"></div>