반응형
3장. 기본 문법과 표현식
3.1 Thymeleaf 네임스페이스와 속성
Thymeleaf는 HTML 태그에 th:
접두사를 사용하여 동적 기능을 추가합니다. 이를 위해 HTML 문서의 루트 요소에 Thymeleaf 네임스페이스를 선언해야 합니다.
네임스페이스 선언
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${pageTitle}">Default Title</title>
</head>
<body>
<h1 th:text="${welcomeMessage}">Welcome!</h1>
</body>
</html>
설명:
xmlns:th="http://www.thymeleaf.org"
: Thymeleaf 네임스페이스 선언- 이 선언 없이도 Thymeleaf는 작동하지만, IDE의 자동완성과 문법 검사를 위해 권장
- HTML5 유효성 검사를 통과하며 브라우저에서 무시됨
주요 Thymeleaf 속성 개요
<div class="attribute-examples">
<!-- 텍스트 설정 -->
<p th:text="${message}">기본 텍스트</p>
<p th:utext="${htmlContent}">기본 HTML</p>
<!-- 속성 설정 -->
<input th:value="${userName}" th:placeholder="${userHint}">
<img th:src="@{/images/{filename}(filename=${user.avatar})}" th:alt="${user.name}">
<!-- 조건부 렌더링 -->
<div th:if="${user.isActive}">활성 사용자</div>
<div th:unless="${user.isActive}">비활성 사용자</div>
<!-- 반복 -->
<ul>
<li th:each="item : ${items}" th:text="${item.name}">아이템</li>
</ul>
<!-- 객체 선택 -->
<div th:object="${user}">
<span th:text="*{name}">사용자명</span>
<span th:text="*{email}">이메일</span>
</div>
</div>
3.2 Variable Expressions (${...}
)
Variable Expression은 컨텍스트에서 변수를 참조하는 가장 기본적인 표현식입니다.
기본 사용법
<!-- 컨트롤러에서 전달된 데이터 -->
<div class="user-info">
<h2 th:text="${user.name}">사용자명</h2>
<p th:text="${user.email}">이메일</p>
<p th:text="${user.age}">나이</p>
</div>
<!-- 컬렉션 크기 -->
<p>총 <span th:text="${#lists.size(products)}">0</span>개의 상품이 있습니다.</p>
<!-- 조건부 표현 -->
<p th:text="${user.age >= 18} ? '성인' : '미성년자'">연령 구분</p>
<!-- null 체크와 기본값 -->
<p th:text="${user.nickname != null} ? ${user.nickname} : ${user.name}">표시명</p>
<!-- Elvis 연산자 사용 -->
<p th:text="${user.nickname ?: user.name}">표시명</p>
컨트롤러 예제:
@Controller
public class UserController {
@GetMapping("/user")
public String userPage(Model model) {
User user = new User("김철수", "kim@example.com", 25);
user.setNickname("철수");
List<Product> products = Arrays.asList(
new Product("노트북", 1200000),
new Product("마우스", 30000),
new Product("키보드", 80000)
);
model.addAttribute("user", user);
model.addAttribute("products", products);
return "user";
}
}
중첩 객체와 메소드 호출
<div class="user-profile">
<!-- 중첩 객체 접근 -->
<h3 th:text="${user.profile.displayName}">프로필명</h3>
<p th:text="${user.address.city}">도시</p>
<!-- 메소드 호출 -->
<p th:text="${user.getFullName()}">전체 이름</p>
<p th:text="${user.isAdult()}">성인 여부</p>
<!-- 파라미터가 있는 메소드 호출 -->
<p th:text="${user.getFormattedAge('년')}">포맷된 나이</p>
<p th:text="${user.hasPermission('WRITE')}">쓰기 권한</p>
<!-- 컬렉션 인덱스 접근 -->
<p th:text="${user.hobbies[0]}">첫 번째 취미</p>
<p th:text="${user.scores['math']}">수학 점수</p>
<!-- Safe Navigation (null-safe) -->
<p th:text="${user.profile?.bio}">프로필 소개</p>
<p th:text="${user.address?.street?.toUpperCase()}">주소 (대문자)</p>
</div>
Java 모델 예제:
public class User {
private String firstName;
private String lastName;
private int age;
private Profile profile;
private Address address;
private List<String> hobbies;
private Map<String, Integer> scores;
public String getFullName() {
return firstName + " " + lastName;
}
public boolean isAdult() {
return age >= 18;
}
public String getFormattedAge(String suffix) {
return age + suffix;
}
public boolean hasPermission(String permission) {
return profile != null && profile.getPermissions().contains(permission);
}
}
컬렉션과 배열 처리
<div class="collections-example">
<!-- 리스트 접근 -->
<h4>상품 목록</h4>
<div th:each="product, stat : ${products}">
<p>
<span th:text="${stat.count}">1</span>.
<span th:text="${product.name}">상품명</span> -
<span th:text="${#numbers.formatCurrency(product.price)}">가격</span>
</p>
</div>
<!-- 맵 접근 -->
<h4>사용자 설정</h4>
<div th:each="setting : ${userSettings}">
<p>
<strong th:text="${setting.key}">설정명</strong>:
<span th:text="${setting.value}">설정값</span>
</p>
</div>
<!-- 배열 길이와 비어있음 검사 -->
<p th:if="${#arrays.isEmpty(user.tags)}">태그가 없습니다.</p>
<p th:unless="${#arrays.isEmpty(user.tags)}">
태그 개수: <span th:text="${#arrays.length(user.tags)}">0</span>개
</p>
<!-- 첫 번째와 마지막 요소 -->
<p th:if="${!#lists.isEmpty(products)}">
첫 상품: <span th:text="${products[0].name}">첫 상품</span><br>
마지막 상품: <span th:text="${products[#lists.size(products) - 1].name}">마지막 상품</span>
</p>
</div>
3.3 Selection Variable Expressions (*{...}
)
Selection Variable Expression은 th:object
로 선택된 객체의 속성에 접근할 때 사용합니다.
기본 사용법
<div class="user-form" th:object="${user}">
<!-- *{property}는 ${user.property}와 동일 -->
<h2 th:text="*{name}">사용자명</h2>
<p>이메일: <span th:text="*{email}">이메일</span></p>
<p>나이: <span th:text="*{age}">나이</span> 세</p>
<!-- 중첩된 객체도 사용 가능 -->
<div th:object="*{profile}">
<p>닉네임: <span th:text="*{nickname}">닉네임</span></p>
<p>소개: <span th:text="*{bio}">소개</span></p>
</div>
</div>
폼 바인딩에서의 활용
<form th:action="@{/user/update}" th:object="${userForm}" method="post">
<div class="form-group">
<label for="name">이름</label>
<input type="text" id="name" th:field="*{name}" th:value="*{name}">
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error">이름 오류</span>
</div>
<div class="form-group">
<label for="email">이메일</label>
<input type="email" id="email" th:field="*{email}">
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error">이메일 오류</span>
</div>
<div class="form-group">
<label for="age">나이</label>
<input type="number" id="age" th:field="*{age}" min="1" max="120">
<span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" class="error">나이 오류</span>
</div>
<!-- 체크박스 -->
<div class="form-group">
<label>
<input type="checkbox" th:field="*{newsletter}">
뉴스레터 수신 동의
</label>
</div>
<!-- 라디오 버튼 -->
<div class="form-group">
<label>성별:</label>
<label><input type="radio" th:field="*{gender}" value="M"> 남성</label>
<label><input type="radio" th:field="*{gender}" value="F"> 여성</label>
</div>
<!-- 선택 박스 -->
<div class="form-group">
<label for="role">역할</label>
<select id="role" th:field="*{role}">
<option value="">선택해주세요</option>
<option th:each="roleOption : ${roleOptions}"
th:value="${roleOption.value}"
th:text="${roleOption.label}">역할</option>
</select>
</div>
<button type="submit">저장</button>
</form>
컨트롤러에서의 폼 처리:
@PostMapping("/user/update")
public String updateUser(@ModelAttribute @Valid UserForm userForm,
BindingResult bindingResult,
Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("roleOptions", getRoleOptions());
return "user-form";
}
// 업데이트 로직
userService.updateUser(userForm);
return "redirect:/user/" + userForm.getId();
}
Selection Expression과 Variable Expression 혼용
<div th:object="${order}">
<h3>주문 정보</h3>
<p>주문번호: <span th:text="*{orderNumber}">12345</span></p>
<p>주문일자: <span th:text="*{orderDate}">2024-01-01</span></p>
<!-- Variable Expression과 혼용 -->
<p>주문자: <span th:text="${currentUser.name}">홍길동</span></p>
<p>총액: <span th:text="*{totalAmount}">50000</span>원</p>
<!-- 중첩된 Selection -->
<div class="shipping-info" th:object="*{shippingAddress}">
<h4>배송 정보</h4>
<p>수령인: <span th:text="*{recipientName}">수령인</span></p>
<p>주소: <span th:text="*{fullAddress}">주소</span></p>
<p>전화번호: <span th:text="*{phoneNumber}">전화번호</span></p>
</div>
<!-- 주문 상품 목록 -->
<div class="order-items">
<h4>주문 상품</h4>
<div th:each="item : *{orderItems}">
<div class="order-item" th:object="${item}">
<span th:text="*{productName}">상품명</span> -
<span th:text="*{quantity}">1</span>개 ×
<span th:text="*{unitPrice}">10000</span>원 =
<span th:text="*{totalPrice}">10000</span>원
</div>
</div>
</div>
</div>
3.4 Message Expressions (#{...}
)
Message Expression은 국제화(i18n) 메시지를 처리할 때 사용합니다.
메시지 파일 설정
# messages.properties (기본)
app.title=Thymeleaf 데모 애플리케이션
user.name=이름
user.email=이메일
user.age=나이
welcome.message=안녕하세요, {0}님!
item.count=총 {0}개의 항목이 있습니다.
validation.required={0}은(는) 필수 입력 항목입니다.
# messages_en.properties (영어)
app.title=Thymeleaf Demo Application
user.name=Name
user.email=Email
user.age=Age
welcome.message=Hello, {0}!
item.count=There are {0} items in total.
validation.required={0} is required field.
# messages_ja.properties (일본어)
app.title=Thymeleaf デモアプリケーション
user.name=名前
user.email=メール
user.age=年齢
welcome.message=こんにちは、{0}さん!
item.count=合計{0}個のアイテムがあります。
validation.required={0}は必須項目です。
기본 메시지 사용
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{app.title}">기본 제목</title>
</head>
<body>
<h1 th:text="#{app.title}">애플리케이션 제목</h1>
<form class="user-form">
<div class="form-group">
<label th:text="#{user.name}" for="name">이름</label>
<input type="text" id="name" name="name">
</div>
<div class="form-group">
<label th:text="#{user.email}" for="email">이메일</label>
<input type="email" id="email" name="email">
</div>
<div class="form-group">
<label th:text="#{user.age}" for="age">나이</label>
<input type="number" id="age" name="age">
</div>
</form>
<!-- 파라미터가 있는 메시지 -->
<p th:text="#{welcome.message(${user.name})}">환영 메시지</p>
<p th:text="#{item.count(${#lists.size(items)})}">아이템 개수</p>
</body>
</html>
복합 메시지와 조건부 메시지
<div class="message-examples">
<!-- 단순 메시지 -->
<h2 th:text="#{page.header}">페이지 헤더</h2>
<!-- 파라미터 치환 -->
<p th:text="#{user.greeting(${user.name}, ${user.age})}">
안녕하세요, 홍길동님! (25세)
</p>
<!-- 조건부 메시지 -->
<p th:text="#{${user.gender == 'M'} ? 'message.male' : 'message.female'}">
성별에 따른 메시지
</p>
<!-- 메시지와 변수 혼용 -->
<p>
<span th:text="#{status.label}">상태</span>:
<span th:text="${user.isActive()} ? #{status.active} : #{status.inactive}">활성</span>
</p>
<!-- 복잡한 메시지 구성 -->
<div class="notification" th:with="count=${#lists.size(notifications)}">
<span th:switch="${count}">
<span th:case="0" th:text="#{notification.none}">알림 없음</span>
<span th:case="1" th:text="#{notification.single}">알림 1개</span>
<span th:case="*" th:text="#{notification.multiple(${count})}">알림 여러개</span>
</span>
</div>
</div>
메시지 파일의 고급 기능
# 메시지 그룹화
button.save=저장
button.cancel=취소
button.delete=삭제
button.edit=편집
# HTML 내용이 포함된 메시지
message.html=<strong>중요:</strong> 이 작업은 <em>되돌릴 수 없습니다</em>.
message.link=자세한 내용은 <a href="/help">도움말</a>을 참조하세요.
# 조건부 메시지
error.validation.required={0}을(를) 입력해주세요.
error.validation.email.invalid=올바른 이메일 주소를 입력해주세요.
error.validation.age.range=나이는 {0}세에서 {1}세 사이여야 합니다.
# 복수형 처리
item.count.zero=상품이 없습니다.
item.count.one=상품 1개가 있습니다.
item.count.many=상품 {0}개가 있습니다.
<!-- HTML 내용이 포함된 메시지 사용 -->
<div th:utext="#{message.html}">HTML 메시지</div>
<div th:utext="#{message.link}">링크 메시지</div>
<!-- 복수형 처리 -->
<p th:switch="${itemCount}">
<span th:case="0" th:text="#{item.count.zero}">상품 없음</span>
<span th:case="1" th:text="#{item.count.one}">상품 1개</span>
<span th:case="*" th:text="#{item.count.many(${itemCount})}">상품 여러개</span>
</p>
3.5 Link URL Expressions (@{...}
)
Link URL Expression은 URL을 생성할 때 사용하며, 컨텍스트 패스 자동 추가, 파라미터 처리 등의 기능을 제공합니다.
기본 URL 생성
<nav class="main-nav">
<!-- 절대 경로 -->
<a th:href="@{/}">홈</a>
<a th:href="@{/users}">사용자 목록</a>
<a th:href="@{/products}">상품 목록</a>
<a th:href="@{/about}">소개</a>
<!-- 상대 경로 -->
<a th:href="@{users/profile}">프로필</a>
<a th:href="@{../admin}">관리자</a>
<!-- 외부 URL -->
<a th:href="@{https://www.example.com}">외부 사이트</a>
<a th:href="@{//cdn.example.com/assets/style.css}">CDN 리소스</a>
</nav>
경로 변수와 쿼리 파라미터
<div class="user-links">
<!-- 경로 변수 (Path Variable) -->
<a th:href="@{/user/{id}(id=${user.id})}">사용자 상세</a>
<a th:href="@{/user/{id}/edit(id=${user.id})}">사용자 편집</a>
<!-- 여러 경로 변수 -->
<a th:href="@{/category/{catId}/product/{prodId}(catId=${category.id}, prodId=${product.id})}">
상품 상세
</a>
<!-- 쿼리 파라미터 -->
<a th:href="@{/search(q=${searchQuery})}">검색 결과</a>
<a th:href="@{/users(page=${currentPage + 1}, size=10)}">다음 페이지</a>
<!-- 경로 변수와 쿼리 파라미터 혼용 -->
<a th:href="@{/user/{id}(id=${user.id}, tab='profile')}">
사용자 프로필 탭
</a>
<a th:href="@{/category/{catId}/products(catId=${category.id}, sort='price', order='asc')}">
가격순 정렬
</a>
</div>
폼 액션과 리소스 URL
<!-- 폼 액션 URL -->
<form th:action="@{/user/create}" method="post">
<input type="text" name="name" placeholder="이름">
<input type="email" name="email" placeholder="이메일">
<button type="submit">생성</button>
</form>
<form th:action="@{/user/{id}/update(id=${user.id})}" method="post">
<input type="hidden" name="_method" value="put">
<input type="text" th:value="${user.name}" name="name">
<button type="submit">업데이트</button>
</form>
<!-- 삭제 폼 -->
<form th:action="@{/user/{id}/delete(id=${user.id})}" method="post"
onsubmit="return confirm('정말 삭제하시겠습니까?')">
<input type="hidden" name="_method" value="delete">
<button type="submit" class="btn-danger">삭제</button>
</form>
정적 리소스 URL
<head>
<!-- CSS 파일 -->
<link th:href="@{/css/main.css}" rel="stylesheet">
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<!-- JavaScript 파일 -->
<script th:src="@{/js/jquery.min.js}"></script>
<script th:src="@{/js/app.js}"></script>
<!-- 파비콘 -->
<link th:href="@{/favicon.ico}" rel="icon" type="image/x-icon">
</head>
<body>
<!-- 이미지 -->
<img th:src="@{/images/logo.png}" alt="로고">
<img th:src="@{/images/users/{filename}(filename=${user.avatar})}"
th:alt="${user.name}">
<!-- 동적 이미지 URL -->
<div th:each="product : ${products}">
<img th:src="@{/images/products/{id}.jpg(id=${product.id})}"
th:alt="${product.name}"
onerror="this.src='/images/no-image.png'">
</div>
<!-- PDF, 다운로드 링크 -->
<a th:href="@{/files/manual.pdf}" target="_blank">사용자 매뉴얼</a>
<a th:href="@{/download/{fileId}(fileId=${file.id})}">파일 다운로드</a>
</body>
조건부 URL과 동적 URL 생성
<div class="conditional-links">
<!-- 조건부 URL -->
<a th:href="@{${user.isAdmin()} ? '/admin/dashboard' : '/user/dashboard'}">
대시보드
</a>
<!-- 동적 기본 URL -->
<a th:href="@{${baseUrl} + '/api/data'}" th:if="${baseUrl}">API 데이터</a>
<!-- 페이지네이션 -->
<div class="pagination">
<a th:if="${currentPage > 0}"
th:href="@{/products(page=${currentPage - 1}, size=${pageSize}, sort=${sortBy})}">
이전
</a>
<span th:each="pageNum : ${#numbers.sequence(0, totalPages - 1)}"
th:if="${pageNum >= currentPage - 2 and pageNum <= currentPage + 2}">
<a th:if="${pageNum != currentPage}"
th:href="@{/products(page=${pageNum}, size=${pageSize}, sort=${sortBy})}"
th:text="${pageNum + 1}">1</a>
<span th:if="${pageNum == currentPage}"
th:text="${pageNum + 1}" class="current-page">1</span>
</span>
<a th:if="${currentPage < totalPages - 1}"
th:href="@{/products(page=${currentPage + 1}, size=${pageSize}, sort=${sortBy})}">
다음
</a>
</div>
<!-- 정렬 링크 -->
<div class="sort-options">
<a th:href="@{/products(sort='name', order='asc', page=0)}"
th:class="${sortBy == 'name' and order == 'asc'} ? 'active' : ''">
이름 ↑
</a>
<a th:href="@{/products(sort='name', order='desc', page=0)}"
th:class="${sortBy == 'name' and order == 'desc'} ? 'active' : ''">
이름 ↓
</a>
<a th:href="@{/products(sort='price', order='asc', page=0)}"
th:class="${sortBy == 'price' and order == 'asc'} ? 'active' : ''">
가격 ↑
</a>
<a th:href="@{/products(sort='price', order='desc', page=0)}"
th:class="${sortBy == 'price' and order == 'desc'} ? 'active' : ''">
가격 ↓
</a>
</div>
</div>
3.6 Fragment Expressions (~{...}
)
Fragment Expression은 템플릿 프래그먼트를 참조할 때 사용합니다. 템플릿의 재사용성을 높이는 중요한 기능입니다.
프래그먼트 정의
<!-- fragments/common.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 헤더 프래그먼트 -->
<div th:fragment="header" class="header">
<nav class="navbar">
<a th:href="@{/}" class="logo">MyApp</a>
<ul class="nav-links">
<li><a th:href="@{/}">홈</a></li>
<li><a th:href="@{/products}">상품</a></li>
<li><a th:href="@{/about}">소개</a></li>
</ul>
</nav>
</div>
<!-- 파라미터가 있는 프래그먼트 -->
<div th:fragment="userCard(user, showDetails)" class="user-card">
<h3 th:text="${user.name}">사용자명</h3>
<p th:text="${user.email}">이메일</p>
<div th:if="${showDetails}">
<p>나이: <span th:text="${user.age}">25</span></p>
<p>역할: <span th:text="${user.role}">user</span></p>
</div>
</div>
<!-- 푸터 프래그먼트 -->
<footer th:fragment="footer" class="footer">
<p>© 2024 MyApp. All rights reserved.</p>
<div class="social-links">
<a href="#" class="social-link">Facebook</a>
<a href="#" class="social-link">Twitter</a>
</div>
</footer>
<!-- 메타 태그 프래그먼트 -->
<head th:fragment="meta(title, description)" th:remove="tag">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:text="${title}">기본 제목</title>
<meta name="description" th:content="${description}">
<link th:href="@{/css/main.css}" rel="stylesheet">
</head>
</html>
프래그먼트 사용
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<!-- 메타 프래그먼트 삽입 -->
<div th:replace="~{fragments/common :: meta('상품 목록', '다양한 상품을 만나보세요')}"></div>
</head>
<body>
<!-- 헤더 프래그먼트 삽입 -->
<div th:replace="~{fragments/common :: header}"></div>
<main class="main-content">
<h1>상품 목록</h1>
<!-- 사용자 카드 프래그먼트 반복 사용 -->
<div class="users-grid">
<div th:each="user : ${users}">
<div th:replace="~{fragments/common :: userCard(${user}, true)}"></div>
</div>
</div>
<!-- 단순한 사용자 목록 (세부정보 없이) -->
<div class="simple-users">
<div th:each="user : ${simpleUsers}">
<div th:replace="~{fragments/common :: userCard(${user}, false)}"></div>
</div>
</div>
</main>
<!-- 푸터 프래그먼트 삽입 -->
<div th:replace="~{fragments/common :: footer}"></div>
</body>
</html>
Insert, Replace, Include의 차이
<!-- fragments/demo.html -->
<div th:fragment="content" class="demo-content">
<p>이것은 프래그먼트 내용입니다.</p>
</div>
<!-- 사용 예제 -->
<body>
<h2>th:insert 사용</h2>
<div th:insert="~{fragments/demo :: content}">
기존 내용은 유지됩니다.
</div>
<h2>th:replace 사용</h2>
<div th:replace="~{fragments/demo :: content}">
이 내용은 완전히 교체됩니다.
</div>
<h2>th:include 사용 (Deprecated)</h2>
<div th:include="~{fragments/demo :: content}">
프래그먼트의 내용만 포함됩니다.
</div>
</body>
렌더링 결과:
<body>
<h2>th:insert 사용</h2>
<div>
기존 내용은 유지됩니다.
<div class="demo-content">
<p>이것은 프래그먼트 내용입니다.</p>
</div>
</div>
<h2>th:replace 사용</h2>
<div class="demo-content">
<p>이것은 프래그먼트 내용입니다.</p>
</div>
<h2>th:include 사용</h2>
<div>
<p>이것은 프래그먼트 내용입니다.</p>
</div>
</body>
3.7 리터럴과 텍스트 연산
Thymeleaf에서 다양한 리터럴 타입과 텍스트 연산을 사용할 수 있습니다.
텍스트 리터럴
<div class="literals-example">
<!-- 문자열 템플릿 (Literal substitutions) -->
<p th:text="|안녕하세요, ${user.name}님! 나이: ${user.age}세|">템플릿 문자열</p>
<p th:text="|오늘은 ${#dates.format(#dates.createNow(), 'yyyy-MM-dd')}입니다.|">날짜 포함</p>
<!-- 숫자 리터럴 -->
<p th:text="42">숫자</p>
<p th:text="3.14159">소수</p>
<p th:text="${user.age + 10}">나이 + 10</p>
<!-- 불리언 리터럴 -->
<p th:text="true">참</p>
<p th:text="false">거짓</p>
<p th:if="true">항상 표시</p>
<p th:unless="false">항상 표시</p>
<!-- null 리터럴 -->
<p th:text="null">null 값</p>
<p th:if="${user.nickname != null}">닉네임이 있음</p>
</div>
문자열 연산
<div class="string-operations">
<!-- 문자열 연결 -->
<p th:text="'Hello' + ' ' + 'World'">Hello World</p>
<p th:text="${user.firstName} + ' ' + ${user.lastName}">전체 이름</p>
<!-- 문자열 템플릿 사용 (권장) -->
<p th:text="|${user.firstName} ${user.lastName}|">전체 이름</p>
<p th:text="|이메일: ${user.email}, 나이: ${user.age}세|">사용자 정보</p>
<!-- 조건부 문자열 -->
<p th:text="|상태: ${user.isActive() ? '활성' : '비활성'}|">상태</p>
<p th:text="|권한: ${user.isAdmin() ? '관리자' : '일반 사용자'}|">권한</p>
<!-- HTML 속성에서의 문자열 템플릿 -->
<img th:src="|/images/users/${user.id}.jpg|" th:alt="|${user.name} 프로필|">
<a th:href="|/user/${user.id}/edit|" th:title="|${user.name} 편집|">편집</a>
<!-- 복잡한 문자열 구성 -->
<p th:text="|${user.name}님의 마지막 로그인: ${#temporals.format(user.lastLogin, 'yyyy-MM-dd HH:mm')}|">
마지막 로그인 정보
</p>
</div>
산술 연산
<div class="arithmetic-operations">
<!-- 기본 산술 연산 -->
<p>덧셈: <span th:text="5 + 3">8</span></p>
<p>뺄셈: <span th:text="10 - 4">6</span></p>
<p>곱셈: <span th:text="6 * 7">42</span></p>
<p>나눗셈: <span th:text="15 / 3">5</span></p>
<p>나머지: <span th:text="17 % 5">2</span></p>
<!-- 변수와의 연산 -->
<p>현재 나이: <span th:text="${user.age}">25</span></p>
<p>10년 후: <span th:text="${user.age + 10}">35</span></p>
<p>태어난 년도: <span th:text="${#dates.year(#dates.createNow())} - ${user.age}">1999</span></p>
<!-- 가격 계산 -->
<div th:each="item : ${cartItems}">
<p th:text="|${item.name}: ${item.quantity} × ${item.price} = ${item.quantity * item.price}원|">
상품: 2 × 10000 = 20000원
</p>
</div>
<p>총합: <span th:text="${#aggregates.sum(cartItems.![quantity * price])}">50000</span>원</p>
<!-- 할인 계산 -->
<div th:if="${discount > 0}">
<p>원가: <span th:text="${totalPrice}">50000</span>원</p>
<p>할인율: <span th:text="${discount}">10</span>%</p>
<p>할인금액: <span th:text="${totalPrice * discount / 100}">5000</span>원</p>
<p>최종가격: <span th:text="${totalPrice * (100 - discount) / 100}">45000</span>원</p>
</div>
</div>
비교 연산
<div class="comparison-operations">
<!-- 숫자 비교 -->
<p th:if="${user.age >= 18}">성인입니다.</p>
<p th:unless="${user.age >= 18}">미성년자입니다.</p>
<p th:text="${user.score > 80} ? '우수' : (${user.score > 60} ? '보통' : '노력필요')">성적</p>
<!-- 문자열 비교 -->
<p th:if="${user.role == 'admin'}">관리자 권한</p>
<p th:if="${user.name != null and user.name != ''}">이름이 설정됨</p>
<!-- 객체 비교 -->
<p th:if="${currentUser == user}">현재 로그인한 사용자</p>
<p th:if="${currentUser.id == user.id}">같은 사용자</p>
<!-- 컬렉션 비교 -->
<p th:if="${#lists.size(user.orders) > 0}">주문 내역이 있습니다.</p>
<p th:if="${#lists.isEmpty(user.orders)}">주문 내역이 없습니다.</p>
<!-- 날짜 비교 -->
<p th:if="${user.createdAt > #dates.createNow().minusDays(30)}">최근 가입 사용자</p>
<!-- 범위 비교 -->
<div th:switch="true">
<p th:case="${user.age < 20}">10대</p>
<p th:case="${user.age < 30}">20대</p>
<p th:case="${user.age < 40}">30대</p>
<p th:case="*">40대 이상</p>
</div>
</div>
논리 연산
<div class="logical-operations">
<!-- AND 연산 -->
<p th:if="${user.isActive() and user.isVerified()}">활성화된 인증 사용자</p>
<p th:if="${user.age >= 18 and user.hasPermission('PURCHASE')}">구매 가능 사용자</p>
<!-- OR 연산 -->
<p th:if="${user.isAdmin() or user.isModerator()}">관리 권한 사용자</p>
<p th:if="${user.isPremium() or user.score > 90}">특별 혜택 대상</p>
<!-- NOT 연산 -->
<p th:if="${not user.isActive()}">비활성 사용자</p>
<p th:unless="${user.isActive()}">비활성 사용자</p> <!-- 위와 동일 -->
<!-- 복합 논리 연산 -->
<div th:if="${(user.isActive() and user.isVerified()) or user.isAdmin()}">
<p>접근 허용</p>
</div>
<!-- 삼항 연산자와 결합 -->
<p th:text="${(user.isActive() and user.isVerified()) ? '정상 사용자' : '제한 사용자'}">
사용자 상태
</p>
<!-- 복잡한 조건 -->
<div th:if="${user.role == 'admin' or (user.role == 'user' and user.score > 80)}">
<p>고급 기능 사용 가능</p>
</div>
</div>
조건 연산자와 Elvis 연산자
<div class="conditional-operations">
<!-- 삼항 연산자 (조건 ? 참 : 거짓) -->
<p th:text="${user.age >= 18} ? '성인' : '미성년자'">연령 분류</p>
<p th:text="${user.score >= 80} ? 'A' : (${user.score >= 60} ? 'B' : 'C')">등급</p>
<!-- Elvis 연산자 (값 ?: 기본값) -->
<p th:text="${user.nickname ?: user.name}">표시 이름</p>
<p th:text="${user.bio ?: '소개가 없습니다.'}">자기소개</p>
<p th:text="${user.profileImage ?: '/images/default-avatar.png'}">프로필 이미지</p>
<!-- null 안전 연산자와 결합 -->
<p th:text="${user.profile?.nickname ?: user.name}">안전한 닉네임</p>
<p th:text="${user.company?.name ?: '소속 없음'}">회사명</p>
<!-- 조건 연산자 중첩 -->
<span th:class="${user.role == 'admin'} ? 'badge-admin' :
(${user.role == 'moderator'} ? 'badge-mod' : 'badge-user')"
th:text="${user.role}">역할</span>
<!-- 복잡한 Elvis 연산자 사용 -->
<p th:text="${user.settings?.displayName ?: user.profile?.nickname ?: user.name}">
우선순위별 이름 표시
</p>
</div>
No-Operation 토큰
<div class="no-operation-examples">
<!-- _ (언더스코어)는 아무 작업도 하지 않음 -->
<p th:text="${user.isActive()} ? ${user.name} : _">
활성 사용자일 때만 이름 표시
</p>
<!-- 조건부 속성 설정 -->
<div th:class="${user.isActive()} ? 'active-user' : _"
th:attr="data-user-id=${user.isActive()} ? ${user.id} : _">
조건부 속성
</div>
<!-- 선택적 링크 -->
<a th:href="${user.hasProfile()} ? @{/user/{id}/profile(id=${user.id})} : _"
th:text="${user.name}">사용자명</a>
<!-- 배경 이미지 조건부 설정 -->
<div th:style="${user.coverImage} ? |background-image: url(${user.coverImage})| : _"
class="user-header">
사용자 헤더
</div>
</div>
종합 예제: 사용자 카드 컴포넌트
<div class="user-card" th:each="user : ${users}">
<!-- 사용자 기본 정보 -->
<div class="user-avatar">
<img th:src="${user.avatar ?: '/images/default-avatar.png'}"
th:alt="|${user.name} 아바타|">
<!-- 온라인 상태 표시 -->
<span th:if="${user.isOnline()}" class="status-online" title="온라인"></span>
<span th:unless="${user.isOnline()}" class="status-offline" title="오프라인"></span>
</div>
<div class="user-info">
<!-- 이름과 닉네임 -->
<h3 th:text="${user.nickname ?: user.name}">사용자명</h3>
<p class="user-email" th:text="${user.email}">이메일</p>
<!-- 역할 배지 -->
<span th:class="|badge badge-${user.role}|"
th:text="${user.role == 'admin'} ? '관리자' :
(${user.role == 'moderator'} ? '운영자' : '사용자')">역할</span>
<!-- 가입일 표시 -->
<p class="join-date">
<span th:text="|가입일: ${#temporals.format(user.createdAt, 'yyyy-MM-dd')}|">가입일</span>
<span th:if="${#temporals.daysBetween(user.createdAt, #temporals.createNow())} < 30"
class="new-member">NEW</span>
</p>
<!-- 점수와 등급 -->
<div class="user-score" th:if="${user.score != null}">
<span>점수: </span>
<span th:text="${user.score}">85</span>
<span th:text="'(' + (${user.score >= 90} ? 'S' :
${user.score >= 80} ? 'A' :
${user.score >= 70} ? 'B' : 'C') + '등급)'">
(A등급)
</span>
</div>
<!-- 활동 통계 -->
<div class="user-stats" th:if="${not #lists.isEmpty(user.activities)}">
<small>
<span th:text="|게시글 ${#lists.size(user.posts)}개|">게시글 5개</span> |
<span th:text="|댓글 ${#lists.size(user.comments)}개|">댓글 12개</span>
</small>
</div>
</div>
<!-- 액션 버튼 -->
<div class="user-actions">
<a th:href="@{/user/{id}(id=${user.id})}" class="btn btn-primary">프로필</a>
<!-- 관리자만 볼 수 있는 버튼 -->
<div th:if="${currentUser.isAdmin()}">
<button th:onclick="|toggleUserStatus(${user.id})|"
th:class="${user.isActive()} ? 'btn btn-warning' : 'btn btn-success'"
th:text="${user.isActive()} ? '비활성화' : '활성화'">
상태 변경
</button>
</div>
<!-- 메시지 전송 (자신이 아닐 때만) -->
<a th:unless="${currentUser.id == user.id}"
th:href="@{/messages/new(to=${user.id})}"
class="btn btn-secondary">메시지</a>
</div>
<!-- 최근 활동 -->
<div class="recent-activity" th:if="${user.lastActivity != null}">
<small th:text="|최근 활동: ${#temporals.formatISO(user.lastActivity)}|">
최근 활동: 2024-01-15T14:30:00
</small>
</div>
</div>
이 3장에서는 Thymeleaf의 5가지 핵심 표현식과 다양한 연산자들을 상세히 살펴보았습니다. 각 표현식의 특징과 사용 사례를 이해하면 더욱 효과적으로 Thymeleaf를 활용할 수 있습니다.
반응형