반응형
5장. 표준 속성 (Standard Attributes)
5.1 텍스트와 HTML 출력 (th:text
, th:utext
)
th:text
- 안전한 텍스트 출력
th:text
는 가장 기본적인 Thymeleaf 속성으로, 텍스트를 안전하게 출력합니다. HTML 특수 문자들이 자동으로 이스케이프됩니다.
<div class="text-examples">
<!-- 기본 텍스트 출력 -->
<h1 th:text="${pageTitle}">기본 페이지 제목</h1>
<p th:text="${user.name}">사용자 이름</p>
<!-- HTML 특수문자 이스케이프 -->
<p th:text="${userInput}">사용자 입력: <script>alert('XSS')</script></p>
<!-- 숫자와 날짜 출력 -->
<p>나이: <span th:text="${user.age}">25</span>세</p>
<p>가입일: <span th:text="${#dates.format(user.joinDate, 'yyyy-MM-dd')}">2024-01-15</span></p>
<!-- null 값 처리 -->
<p th:text="${user.nickname ?: '닉네임 없음'}">닉네임</p>
<p th:text="${user.bio}">자기소개가 없습니다.</p>
<!-- 조건부 텍스트 -->
<span th:text="${user.isOnline()} ? '온라인' : '오프라인'">상태</span>
<span th:text="${order.isPaid()} ? '결제완료' : '미결제'">결제상태</span>
</div>
th:utext
- HTML 내용 출력
th:utext
는 HTML을 해석하여 출력합니다. 신뢰할 수 있는 HTML 내용에만 사용해야 합니다.
<div class="utext-examples">
<!-- 에디터에서 작성된 HTML 내용 -->
<div class="article-content" th:utext="${article.htmlContent}">
<p>기본 <strong>HTML</strong> 내용입니다.</p>
</div>
<!-- 마크다운을 HTML로 변환한 내용 -->
<div class="comment-body" th:utext="${comment.renderedContent}">
<p>렌더링된 댓글 내용</p>
</div>
<!-- 이메일 템플릿 -->
<div class="email-body" th:utext="${emailTemplate.body}">
<h2>안녕하세요!</h2>
<p>이메일 <em>내용</em>입니다.</p>
</div>
<!-- 조건부 HTML 출력 -->
<div th:utext="${user.isVip()} ? '<span class="vip-badge">VIP</span>' : ''"></div>
<!-- 국제화된 HTML 메시지 -->
<div th:utext="#{message.welcome.html(${user.name})}">
환영합니다, <strong>사용자</strong>님!
</div>
</div>
th:text
와 th:utext
보안 고려사항
<div class="security-examples">
<!-- 안전한 사용 - 사용자 입력은 th:text 사용 -->
<div class="user-comment">
<h4 th:text="${comment.author.name}">작성자</h4>
<p th:text="${comment.text}">댓글 내용</p>
<small th:text="${#dates.format(comment.createdAt, 'yyyy-MM-dd HH:mm')}">작성일</small>
</div>
<!-- 위험한 사용 - 사용자 입력을 th:utext로 출력하면 XSS 위험 -->
<!-- <div th:utext="${userInput}"></div> --> <!-- 절대 하지 말 것! -->
<!-- 안전한 HTML 출력 - 서버에서 검증된 내용만 -->
<div class="admin-notice" th:if="${adminNotice}" th:utext="${adminNotice.content}">
<p>관리자 공지사항입니다.</p>
</div>
<!-- HTML 태그 제거 후 출력 -->
<p th:text="${#strings.unescapeHtml(#strings.escapeJava(userBio))}">
HTML 태그가 제거된 자기소개
</p>
<!-- 조건부 HTML 이스케이프 -->
<div th:with="isAdminContent=${currentUser.isAdmin()}">
<div th:if="${isAdminContent}" th:utext="${content}">관리자 전용 HTML</div>
<div th:unless="${isAdminContent}" th:text="${content}">일반 텍스트</div>
</div>
</div>
5.2 속성 설정 (th:attr
, th:attrappend
, th:attrprepend
)
th:attr
- 일반적인 속성 설정
<div class="attr-examples">
<!-- 단일 속성 설정 -->
<img th:attr="src=@{/images/users/{id}.jpg(id=${user.id})}, alt=${user.name}">
<!-- 여러 속성 동시 설정 -->
<a th:attr="href=@{/user/{id}(id=${user.id})},
title=${user.name} + ' 프로필',
class=${user.isOnline()} ? 'user-link online' : 'user-link offline'">
<span th:text="${user.name}">사용자명</span>
</a>
<!-- 조건부 속성 설정 -->
<input type="text"
th:attr="placeholder=${#strings.isEmpty(user.name)} ? '이름을 입력하세요' : ${user.name},
maxlength=${user.isPremium()} ? '200' : '100'">
<!-- 데이터 속성 설정 -->
<div th:attr="data-user-id=${user.id},
data-role=${user.role},
data-premium=${user.isPremium()},
data-last-seen=${#dates.format(user.lastSeen, 'yyyy-MM-dd')}">
사용자 정보
</div>
<!-- 동적 ID와 클래스 -->
<div th:attr="id='user-' + ${user.id},
class='card ' + ${user.role} + ' ' + (${user.isActive()} ? 'active' : 'inactive')">
동적 속성
</div>
</div>
th:attrappend
- 속성 값 추가
<div class="attrappend-examples">
<!-- 클래스 추가 -->
<div class="card" th:attrappend="class=${user.isVip()} ? ' vip-card' : ''">
기본 카드에 VIP 클래스 추가
</div>
<div class="user-profile"
th:attrappend="class=${user.isOnline()} ? ' online' : ' offline'">
온라인 상태 클래스 추가
</div>
<!-- 스타일 추가 -->
<div style="padding: 10px;"
th:attrappend="style=${user.customColor} ? '; background-color: ' + ${user.customColor} : ''">
사용자 정의 색상 추가
</div>
<!-- 여러 조건부 클래스 추가 -->
<div class="post"
th:attrappend="class=${post.isPinned()} ? ' pinned' : '',
class=${post.isHot()} ? ' hot' : '',
class=${post.hasImage()} ? ' with-image' : ''">
게시글 상태 클래스들 추가
</div>
<!-- 데이터 속성 추가 -->
<button class="action-btn" data-action="like"
th:attrappend="data-count=' ' + ${post.likeCount},
data-user-liked=${currentUser.hasLiked(post)} ? ' user-liked' : ''">
좋아요
</button>
</div>
th:attrprepend
- 속성 값 앞에 추가
<div class="attrprepend-examples">
<!-- 클래스 앞에 추가 -->
<div class="content" th:attrprepend="class=${user.role} + ' '">
역할별 클래스를 앞에 추가
</div>
<!-- 경로 앞에 추가 -->
<img src="default.jpg"
th:attrprepend="src=${user.avatarPath} ? ${user.avatarPath} + '/' : '/images/'">
<!-- ID 접두사 추가 -->
<div id="content" th:attrprepend="id=${pageType} + '-'">
페이지 타입별 ID 접두사
</div>
<!-- 조건부 접두사 -->
<span class="badge"
th:attrprepend="class=${notification.isUrgent()} ? 'urgent-' : 'normal-'">
알림 배지
</span>
</div>
특정 속성 직접 설정
<div class="specific-attr-examples">
<!-- 자주 사용되는 속성들의 직접 설정 -->
<!-- th:href -->
<a th:href="@{/user/{id}(id=${user.id})}">프로필 보기</a>
<a th:href="@{/products(category=${category.id}, page=${currentPage})}">상품 목록</a>
<!-- th:src -->
<img th:src="@{/images/products/{id}.jpg(id=${product.id})}"
th:alt="${product.name}">
<!-- th:value -->
<input type="text" th:value="${user.name}" name="name">
<input type="hidden" th:value="${csrfToken}" name="_token">
<!-- th:id와 th:class -->
<div th:id="'section-' + ${section.id}"
th:class="'content-section ' + ${section.type}">
섹션 내용
</div>
<!-- th:title과 th:alt -->
<img th:src="@{/images/help.png}"
th:alt="#{help.icon.alt}"
th:title="#{help.icon.title}">
<!-- th:style -->
<div th:style="'width: ' + ${progress} + '%; background-color: ' + ${progressColor}">
진행률 바
</div>
<!-- th:disabled와 th:readonly -->
<input type="text" th:value="${user.email}"
th:readonly="${not user.canEditEmail()}">
<button th:disabled="${not user.canPerformAction()}"
th:text="#{action.submit}">제출</button>
<!-- th:checked와 th:selected -->
<input type="checkbox" th:checked="${user.receiveNewsletter}">
<select name="country">
<option th:each="country : ${countries}"
th:value="${country.code}"
th:text="${country.name}"
th:selected="${country.code == user.country}">국가</option>
</select>
</div>
5.3 조건부 렌더링 (th:if
, th:unless
, th:switch
)
th:if
와 th:unless
- 기본 조건부 렌더링
<div class="conditional-rendering">
<!-- 기본 조건부 표시 -->
<div th:if="${user != null}">
<h3>로그인 사용자 정보</h3>
<p th:text="${user.name}">사용자명</p>
</div>
<div th:unless="${user != null}">
<p><a th:href="@{/login}">로그인하세요</a></p>
</div>
<!-- 불리언 조건 -->
<div th:if="${user.isActive()}">
<span class="status active">활성 계정</span>
</div>
<div th:unless="${user.isActive()}">
<span class="status inactive">비활성 계정</span>
<p>계정을 활성화하려면 이메일을 확인하세요.</p>
</div>
<!-- 숫자 비교 -->
<div th:if="${user.age >= 18}">
<p>성인 인증이 완료되었습니다.</p>
<button>성인 콘텐츠 보기</button>
</div>
<div th:if="${user.score >= 100}">
<div class="achievement">
<h4>🏆 100점 달성!</h4>
<p>축하합니다!</p>
</div>
</div>
<!-- 문자열 조건 -->
<div th:if="${user.role == 'admin'}">
<nav class="admin-nav">
<a th:href="@{/admin}">관리자 패널</a>
<a th:href="@{/admin/users}">사용자 관리</a>
</nav>
</div>
<div th:unless="${#strings.isEmpty(user.bio)}">
<div class="user-bio">
<h4>자기소개</h4>
<p th:text="${user.bio}">자기소개</p>
</div>
</div>
</div>
복합 조건과 논리 연산
<div class="complex-conditions">
<!-- AND 조건 -->
<div th:if="${user.isActive() and user.isVerified()}">
<div class="full-access">
<p>모든 기능을 사용할 수 있습니다.</p>
</div>
</div>
<!-- OR 조건 -->
<div th:if="${user.isPremium() or user.score >= 90}">
<div class="premium-features">
<h4>프리미엄 기능</h4>
<ul>
<li>광고 제거</li>
<li>고급 통계</li>
<li>우선 지원</li>
</ul>
</div>
</div>
<!-- NOT 조건 -->
<div th:if="${not user.hasCompletedProfile()}">
<div class="profile-incomplete-warning">
<p>프로필을 완성해주세요!</p>
<a th:href="@{/profile/edit}">프로필 편집</a>
</div>
</div>
<!-- 복잡한 조건 -->
<div th:if="${(user.isActive() and user.isVerified()) or user.isAdmin()}">
<button>고급 설정</button>
</div>
<!-- 컬렉션 조건 -->
<div th:if="${not #lists.isEmpty(user.orders)}">
<div class="order-history">
<h4>주문 내역</h4>
<p>총 <span th:text="${#lists.size(user.orders)}">0</span>개의 주문</p>
</div>
</div>
<div th:unless="${#lists.isEmpty(notifications)}">
<div class="notifications">
<span class="badge" th:text="${#lists.size(notifications)}">5</span>
새 알림
</div>
</div>
</div>
th:switch
와 th:case
- 다중 분기
<div class="switch-examples">
<!-- 기본 switch 문 -->
<div th:switch="${user.role}">
<div th:case="'admin'">
<h3>관리자</h3>
<p>시스템 전체를 관리할 수 있습니다.</p>
<a th:href="@{/admin}" class="btn btn-danger">관리자 패널</a>
</div>
<div th:case="'moderator'">
<h3>운영자</h3>
<p>콘텐츠를 관리할 수 있습니다.</p>
<a th:href="@{/moderation}" class="btn btn-warning">운영 도구</a>
</div>
<div th:case="'premium'">
<h3>프리미엄 사용자</h3>
<p>모든 프리미엄 기능을 이용할 수 있습니다.</p>
<span class="badge badge-gold">PREMIUM</span>
</div>
<div th:case="*">
<h3>일반 사용자</h3>
<p>기본 기능을 이용할 수 있습니다.</p>
<a th:href="@{/upgrade}" class="btn btn-primary">업그레이드</a>
</div>
</div>
<!-- 숫자 기반 switch -->
<div class="grade-display" th:switch="${student.grade}">
<div th:case="'A'">
<span class="grade excellent">A학점</span>
<p>우수한 성적입니다! 🎉</p>
</div>
<div th:case="'B'">
<span class="grade good">B학점</span>
<p>양호한 성적입니다.</p>
</div>
<div th:case="'C'">
<span class="grade average">C학점</span>
<p>보통 성적입니다.</p>
</div>
<div th:case="*">
<span class="grade poor">재수강 필요</span>
<p>더 노력이 필요합니다.</p>
</div>
</div>
<!-- 주문 상태별 처리 -->
<div class="order-status" th:switch="${order.status}">
<div th:case="'PENDING'">
<span class="status pending">⏳ 처리 대기중</span>
<p>주문이 접수되었습니다.</p>
<button th:onclick="|cancelOrder(${order.id})|">취소</button>
</div>
<div th:case="'PROCESSING'">
<span class="status processing">⚙️ 처리중</span>
<p>주문을 준비하고 있습니다.</p>
</div>
<div th:case="'SHIPPED'">
<span class="status shipped">🚚 배송중</span>
<p>상품이 배송 중입니다.</p>
<p>운송장번호: <code th:text="${order.trackingNumber}">123456789</code></p>
</div>
<div th:case="'DELIVERED'">
<span class="status delivered">✅ 배송완료</span>
<p>배송이 완료되었습니다.</p>
<button th:onclick="|reviewOrder(${order.id})|">리뷰 작성</button>
</div>
<div th:case="'CANCELLED'">
<span class="status cancelled">❌ 취소됨</span>
<p>주문이 취소되었습니다.</p>
<p>환불은 3-5일 소요됩니다.</p>
</div>
<div th:case="*">
<span class="status unknown">❓ 상태 불명</span>
<p>주문 상태를 확인할 수 없습니다.</p>
</div>
</div>
<!-- 계절별 메시지 -->
<div th:switch="${#dates.month(#dates.createNow())}">
<div th:case="12" th:case="1" th:case="2">
<div class="season winter">
❄️ 겨울입니다. 따뜻하게 입으세요!
</div>
</div>
<div th:case="3" th:case="4" th:case="5">
<div class="season spring">
🌸 봄입니다. 꽃구경 어떠세요?
</div>
</div>
<div th:case="6" th:case="7" th:case="8">
<div class="season summer">
☀️ 여름입니다. 시원한 음료 어떠세요?
</div>
</div>
<div th:case="*">
<div class="season autumn">
🍂 가을입니다. 단풍구경 어떠세요?
</div>
</div>
</div>
</div>
조건부 CSS 클래스와 스타일
<div class="conditional-styling">
<!-- 조건부 클래스 적용 -->
<div th:class="${user.isOnline()} ? 'user-card online' : 'user-card offline'">
<span th:text="${user.name}">사용자명</span>
<span th:if="${user.isOnline()}" class="status-indicator">🟢</span>
<span th:unless="${user.isOnline()}" class="status-indicator">⚫</span>
</div>
<!-- 여러 조건부 클래스 -->
<article th:class="'post ' +
(${post.isPinned()} ? 'pinned ' : '') +
(${post.isHot()} ? 'hot ' : '') +
(${post.hasImages()} ? 'with-images ' : '') +
${post.category}">
<h3 th:text="${post.title}">게시글 제목</h3>
</article>
<!-- 조건부 스타일 -->
<div class="progress-bar">
<div th:style="'width: ' + ${task.progress} + '%; ' +
'background-color: ' + (${task.progress >= 100} ? 'green' :
${task.progress >= 70} ? 'orange' : 'red')">
</div>
</div>
<!-- 테마별 스타일 -->
<div th:style="${user.theme == 'dark'} ? 'background: #333; color: white;' :
(${user.theme == 'blue'} ? 'background: #e3f2fd; color: #1565c0;' :
'background: white; color: black;')">
사용자 정의 테마
</div>
</div>
반응형