Thymeleaf 가이드

Thymeleaf 기본 문법, 표현식, 보안 주의사항

마지막 수정: 2026-05

Thymeleaf 가이드

기본 문법 요약

속성설명예시
th:text텍스트 출력 (HTML 이스케이프)th:text="${user.name}"
th:href링크 URL 바인딩th:href="@{/path}"
th:if조건부 렌더링th:if="${condition}"
th:unless반전 조건 렌더링th:unless="${user.admin}"
th:each반복 렌더링th:each="item : ${list}"
th:replace태그를 fragment로 교체th:replace="~{fragments/header :: header}"
th:insert태그 안에 fragment 삽입th:insert="~{fragments/header :: header}"
th:fragmentfragment 정의th:fragment="header"
th:class클래스 바인딩th:class="${active} ? 'active' : ''"
th:utextHTML 비이스케이프 출력th:utext="${doc.htmlContent}"

표현식

변수 표현식

<!-- ${variable} 형식으로 모델 변수 접근 -->
<span th:text="${user.name}"></span>
<span th:text="${#strings.abbreviate(content, 100)}"></span>

링크 표현식

<!-- @{/path} 형식으로 URL 생성 -->
<a th:href="@{/guide}">홈</a>
<a th:href="@{/guide/{section}/{page}(section=${item.sectionKey}, page=${item.pageKey})}">페이지</a>
<a th:href="@{/list(page=${page}, category=${category})}">목록</a>

<!-- 정적 리소스 -->
<link rel="stylesheet" th:href="@{/css/guide.css}">
<script th:src="@{/js/guide.js}"></script>

조건문

<!-- th:if: 조건이 true이면 렌더링 -->
<div th:if="${doc != null}">...</div>
<div th:if="${list != null and !list.isEmpty()}">...</div>

<!-- th:unless: 조건이 false이면 렌더링 -->
<div th:unless="${user.admin}">일반 사용자 전용</div>

반복문

<!-- th:each: 리스트 반복 -->
<li th:each="item : ${list}" th:text="${item.title}"></li>

<!-- stat 객체로 인덱스 접근 -->
<li th:each="item, stat : ${list}" th:text="${stat.index + 1} + '. ' + ${item.title}"></li>

Fragment (조각) 사용법

Fragment 정의

<!-- templates/fragments/header.html -->
<header th:fragment="header" class="site-header">
    <nav>...</nav>
</header>

th:replace로 조각 삽입 (권장)

<!-- 태그 자체를 fragment로 교체 -->
<th:block th:replace="~{fragments/header :: header}"></th:block>

th:insert로 조각 삽입

<!-- 태그 안에 fragment 삽입 -->
<div th:insert="~{fragments/header :: header}"></div>

파라미터 전달

<!-- 파라미터 있는 fragment 호출 -->
<th:block th:replace="~{fragments/alert :: alert('success', '저장되었습니다.')}"></th:block>

<!-- fragment 정의 -->
<div th:fragment="alert(type, message)"
     th:classappend="'alert-' + ${type}"
     class="alert">
    <span th:text="${message}"></span>
</div>

데이터 바인딩

<!-- 올바른 예 — th:text로 자동 이스케이프 -->
<span th:text="${user.name}"></span>

<!-- 신뢰된 서버 생성 HTML — th:utext 허용 (주석 필수) -->
<!-- 서버에서 렌더링된 마크다운 HTML, 사용자 입력 아님 -->
<div th:utext="${doc.htmlContent}"></div>

<!-- 속성 조건부 추가 -->
<a th:class="${item.slug == currentSlug} ? 'nav-link active' : 'nav-link'">링크</a>
<button th:disabled="${!canEdit}">수정</button>

th:utext 사용 기준

상황사용 여부
사용자 입력 데이터금지
관리자가 등록한 내부 콘텐츠허용 (주석 필수)
서버에서 생성한 HTML (마크다운 변환 등)허용

JavaScript에 서버 데이터 전달

<!-- 인라인 표현식 사용 (주석 처리로 IDE 오류 방지) -->
<script>
    window.GUIDE_CONFIG = {
        currentSlug: /*[[${currentSlug}]]*/ '',
        pageTitle: /*[[${pageTitle}]]*/ '',
    };
</script>

Thymeleaf 유틸리티

<!-- 날짜 포맷 -->
<span th:text="${#temporals.format(item.createdAt, 'yyyy-MM-dd')}"></span>
<span th:text="${#temporals.format(item.createdAt, 'yyyy년 MM월 dd일')}"></span>

<!-- 문자열 유틸 -->
<span th:text="${#strings.isEmpty(value) ? '없음' : value}"></span>
<span th:text="${#strings.abbreviate(title, 50)}"></span>

<!-- 숫자 포맷 -->
<span th:text="${#numbers.formatInteger(count, 3, 'COMMA')}"></span>

<!-- null 체크 조건부 렌더링 -->
<p th:if="${doc.description}" th:text="${doc.description}">설명</p>

새 페이지 작성 체크리스트

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>페이지 제목 - 개발 가이드</title>
    <link rel="stylesheet" th:href="@{/css/guide.css}">
</head>
<body>
    <!-- 페이지 콘텐츠 -->
    <script th:src="@{/js/guide.js}"></script>
</body>
</html>

ISMS-P 프론트엔드 보안 체크리스트

  • [ ] 사용자 입력 데이터 → th:text 이스케이프 처리
  • [ ] POST 폼 → CSRF 토큰 포함
  • [ ] 입력 필드 → maxlength 속성 지정
  • [ ] JavaScript → eval(), innerHTML 미사용
  • [ ] 외부 CDN → SRI(Subresource Integrity) 해시 적용 권장