- 간단한 표현:
- 변수 표현식: ${...}
- 선택 변수 표현식: *{...}
- 메시지 표현식: #{...}
- 링크 URL 표현식: @{...}
- 조각 표현식: ~{...}
- 리터럴
- 텍스트: 'one text', 'Another one!',…
- 숫자: 0, 34, 3.0, 12.3,…
- 불린: true, false
- 널: null
- 리터럴 토큰: one, sometext, main,…
- 문자 연산:
- 문자 합치기: +
- 리터럴 대체: |The name is ${name}|
- 산술 연산:
- Binary operators: +, -, *, /, %
- Minus sign (unary operator): -
- 불린 연산:
- Binary operators: and, or
- Boolean negation (unary operator): !, not
- 비교와 동등:
- 비교: >, <, >=, <= (gt, lt, ge, le)
- 동등 연산: ==, != (eq, ne)
- 조건 연산:
- If-then: (if) ? (then)
- If-then-else: (if) ? (then) : (else)
- Default: (value) ?: (defaultvalue)
- 특별한 토큰:
- No-Operation: _
<span th:text="${data}">
[[${data}]]
<
를 HTML 태그의 시작으로 인식한다. 태그의 시작이 아니라 문자로 표현하는 방법을 HTML 엔티티라 한다. 이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 이스케이프(escape)라 한다. 타임리프가 제공하는 th:text
, [[...]]
는 기본적으로 이스케이프(escape)를 제공한다.th:text
-> th:utext
[[...]]
-> [(...)]
th:inline="none"
: 타임리프는 콘텐츠 영역안의 [[...]]
를 해석하기 때문에, 이 태그 안에서는 타임리프가 해석하지 말라는 옵션.${...}
: 이 변수 표현식에는 스프링 EL이라는 스프링이 제공하는 표현식을 사용할 수 있다.${user.username}
: user의 username을 프로퍼티 접근 -> user.getUsername()
${user['username']}
: 위와 같음 -> user.getUsername()
${user.getUsername()}
: user의 getUsername()
을 직접 호출${users[0].username}
: List에서 첫 번째 회원을 찾고 username 프로퍼티 접근 -> list.get(0).getUsername()
${users[0]['username']}
: 위와 같음${users[0}.getUsername()}
: List에서 첫 번째 회원을 찾고 메서드 직접 호출${userMap['userA'].username
: Map에서 userA를 찾고, username 프로퍼티 접근 -> map.get("userA").getUsername()
${userMap['userA']['username']}
: 위와 같음${userMap['userA'].getUsername()}
: Map에서 userA를 찾고 메서드 직접 호출th:with
<!-- 지역 변수는 선언한 태그 안에서만 사용할 수 있다.-->
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
${#request}
- 스프링 부트 3.0부터 제공 X${#response}
- 스프링 부트 3.0부터 제공 X${#session}
- 스프링 부트 3.0부터 제공 X${#servletContext}
- 스프링 부트 3.0부터 제공 X${#locale}
model
에 해당 객체를 추가해서 사용해야 한다.param
: HTTP 요청 파라미터 접근 편의 객체${param.paramData}
session
: HTTP 세션 접근 편의 객체${session.sessionData}
<- session.setAttribute("sessionData", "Hello Session");
@
: 스프링 빈 접근${@helloBean.hello('Spring!')}
<- HelloBean 객체의 hello 메서드 사용타임리프 유틸리티 객체들 - 타임리프 유틸리티 객체, 유틸리티 객체 예시
#message
: 메시지, 국제화 처리
#uris
: URI 이스케이프 지원
#dates
:java.util.Date
서식 지원
#calendars
:java.util.Calendar
서식 지원
#temporals
: 자바8 날짜 서식 지원
#numbers
: 숫자 서식 지원
#strings
: 문자 관련 편의 기능
#objects
: 객체 관련 기능 제공
#bools
: boolean 관련 기능 제공
#arrays
: 배열 관련 기능 제공
#lists
,#sets
,#maps
: 컬렉션 관련 기능 제공
#ids
: 아이디 처리 관련 기능 제공, 뒤에서 설명
LocalDate
, LocalDateTime
, Instant
를 사용하려면 추가 라이브러리 필요. 스프링 부트 타임리프를 사용하면 해당 라이브러리(thymeleaf-extras-java8time
)가 자동으로 추가되고 통합된다.#temporals
사용 예시@GetMapping("/date")
public String date(Model model) {
model.addAttribute("localDateTime", LocalDateTime.now());
return "basic/date";
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>LocalDateTime</h1>
<ul>
<li>default = <span th:text="${localDateTime}"></span></li>
<li>yyyy-MM-dd HH:mm:ss =
<span th:text="${#temporals.format(localDateTime, 'yyyy-MM-dd HH:mm:ss')}">
</span>
</li>
</ul>
<h1>LocalDateTime - Utils</h1>
<ul>
<li>${#temporals.day(localDateTime)} =
<span th:text="${#temporals.day(localDateTime)}"></span>
</li>
<li>${#temporals.month(localDateTime)} =
<span th:text="${#temporals.month(localDateTime)}"></span>
</li>
<li>${#temporals.monthName(localDateTime)} =
<span th:text="${#temporals.monthName(localDateTime)}"></span>
</li>
<li>${#temporals.monthNameShort(localDateTime)} =
<span th:text="${#temporals.monthNameShort(localDateTime)}"></span>
</li>
<li>${#temporals.year(localDateTime)} =
<span th:text="${#temporals.year(localDateTime)}"></span>
</li>
<li>${#temporals.dayOfWeek(localDateTime)} =
<span th:text="${#temporals.dayOfWeek(localDateTime)}"></span>
</li>
<li>${#temporals.dayOfWeekName(localDateTime)} =
<span th:text="${#temporals.dayOfWeekName(localDateTime)}"></span>
</li>
<li>${#temporals.dayOfWeekNameShort(localDateTime)} =
<span th:text="${#temporals.dayOfWeekNameShort(localDateTime)}"></span>
</li>
<li>${#temporals.hour(localDateTime)} =
<span th:text="${#temporals.hour(localDateTime)}"></span>
</li>
<li>${#temporals.minute(localDateTime)} =
<span th:text="${#temporals.minute(localDateTime)}"></span>
</li>
<li>${#temporals.second(localDateTime)} =
<span th:text="${#temporals.second(localDateTime)}"></span>
</li>
<li>${#temporals.nanosecond(localDateTime)} =
<span th:text="${#temporals.nanosecond(localDateTime)}"></span>
</li>
</ul>
</body>
</html>
@{/hello}
-> /hello
@{/hello(param1=${param1}, param2=${param2})}
/hello?param1=data1¶m2=data2
@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}
/hello/data1/data2
@{/hello/{param1}(param1=${param1}, param2=${param2})}
/hello/data1?param2=data2
/hello
: 절대 경로hello
: 상대 경로리터럴은 소스 코드상에 고정된 값, 타임리프는 다음과 같은 리터럴이 있다.
- 문자:
'hello'
- 숫자:
10
- 불린
true
,false
- null:
null
'
(작은 따옴표)로 감싸야 한다. 공백 없이 쭉 이어진다면 하나의 의미있는 토큰으로 인지해서 작은 따옴표를 생략할 수 있다.<span th:text="hello world!"></span>
-> 오류<span th:text="'hello world!'"></span>
-> 정상 동작<span th:text="'hello' + ' world!'"></span>
-> 정상 동작<span th:text="'hello ' + ${data}"></span>
-> 정상 동작<span th:text="|hello ${data}|"></span>
-> 정상 동작<li>10 + 2 = <span th:text="10 + 2"></span></li>
<li>10 % 2 == 0 = <span th:text="10 % 2 == 0"></span></li>
>
(gt), <
(lt), >=
(ge), <=
(le), !
(not), ==
(eq), !=
(neq, ne)<li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)? '짝수':'홀수'"></span></li>
<li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가없습니다.'"></span></li>
<li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?: '데이터가 없습니다.'"></span></li>
<li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</span></li>
<li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가없습니다.</span></li>
th:*
속성을 지정하면 타임리프는 기존 속성을 th:*
로 지정한 속성으로 대체한다. 기존 속성이 없다면 새로 만든다.<input type="text" name="mock" th:name="userA" />
<input type="text" name="userA" />
<input type="text" class="text" th:attrappend="class=' large'" />
<input type="text" class="text" th:attrprepend="class='large '" />
<input type="text" class="text" th:classappend="large" />
<input type="checkbox" name="active" checked="false" />
이 경우에도 checked 속성이 있기 때문에 checked 처리 되어버린다.th:checked
는 값이 false
인 경우 checked
속성 자체를 제거한다.<tr th:each="user : ${users}">
-> 컬렉션 ${users}
의 값을 하나씩 꺼내서 왼쪽 변수(user
)에 담아서 태그를 반복 실행.th:each
는 List
뿐만 아니라 배열, java.util.Iterable
, java.util.Enumeration
을 구현한 모든 객체를 반복에 사용할 수 있다. Map
사용시 변수에 담기는 값은 Map.Entry
이다.<tr th:each="user, userStat : ${users}">
-> 반복의 두번째 파라미터를 설정해서 반복의 상태를 확인 할 수 있다. 두번째 파라미터 생략시, 지정한 변수명(user
) + Stat
.index
: 0부터 시작count
: 1부터 시작size
: 전체 사이즈even
, odd
: 홀수, 짝수 여부(boolean
)first
, last
: 처음, 마지막 여부(boolean
)current
: 현재 객체<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
<!-- *은 만족하는 조건이 없을 때 사용하는 디폴트-->
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="*">기타</span>
</td>
HTML
<h1>예시</h1>
<span th:text="${data}">html data</span>
<h1>1. 표준 HTML 주석</h1>
<!--
<span th:text="${data}">html data</span>
-->
<h1>2. 타임리프 파서 주석</h1>
<!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->
<h1>3. 타임리프 프로토타입 주석</h1>
<!--/*/
<span th:text="${data}">html data</span>
/*/-->
결과
<h1>예시</h1>
<span>Spring!</span>
<h1>1. 표준 HTML 주석</h1>
<!--
<span th:text="${data}">html data</span>
-->
<h1>2. 타임리프 파서 주석</h1>
<h1>3. 타임리프 프로토타입 주석</h1>
<span>Spring!</span>
<th:block th:each="user : ${users}">
<div>
사용자 이름1 <span th:text="${user.username}"></span>
사용자 나이1 <span th:text="${user.age}"></span>
</div>
<div>
요약 <span th:text="${user.username} + ' / ' + ${user.age}"></span>
</div>
</th:block>
<th:block>
은 HTML 태그가 아닌 타임리프의 유일한 자체 태그.<th:block>
은 렌더링시 제거 된다.HTML
<!-- 자바스크립트 인라인 사용 전 -->
<script>
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script th:inline="javascript">
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
</script>
자바스크립트 인라인 사용 전 - 결과
<!-- 자바스크립트 인라인 사용 전 -->
<script>
var username = UserA;
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = /*UserA*/ "test username";
//객체
var user = BasicController.User(username=UserA, age=10);
</script>
자바스크립트 인라인 사용 후- 결과
<!-- 자바스크립트 인라인 사용 후 -->
<script>
var username = "UserA";
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = "UserA";
//객체
var user = {"username":"UserA","age":10};
</script>
"
를 포함 해준다. 추가로 자바스크립트에서 문제가 될 수 있는 문자가 포함되어 있으면 이스케이프 처리도 해준다. 예)"
->\"
HTML
<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
결과
<!-- 자바스크립트 인라인 each -->
<script>
var user1 = {"username":"UserA","age":10};
var user2 = {"username":"UserB","age":20};
var user3 = {"username":"UserC","age":30};
</script>
<!-- /resources/templates/template/fragment/footer.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="copy">
푸터 자리 입니다.
</footer>
<footer th:fragment="copyParam (param1, param2)">
<p>파라미터 자리 입니다.</p>
<p th:text="${param1}"></p>
<p th:text="${param2}"></p>
</footer>
</body>
</html>
<!-- /resources/templates/template/fragment/fragmentMain.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>부분 포함</h1>
<h2>부분 포함 insert</h2>
<div th:insert="~{template/fragment/footer :: copy}"></div>
<h2>부분 포함 replace</h2>
<div th:replace="~{template/fragment/footer :: copy}"></div>
<h2>부분 포함 단순 표현식</h2>
<div th:replace="template/fragment/footer :: copy"></div>
<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
</body>
</html>
th:fragment
가 있는 태그는 다른곳에 포함되는 코드 조각으로 이해하면 된다.template/fragment/footer :: copy
-> template/fragment/footer.html
템플릿에 있는 th:fragment="copy"
라는 부분을 템플릿 조각으로 가져와서 사용한다는 의미.th:insert
를 사용하면 현재 태그(div
) 내부에 추가.th:replace
를 사용하면 현재 태그(div
)를 대체.~{...}
를 사용하는 것이 원칙이지만 템플릿 조각을 사용하는 코드가 단순하면 이 부분을 생략할 수 있다.<!-- /resources/templates/template/layout/base.html -->
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common_header(title,links)">
<title th:replace="${title}">레이아웃 타이틀</title>
<!-- 공통 -->
<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
<link rel="shortcut icon" th:href="@{/images/favicon.ico}">
<script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>
<!-- 추가 -->
<th:block th:replace="${links}" />
</head>
<!-- /resources/templates/template/layout/layoutMain.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
<title>메인 타이틀</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>
<body>
메인 컨텐츠
</body>
</html>
common_header(~{::title},~{::link})
이 부분이 핵심이다.::title
은 현재 페이지의 title 태그들을 전달.::link
은 현재 페이지의 link 태그들을 전달.<link>
들이 포함된다.<!-- /resources/templates/template/layoutExtend/layoutFile.html -->
<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<title th:replace="${title}">레이아웃 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}">
<p>레이아웃 컨텐츠</p>
</div>
<footer>
레이아웃 푸터
</footer>
</body>
</html>
<!-- /resources/templates/template/layoutExtend/layoutExtendMain.html -->
<!DOCTYPE html>
<html th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title}, ~{::section})}"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
</body>
</html>
layoutFile.html
을 보면 기본 레이아웃을 가지고 있는데 <html>
에 th:fragment
속성이 정의되어 있다. 이 레이아웃 파일을 기본으로 하고 여기에 필요한 내용을 전달해서 부분부분 변경하는 것이다.layoutExtendMain.html
은 현재 페이지인데, <html>
자체를 th:replace
를 사용해서 변경하는 것을 확인할 수 있다. 결국 layoutFile.html
에 필요한 내용을 전달하면서 <html>
자체를 layoutFile.html
로 변경한다.
- 스프링의 SpringEL 문법 통합
- ${@myBean.doSomething()} 처럼 스프링 빈 호출 지원
- 편리한 폼 관리를 위한 추가 속성
th:object
(기능 강화, 폼 커맨드 객체 선택)th:field
,th:errors
,th:errorclass
- 폼 컨포넌트 기능
- checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원
- 스프링의 메시지, 국제화 기능의 편리한 통합
- 스프링의 검증, 오류 처리 통합
- 스프링의 변환 서비스 통합(ConversionService)
build.gradle
에 다음 한줄을 넣어주면 Gradle은 타임리프와 관련된 라이브러리를 다운로드 받고, 스프링 부트는 앞서 설명한 타임리프와 관련된 설정용 스프링 빈을 자동으로 등록해준다.implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
application.properties
에 추가하면 된다.타임리프가 제공하는 입력 폼 기능 적용
th:object
: 커맨트 객체를 지정.*{...}
: 선택 변수 식.th:object
에서 선택한 객체에 접근.th:field
- HTML 태그의
id
,name
,value
속성을 자동으로 처리해준다.
th:object
를 적용하려면 먼저 해당 오브젝트 정보를 넘겨주어야 한다. 등록 폼이기 때문에 데이터가 비어있는 빈 오브젝트를 만들어서 뷰에 전달하자.<!-- form/addForm.html 변경 코드 부분 -->
<form action="item.html" th:action th:object="${item}" method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
class="form-control" placeholder="이름을 입력하세요">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" th:field="*{price}"
class="form-control" placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" th:field="*{quantity}"
class="form-control" placeholder="수량을 입력하세요">
</div>
th:object="${item}
: <form>
에서 사용할 객체를 지정. 선택 변수 식(*{...}
)을 적용할 수 있다.th:field="*{itemName}"
*{itemName}
은 선택 변수 식을 사용했는데, ${item.itemName}
과 같다. 앞서 th:object
로 item
을 선택했기 때문에 선택 변수 식을 적용할 수 있다.th:field
는 id
, name
, value
속성을 모두 자동으로 만들어준다.id
: th:field
에서 지정한 변수 이름과 같다. id="itemName"
name
: th:field
에서 지정한 변수 이름과 같다. name="itemName"
value
: th:field
에서 지정한 변수의 값을 사용한다. value=""
<form action="item.html" th:action th:object="${item}" method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" class="form-control"
th:field="*{id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" class="form-control"
th:field="*{itemName}" >
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" class="form-control"
th:field="*{price}">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" class="form-control"
th:field="*{quantity}">
</div>
- 판매 여부
- 판매 오픈 여부
- 체크 박스로 선택할 수 있다.
- 등록 지역
- 서울, 부산, 제주
- 체크 박스로 다중 선택할 수 있다.
- 상품 종류
- 도서, 식품, 기타
- 라디오 버튼으로 하나만 선택할 수 있다.
- 배송 방식
- 빠른 배송
- 일반 배송
- 느린 배송
- 셀렉트 박스로 하나만 선택할 수 있다.
public enum ItemType {
BOOK("도서"), FOOD("음식"), ETC("기타");
private final String description;
ItemType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* FAST: 빠른 배송
* NORMAL: 일반 배송
* SLOW: 느린 배송
*/
@Data
@AllArgsConstructor
public class DeliveryCode {
private String code;
private String displayName;
}
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
private Boolean open; //판매 여부
private List<String> regions; //등록 지역
private ItemType itemType; //상품 종류
private String deliveryCode; // 배송 방식
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
<!-- resources/templates/form/addForm.html 추가 -->
<hr class="my-4">
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
open=on
이라는 값이 넘어간다. 스프링은 on
이라는 문자를 true
타입으로 변환(스프링 타입 컨버터)해준다.open
이라는 필드 자체가 서버로 전송되지 않는다.open
의 이름도 전송이 되지 않는 것을 확인할 수 있다. -> itemName=itemA&price=10000&quantity=10
null
인 것을 확인할 수 있다. -> log.info("item.open={}", item.getOpen());
_open
처럼 기존 체크 박스 이름 앞에 언더스코어(_
)를 붙여서 전송하면 체크를 해제했다고 인식할 수 있다. 체크를 해제한 경우 open
은 전송되지 않고, _open
만 전송되는데, 이 경우 스프링 MVC는 체크를 해제했다고 판단한다. <input type="hidden" name="_open" value="on"/>
open
에 값이 있는 것을 확인하고 사용한다. 이때 _open
은 무시._open
만 있는 것을 확인하고, open
의 값이 체크되지 않았다고 인식한다.<!-- addForm.html -->
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open"
th:field="*{open}" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
item.html
에는 th:object
를 사용하지 않았기 때문에 th:field
부분에 ${item.open}
으로 적어준다. disabled
를 사용해서 상품 상세에서는 체크 박스가 선택되지 않도록 한다.checked
속성이 추가된 것을 확인할 수 있다. 타임리프의 th:field
를 사용하면, 값이 true
인 경우 체크를 자동으로 처리해준다.@ModelAttribute("regions")
public Map<String, String> regions() {
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
return regions;
}
<!-- addForm.html 추가 -->
<!-- multi checkbox -->
<div>
<div>등록 지역</div>
<div th:each="region : ${regions}" class="form-check form-check-inline">
<input type="checkbox" th:field="*{regions}"
th:value="${region.key}" class="form-check-input">
<label th:for="${#ids.prev('regions')}"
th:text="${region.value}" class="form-check-label">서울</label>
</div>
</div>
@ModelAttribute
는 이렇게 컨트롤러에 있는 별도의 메서드에 적용할 수 있다. 이렇게 하면 해당 컨트롤러를 요청할 때 regions
에서 반환한 값이 자동으로 모델(model
)에 담기게 된다.th:for="${#ids.prev('regions')}"
name
은 같아도 되지만, id
는 모두 달라야 한다. 타임리프는 체크박스를 each
루프 안에서 반복해서 만들 때 임으로 1
, 2
, 3
숫자를 뒤에 붙여준다.ids.prev(...)
, ids.next(...)
를 제공해서 동적으로 생성되는 id
값을 사용할 수 있도록 한다.regions=SEOUL&_regions=on®ions=BUSAN&_regions=on&_regions=on
item.regions=[SEOUL, BUSAN]
_regions=on&_regions=on&_regions=on
item.regions=[]
item.html
에는 th:object
를 사용하지 않았기 때문에 th:field
부분에 ${item.regions}
로 적어준다. disabled
를 사용해서 상품 상세에서는 체크 박스가 선택되지 않도록 한다.checked
속성이 추가된 것을 확인할 수 있다. 타임리프는 th:field
에 지정한 값과 th:value
의 값을 비교해서 체크를 자동으로 처리해준다.@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
}
<!-- addForm.html 추가 -->
<!-- radio button -->
<div>
<div>상품 종류</div>
<div th:each="type : ${itemTypes}" class="form-check form-check-inline">
<input type="radio" th:field="*{itemType}"
th:value="${type.name()}" class="form-check-input">
<label th:for="${#ids.prev('itemType')}"
th:text="${type.description}" class="form-check-label">
BOOK
</label>
</div>
</div>
ItemType.values()
를 사용하면 해당 ENUM의 모든 정보를 배열로 반환한다. 예) [BOOK, FOOD, ETC]
item.html
에는 th:object
를 사용하지 않았기 때문에 th:field
부분에 ${item.itemType}
으로 적어준다. disabled
를 사용해서 상품 상세에서는 라디오 버튼이 선택되지 않도록 한다.checked="checked"
적용.<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
${T(hello.itemservice.domain.item.ItemType).values()}
스프링EL 문법으로 ENUM을 직접 사용할 수 있다.@ModelAttribute("deliveryCodes")
public List<DeliveryCode> deliveryCodes() {
List<DeliveryCode> deliveryCodes = new ArrayList<>();
deliveryCodes.add(new DeliveryCode("FAST", "빠른 배송"));
deliveryCodes.add(new DeliveryCode("NORMAL", "일반 배송"));
deliveryCodes.add(new DeliveryCode("SLOW", "느린 배송"));
return deliveryCodes;
}
<!-- addForm.html 추가 -->
<!-- SELECT -->
<div>
<div>배송 방식</div>
<select th:field="*{deliveryCode}" class="form-select">
<option value="">==배송 방식 선택==</option>
<option th:each="deliveryCode : ${deliveryCodes}"
th:value="${deliveryCode.code}"
th:text="${deliveryCode.displayName}">FAST</option>
</select>
</div>
@ModelAttribute
가 있는 deliveryCodes()
메서드는 컨트롤러가 호출 될 때 마다 사용되므로 deliveryCodes
객체도 계속 생성된다. 미리 생성해두고 재사용하는 것이 더 효율적.item.html
에는 th:object
를 사용하지 않았기 때문에 th:field
부분에 ${item.deliveryCode}
로 적어준다. disabled
를 사용해서 상품 상세에서는 셀렉트 박스가 선택되지 않도록 한다.selected="selected"
적용.// MessageSource는 인터페이스.
// 구현체인 ResourceBundleMessageSource를 스프링 빈으로 등록.
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("messages", "errors");
messageSource.setDefaultEncoding("utf-8");
return messageSource;
}
basename
: 설정 파일의 이름을 지정.messages
로 지정하면 messages.properties
파일을 읽어서 사용.messages_en.proerties
, messages_ko.properties
와 같이 지정. 만약 찾을 수 있는 국제화 파일이 없으면 messages.properties
를 기본으로 사용./resources/messages.properties
defaultEncoding
: 인코딩 정보를 지정.MessageSource
를 자동으로 빈으로 등록.application.properties
에 다음과 같이 메시지 소스를 설정 할 수 있다.spring.messages.basename=messages,config.i18n.messages
spring.messages.basename=messages
MessageSource
를 스프링 빈으로 등록하지 않고, 스프링 부트와 관련된 별도의 설정을 하지 않으면 messages
라는 이름으로 기본 등록된다. 따라서 messages_en.properties
, messages_ko.properties
, messages.properties
파일만 등록하면 자동으로 인식.hello=안녕
hello.name=안녕 {0}
hello=hello
hello.name=hello {0}
public interface MessageSource {
String getMessage(String code, @Nullable Object[] args,
@Nullable String defaultMessage, Locale locale);
String getMessage(String code, @Nullable Object[] args,
Locale locale) throwsNoSuchMessageException;
@SpringBootTest
public class MessageSourceTest {
@Autowired
MessageSource ms;
@Test
void helloMessage() {
String result = ms.getMessage("hello", null, null);
assertThat(result).isEqualTo("안녕");
}
}
hello
를 입력하고 나머지는 null
입력.locale
정보가 없으면 Locale.getDefault()
를 호출해서 시스템의 기본 로케일을 사용한다.locale = null
인 경우 -> 시스템 기본 locale
이 ko_KR
이므로 messages_ko.properties
조회 시도 -> 조회 실패 -> messages.properties
조회@Test
void notFoundMessageCode() {
assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
.isInstanceOf(NoSuchMessageException.class);
}
@Test
void notFoundMessageCodeDefaultMessage() {
String result = ms.getMessage("no_code", null, "기본 메시지", null);
assertThat(result).isEqualTo("기본 메시지");
}
NoSuchMessageException
발생.defaultMessage
)를 사용하면 기본 메시지가 반환.@Test
void argumentMessage() {
String message = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
assertThat(message).isEqualTo("안녕 Spring");
}
hello.name=안녕 {0}
-> Spring 단어를 매개변수로 전달 -> 안녕 Spring
@Test
void defaultLang() {
assertThat(ms.getMessage("hello", null, null)).isEqualTo("안녕");
assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("안녕");
}
en_US
의 경우 messages_en_US
-> messages_en
-> messages
순서로 찾는다.Locale
에 맞추어 구체적인 것이 있으면 구체적인 것을 찾고, 없으면 디폴트를 찾는다.ms.getMessage("hello", null, Locale.KOREA)
: locale 정보가 있지만, messages_ko
가 없으므로 messages
사용@Test
void enLang() {
assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
}
messages_en
찾아서 사용label.item=상품
label.item.id=상품 ID
label.item.itemName=상품명
label.item.price=가격
label.item.quantity=수량
page.items=상품 목록
page.item=상품 상세
page.addItem=상품 등록
page.updateItem=상품 수정
button.save=저장
button.cancel=취소
${...}
을 사용하면 스프링의 메시지를 편리하게 조회할 수 있다.<div th:text="#{label.item}"></h2>
-> <div>상품</h2>
hello.name=안녕 {0}
<p th:text="#{hello.name(${item.itemName})}"></p>
label.item=Item
label.item.id=Item ID
label.item.itemName=Item Name
label.item.price=price
label.item.quantity=quantity
page.items=Item List
page.item=Item Detail
page.addItem=Item Add
page.updateItem=Item Update
button.save=Save
button.cancel=Cancel
Accept-Language
의 값이 변경.MessageSource
테스트에서 보았듯이 메시지 기능은 Locale
정보를 알아야 언어를 선택할 수 있다.Accept-Language
헤더의 값을 사용한다.public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest request);
void setLocale(HttpServletRequest request,
@Nullable HttpServletResponse response, @Nullable Locale locale);
}
Locale
선택 방식을 변경할 수 있도록 LocaleResolver
라는 인터페이스를 제공하는데, 스프링 부트는 기본으로 Accept-Language
를 활용하는 AcceptHeaderLocaleResolver
를 사용.Locale
선택 방식을 변경하려면 LocaleResolver
의 구현체를 변경해서 쿠키나 세션 기반의 Locale
선택 기능을 사용할 수 있다. 예를 들어서 고객이 직접 Locale
을 선택하도록 하는 것이다. 관련해서 LocaleResolver
를 검색 해보자.