카테고리 없음

Thymeleaf 가이드 - #1. Thymeleaf 소개

shaprimanDev 2025. 8. 25. 11:23
반응형

1장. Thymeleaf 소개

1.1 Thymeleaf란 무엇인가?

Thymeleaf는 웹 및 독립형 환경에서 사용할 수 있는 현대적인 서버사이드 Java 템플릿 엔진입니다. 2011년에 Daniel Fernández에 의해 개발되었으며, HTML, XML, JavaScript, CSS, 그리고 일반 텍스트를 처리할 수 있습니다.

핵심 개념

Thymeleaf의 핵심은 "Natural Templates" 개념입니다. 이는 템플릿 파일이 웹 브라우저에서 직접 열어도 올바르게 표시되는 유효한 HTML 문서라는 의미입니다. 이러한 특성은 디자이너와 개발자 간의 협업을 크게 향상시킵니다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Welcome Page</title>
</head>
<body>
    <h1 th:text="${welcomeMessage}">Welcome to our website!</h1>
    <p th:text="${description}">This is a sample description that shows in browser.</p>

    <div th:if="${user}">
        <p>Hello, <span th:text="${user.name}">Guest User</span>!</p>
        <p>Your role: <span th:text="${user.role}">User</span></p>
    </div>

    <div th:unless="${user}">
        <p><a href="/login">Please log in</a></p>
    </div>
</body>
</html>

예제 설명:

  • xmlns:th="http://www.thymeleaf.org": Thymeleaf 네임스페이스 선언
  • th:text="${welcomeMessage}": 서버에서 전달된 welcomeMessage 변수의 값으로 텍스트 교체
  • 브라우저에서 직접 열면 "Welcome to our website!"가 표시되지만, 서버에서 렌더링되면 실제 변수 값으로 교체됩니다
  • th:ifth:unless를 통한 조건부 렌더링

1.2 주요 특징과 장점

Natural Templates (자연스러운 템플릿)

가장 큰 특징으로, HTML 파일 자체가 완전히 유효한 HTML 문서입니다.

<!-- 일반적인 템플릿 엔진의 경우 -->
<p>Hello <%= user.getName() %>!</p>  <!-- 브라우저에서 깨짐 -->

<!-- Thymeleaf의 경우 -->
<p th:text="|Hello ${user.name}!|">Hello Guest!</p>  <!-- 브라우저에서 정상 표시 -->

설명:

  • 첫 번째 예제는 JSP 스타일로, 브라우저에서 직접 열면 스크립틀릿이 그대로 표시됩니다
  • 두 번째 예제는 Thymeleaf 방식으로, 브라우저에서는 "Hello Guest!"가 표시되고, 서버에서 렌더링되면 실제 사용자 이름이 표시됩니다

강력한 표현식 언어

Thymeleaf는 Spring Expression Language(SpEL)를 기반으로 한 풍부한 표현식을 제공합니다.

<!-- 기본 변수 표현식 -->
<p th:text="${user.name}">사용자명</p>

<!-- 선택 변수 표현식 (객체 선택) -->
<div th:object="${user}">
    <p>Name: <span th:text="*{name}">John</span></p>
    <p>Email: <span th:text="*{email}">john@example.com</span></p>
    <p>Age: <span th:text="*{age}">25</span></p>
</div>

<!-- 링크 URL 표현식 -->
<a th:href="@{/users/{id}(id=${user.id})}">View Profile</a>

<!-- 메시지 표현식 (국제화) -->
<p th:text="#{welcome.message}">Welcome message</p>

<!-- 복합 표현식 -->
<p th:text="|Hello ${user.name}, you have ${user.messageCount} messages|">
    Hello John, you have 5 messages
</p>

각 표현식 설명:

  • ${...}: 컨텍스트 변수에 접근
  • *{...}: 선택된 객체의 속성에 접근 (th:object와 함께 사용)
  • @{...}: URL 생성 (컨텍스트 경로 자동 추가, 파라미터 처리)
  • #{...}: 국제화 메시지
  • |...|: 문자열 리터럴 템플릿 (문자열 연결 간소화)

조건부 처리와 반복

<!-- 조건부 렌더링 -->
<div th:if="${user.isActive()}">
    <p th:text="|Welcome back, ${user.name}!|">Welcome back, John!</p>
</div>

<div th:unless="${user.isActive()}">
    <p class="warning">Your account is inactive.</p>
</div>

<!-- Switch 문 -->
<div th:switch="${user.role}">
    <p th:case="'admin'" class="admin">Administrator Access</p>
    <p th:case="'moderator'" class="mod">Moderator Access</p>
    <p th:case="*" class="user">Regular User Access</p>
</div>

<!-- 반복 처리 -->
<table>
    <tr th:each="product : ${products}">
        <td th:text="${product.name}">Product Name</td>
        <td th:text="${product.price}">$99.99</td>
        <td>
            <span th:if="${product.inStock}" class="available">Available</span>
            <span th:unless="${product.inStock}" class="soldout">Sold Out</span>
        </td>
    </tr>
</table>

<!-- 반복 상태 변수 활용 -->
<ul>
    <li th:each="item, stat : ${items}" 
        th:class="${stat.odd} ? 'odd-row' : 'even-row'"
        th:text="|${stat.index + 1}. ${item.title}|">
        1. Sample Item
    </li>
</ul>

설명:

  • th:if/th:unless: 조건에 따른 요소 표시/숨김
  • th:switch/th:case: 다중 조건 분기 (Java의 switch문과 유사)
  • th:each: 컬렉션 반복, 상태 변수(stat)로 인덱스, 카운트, 홀짝 여부 등 확인 가능

1.3 다른 템플릿 엔진과의 비교

Thymeleaf vs JSP

특징 Thymeleaf JSP
Natural Templates ✅ 완전한 HTML ❌ 스크립틀릿으로 인한 HTML 파괴
브라우저 미리보기 ✅ 가능 ❌ 불가능
디자이너 친화성 ✅ 매우 좋음 ❌ 어려움
성능 ⚠️ 상대적으로 느림 ✅ 빠름
학습 곡선 ⚠️ 중간 ✅ Java 개발자에게 친숙
<!-- JSP 예제 -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
    <% if (user != null) { %>
        <p>Hello <%= user.getName() %>!</p>
    <% } else { %>
        <p>Please log in</p>
    <% } %>
</body>
</html>

<!-- 동일한 기능의 Thymeleaf 예제 -->
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <p th:if="${user}" th:text="|Hello ${user.name}!|">Hello Guest!</p>
    <p th:unless="${user}">Please log in</p>
</body>
</html>

Thymeleaf vs Mustache

<!-- Mustache 예제 -->
<html>
<body>
    {{#user}}
        <p>Hello {{name}}!</p>
    {{/user}}
    {{^user}}
        <p>Please log in</p>
    {{/user}}
</body>
</html>

<!-- Thymeleaf 예제 (더 풍부한 표현식) -->
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <p th:if="${user}" 
       th:text="|Hello ${user.name?.toUpperCase() ?: 'Guest'}!|">
       Hello Guest!
    </p>
    <p th:unless="${user}">Please log in</p>
</body>
</html>

설명:

  • Mustache는 로직리스 템플릿으로 단순하지만 표현력이 제한적
  • Thymeleaf는 메소드 호출, Elvis 연산자(?:) 등 풍부한 표현식 지원

1.4 Natural Templates의 개념

Natural Templates는 Thymeleaf의 핵심 철학입니다. 이는 템플릿 파일이 템플릿 엔진 없이도 웹 브라우저에서 의미 있게 표시될 수 있다는 개념입니다.

전통적인 방식의 문제점

<!-- 전통적인 템플릿 (JSP, PHP 등) -->
<html>
<body>
    <h1><?= $title ?></h1>
    <% for(User user : users) { %>
        <p><%= user.getName() %> - <%= user.getEmail() %></p>
    <% } %>
</body>
</html>

이 파일을 브라우저에서 직접 열면 스크립트 코드가 그대로 표시되어 디자인 확인이 불가능합니다.

Thymeleaf의 Natural Templates 접근법

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:text="${pageTitle}">Default Page Title</title>
</head>
<body>
    <h1 th:text="${title}">Sample Page Title</h1>

    <!-- 사용자 목록 -->
    <div class="user-list">
        <div class="user-card" th:each="user : ${users}" th:remove="tag">
            <div class="user-info">
                <h3 th:text="${user.name}">John Doe</h3>
                <p th:text="${user.email}">john.doe@example.com</p>
                <span class="role" th:text="${user.role}">admin</span>
            </div>
        </div>

        <!-- 샘플 데이터 (실제 렌더링 시 제거됨) -->
        <div class="user-card">
            <div class="user-info">
                <h3>Jane Smith</h3>
                <p>jane.smith@example.com</p>
                <span class="role">user</span>
            </div>
        </div>
        <div class="user-card">
            <div class="user-info">
                <h3>Bob Johnson</h3>
                <p>bob.johnson@example.com</p>
                <span class="role">moderator</span>
            </div>
        </div>
    </div>
</body>
</html>

Natural Templates의 장점:

  1. 디자이너 친화적: HTML/CSS 디자이너가 실제 데이터 없이도 디자인을 확인할 수 있음
  2. 프로토타이핑: 백엔드 개발 전에 프론트엔드 작업이 가능
  3. 협업 향상: 디자이너와 개발자가 같은 파일로 작업 가능
  4. 유지보수성: HTML 구조가 명확하게 보임

1.5 사용 사례와 적용 분야

웹 애플리케이션

<!-- 전자상거래 상품 목록 -->
<div class="product-grid">
    <div class="product-card" th:each="product : ${products}">
        <img th:src="@{/images/products/{id}.jpg(id=${product.id})}" 
             th:alt="${product.name}"
             src="/images/placeholder.jpg" alt="Product Image">

        <h3 th:text="${product.name}">Sample Product</h3>

        <div class="price">
            <span th:if="${product.discount > 0}" class="original-price" 
                  th:text="${#numbers.formatCurrency(product.originalPrice)}">$99.99</span>
            <span class="current-price" 
                  th:text="${#numbers.formatCurrency(product.currentPrice)}">$79.99</span>
            <span th:if="${product.discount > 0}" class="discount" 
                  th:text="|${product.discount}% OFF|">20% OFF</span>
        </div>

        <button th:onclick="|addToCart(${product.id})|" 
                th:disabled="${!product.inStock}">
            <span th:text="${product.inStock} ? 'Add to Cart' : 'Out of Stock'">Add to Cart</span>
        </button>
    </div>
</div>

이메일 템플릿

<!-- 이메일 템플릿 -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="#{email.welcome.title}">Welcome Email</title>
</head>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
    <div class="header" style="background-color: #f8f9fa; padding: 20px;">
        <h1 th:text="#{email.welcome.header(${user.name})}">Welcome John!</h1>
    </div>

    <div class="content" style="padding: 20px;">
        <p th:text="#{email.welcome.message}">Thank you for joining our service.</p>

        <div class="account-info">
            <h3 th:text="#{email.account.details}">Account Details</h3>
            <ul>
                <li><strong th:text="#{email.username}">Username</strong>: 
                    <span th:text="${user.username}">john_doe</span></li>
                <li><strong th:text="#{email.email}">Email</strong>: 
                    <span th:text="${user.email}">john@example.com</span></li>
                <li><strong th:text="#{email.registered}">Registered</strong>: 
                    <span th:text="${#temporals.format(user.createdAt, 'MMM dd, yyyy')}">Jan 15, 2024</span></li>
            </ul>
        </div>

        <div class="action" style="text-align: center; margin: 30px 0;">
            <a th:href="@{https://oursite.com/activate/{token}(token=${activationToken})}" 
               style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px;"
               th:text="#{email.activate.button}">Activate Account</a>
        </div>
    </div>

    <div class="footer" style="background-color: #f8f9fa; padding: 20px; text-align: center; font-size: 12px;">
        <p th:text="#{email.footer.thanks}">Thank you for choosing our service!</p>
        <p>
            <a th:href="@{https://oursite.com}" th:text="#{email.footer.website}">Visit Website</a> |
            <a th:href="@{mailto:support@oursite.com}" th:text="#{email.footer.support}">Contact Support</a>
        </p>
    </div>
</body>
</html>

관리자 대시보드

<!-- 관리자 대시보드 통계 -->
<div class="dashboard-stats">
    <div class="stat-card" th:each="stat : ${dashboardStats}">
        <div class="stat-icon" th:class="|icon-${stat.type}|">
            <i th:class="${stat.iconClass}"></i>
        </div>
        <div class="stat-content">
            <h3 th:text="${stat.title}">Total Users</h3>
            <div class="stat-number" th:text="${#numbers.formatInteger(stat.value, 0, 'COMMA')}">1,234</div>
            <div class="stat-change" th:classappend="${stat.changePercent >= 0} ? 'positive' : 'negative'">
                <span th:text="|${stat.changePercent >= 0 ? '+' : ''}${stat.changePercent}%|">+5.2%</span>
                <span th:text="#{dashboard.from.lastmonth}">from last month</span>
            </div>
        </div>
    </div>
</div>

<!-- 최근 활동 로그 -->
<div class="recent-activity">
    <h2 th:text="#{dashboard.recent.activity}">Recent Activity</h2>
    <div class="activity-list">
        <div class="activity-item" th:each="activity : ${recentActivities}">
            <div class="activity-time" th:text="${#temporals.format(activity.timestamp, 'HH:mm')}">14:30</div>
            <div class="activity-content">
                <span th:text="${activity.description}">User logged in</span>
                <small th:text="${activity.userEmail}">user@example.com</small>
            </div>
            <div class="activity-status" th:class="|status-${activity.type}|">
                <span th:text="${activity.status}">Success</span>
            </div>
        </div>
    </div>
</div>

설명:

  • #numbers.formatInteger(): 숫자 포맷팅 (천 단위 콤마)
  • th:classappend: 기존 클래스에 조건부 클래스 추가
  • #temporals.format(): 날짜/시간 포맷팅
  • 복합적인 조건식과 표현식 활용으로 복잡한 UI 로직 처리

이러한 다양한 사례들을 통해 Thymeleaf가 단순한 웹 페이지부터 복잡한 대시보드까지 다양한 용도로 활용될 수 있음을 알 수 있습니다.

반응형