개발 가이드 프론트엔드 Thymeleaf 가이드
최종 수정:

Thymeleaf 가이드

Thymeleaf 템플릿 작성 규칙 및 보안 주의사항

Thymeleaf 가이드

새 페이지 작성 체크리스트

<!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>

폼 처리

<!-- CSRF 토큰 필수 (POST/PUT/PATCH/DELETE) -->
<form th:action="@{/submit}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

    <!-- 커맨드 객체 바인딩 -->
    <input type="text" th:field="*{title}" maxlength="200" required/>
    <textarea th:field="*{content}" maxlength="5000"></textarea>
    <select th:field="*{category}">
        <option th:each="opt : ${categories}"
                th:value="${opt.code}"
                th:text="${opt.name}"></option>
    </select>

    <!-- 검증 에러 표시 -->
    <span th:if="${#fields.hasErrors('title')}"
          th:errors="*{title}" class="error-msg"></span>
</form>

JavaScript에 서버 데이터 전달

<!-- 잘못된 예 — XSS 위험 -->
<script>var userId = [[${user.id}]];</script>

<!-- 올바른 예 — data 속성으로 전달 -->
<div id="app"
     data-user-id="[[${user.id}]]"
     data-api-url="[[${@environment.getProperty('app.api.url')}]]">
</div>
<script>
    const userId = document.getElementById('app').dataset.userId;
    const apiUrl = document.getElementById('app').dataset.apiUrl;
</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>

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

  • [ ] 사용자 입력 데이터 → th:text 이스케이프 처리
  • [ ] POST 폼 → CSRF 토큰 포함
  • [ ] 입력 필드 → maxlength 속성 지정
  • [ ] JavaScript → eval(), innerHTML 미사용
  • [ ] 개인정보 → 마스킹 처리 후 표시
  • [ ] 외부 CDN → SRI(Subresource Integrity) 해시 적용
AI 문서 검색

현재 페이지 내용을 기반으로 질문하세요.