타임리프란?
•
모던 서버 사이드 자바 템플릿 엔진
◦
백엔드 서버에서 HTML을 동적으로 렌더링하는 용도로 사용된다.
•
네츄럴 템플릿
◦
순수 HTML을 최대한 유지한다.
◦
웹 브라우저로 파일을 직접 열어도 내용을 확인할 수 있고,
서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.
◦
JSP를 포함한 다른 뷰 템플릿들은 파일을 직접 열면 정상적인 HTML 결과를 확인할 수 없다.
•
스프링 통합 지원
◦
스프링의 SpringEL 문법 통합
◦
스프링 빈 호출 지원
▪
${@myBean.doSomething()}
◦
편리한 폼 관리를 위한 추가 속성
▪
th:object (기능 강화, 폼 커맨드 객체 선택), th:field, th:errors, th:errorclass
◦
폼 컴포넌트 기능
▪
checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원
◦
스프링의 메시지, 국제화 기능의 편리한 통합
◦
스프링의 검증, 오류 처리 통합
◦
스프링의 변환 서비스 통합(ConversionService)
타임리프의 핵심
•
타임리프 뷰 템플릿을 거치게 되면 th:xxx 가 붙은 부분은 서버사이드에서 렌더링되고 기존 것을 대체한다.
th:xxx 가 없으면 기존 html의 xxx 속성이 그대로 사용된다.
•
HTML 파일을 직접 열었을 때 th:xxx 는 무시된다.
타임리프 사용 선언
<html xmlns:th="http://www.thymeleaf.org">
HTML
복사
•
<html> 안에 xmlns:th=”http://www.thymeleaf.org” 를 추가해준다.
Overview
•
간단한 표현
◦
변수 표현식 : ${…}
◦
선택 변수 표현식 : *{…}
◦
메시지 표현식 : #{…}
◦
링크 URL 표현식 : @{…}
◦
조각 표현식 : ~{…}
•
리터럴
◦
텍스트 : ‘text’
◦
숫자 : 0, 34, 3.0, 12.3
◦
불린 : true, false
◦
널 : null
◦
리터럴 토큰 : one, sometext, main, …
•
문자 연산
◦
문자 합치기 : +
◦
리터럴 대체 : |…|
•
산술 연산
◦
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 : _
텍스트 - th:text, th:utext
<li>th:text 사용 <span th:text="${data}"></span></li>
<li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li>
<li>th:utext 사용 <span th:utext="${data}"></span></li>
<li>컨텐츠 안에서 직접 출력하기 = [(${data})]</li>
HTML
복사
•
Escape
◦
HTML 에서 <, > 같은 특수문자를 문자로 나타내는 것 = HTML 엔티티
◦
th:text 나 [[…]] 를 사용한다
•
UnEscape
◦
Escape 기능을 사용하지 않기 위한 방법
◦
th:utext 나 [(…)]를 사용한다
변수 - SpringEL
•
타임리프의 변수 표현식 ${…} 에는 스프링이 제공하는 SpringEL이라는 표현식을 사용할 수 있다.
<h1>SpringEL 표현식</h1>
<ul>Object
<li>${user.username} = <span th:text="${user.username}"></span></li>
<li>${user['username']} = <span th:text="${user['username']}"></span></li>
<li>${user.getUsername()} = <span th:text="${user.getUsername()}"></span></li>
</ul>
<ul>List
<li>${users[0].username} = <span th:text="${users[0].username}"></span></li>
<li>${users[0]['username']} = <span th:text="${users[0]['username']}"></span></li>
<li>${users[0].getUsername()} = <span th:text="${users[0].getUsername()}"></span></li>
</ul>
<ul>Map
<li>${userMap['userA'].username} = <span th:text="${userMap['userA'].username}"></span></li>
<li>${userMap['userA']['username']} = <span th:text="${userMap['userA']['username']}"></span></li>
<li>${userMap['userA'].getUserName()} = <span th:text="${userMap['userA'].getUsername()}"></span></li>
</ul>
HTML
복사
•
Object
◦
user.username : user의 username에 프로퍼티 접근 ( = user.getUsername())
◦
user[’username’] : 위와 같음.
◦
user.getUsername() : 위와 같음. getUsername()을 직접 호출
•
List
◦
users[0].username : user의 0번 인덱스 username에 프로퍼티 접근 (= list.get(0).getUsername())
◦
users[0]['username'] : 위와 같음.
◦
users[0].getUsername() : 위와 같음. getUsername()을 직접 호출
•
Map
◦
userMap['userA'].username : Map에서 userA를 찾고 username에 프로퍼티 접근 (= map.get(”userA”).getUsername())
◦
userMap['userA']['username'] : 위와 같음.
◦
userMap['userA'].getUsername() : 위와 같음. getUsername()을 직접 호출
지역변수 - th:with
<h1>지역 변수 - (th:with)</h1>
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
HTML
복사
•
사용할 태그 안에 th:with=”변수이름=값” 으로 선언해서 사용할 수 있다.
•
선언한 태그 안에서만 사용할 수 있다.
기본 객체들
<h1>기본 객체 (Expression Basic Objects)</h1>
<ul>
<li>request = <span th:text="${#request}"></span></li>
<li>response = <span th:text="${#response}"></span></li>
<li>session = <span th:text="${#session}"></span></li>
<li>servletContext = <span th:text="${#servletContext}"></span></li>
<li>locale = <span th:text="${#locale}"></span></li>
</ul>
HTML
복사
•
${#request}
•
${#response}
•
${#session}
•
${#servletContext}
•
${#locale}
편의 객체들
<h1>편의 객체</h1>
<ul>
<li>Request Parameter<span th:text="${param.paramData}"></span></li>
<li>session = <span th:text="${session.sessionData}"></span></li>
<li>spring bean = <span th:text="${@helloBean.hello('Spring!')}"></span></li>
</ul>
HTML
복사
•
HTTP 요청 쿼리 파라미터 접근
◦
${param.XXX}
•
HTTP 세션 접근
◦
${session.XXX}
•
스프링 빈 접근
◦
${@빈이름.메서드이름}
유틸리티 객체
•
#message : 메시지, 국제화 처리
•
#uris : URI 이스케이프 지원
•
•
#calendars : java.util.Calendar 서식 지원
•
#temporals : 자바8 날짜 서식 지원
•
#numbers : 숫자 서식 지원
•
#strings : 문자 관련 편의 기능
•
#objects : 객체 관련 기능 제공
•
#bools : boolean 관련 기능 제공
•
#arrays : 배열 관련 기능 제공
•
#lists, #sets, #maps : 컬렉션 관련 기능 제공
•
#ids : 아이디 처리 관련 기능 제공
날짜
<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>
HTML
복사
URL 링크 표현식 - @{…}
<h1>URL 링크</h1>
<ul>
<li><a href="@{/hello}">basic url</a></li>
<li><a href="@{/hello(param1=${param1}, param2=$P{param2})}">hello query param</a></li>
<li><a href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>
<li><a href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>
</ul>
HTML
복사
•
단순 URL
◦
@{/hello} → /hello
•
쿼리 파라미터
◦
@{/hello(param1=${param1}, param2=${param2})} → /hello?param1=data1¶m2=data2
◦
() 부분은 쿼리 파라미터로 처리된다.
•
경로 변수 + 쿼리 파라미터
◦
@{/hello/{param1}(param1=${param1}, param2=${param2})} → /hello/data1?param2=data2
◦
() 부분은 경로 변수로 먼저 사용 후 남는건 쿼리 파라미터로 처리된다.
<link rel="stylesheet" href="value1" th:href="@{value2}">
<a href="value1" th:href="@{value2}"></a>
<a th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"></a>
<a th:href="@{|/basic/items/${item.id}|}"></a>
HTML
복사
•
타임리프는 URL 링크를 사용하는 경우 @{…} 를 사용하고 이를 URL 링크 표현식이라고 한다.
•
URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.
•
@{…} 안에 경로변수{itemId} 를 사용하여 경로를 템플릿처럼 편리하게 사용할 수 있다.
◦
경로변수 뒤에 (itemId=${item.id})를 통해 변수에 값을 대입할 수 있다.
◦
또한 (itemId=${item.id}, query=’test’) 처럼 쿼리 파라미터를 생성할 수도 있다.
•
|…| 리터럴 대체 문법을 사용하여 URL 링크를 간단하게 사용할 수도 있다.
리터럴
<h1>리터럴</h1>
<ul>
<!--주의! 다음 주석을 풀면 예외가 발생함-->
<!-- <li>"hello world!" = <span th:text="hello world!"></span></li>-->
<li>'hello' + ' world!' = <span th:text="'hello' + ' world!'"></span></li>
<li>'hello world!' = <span th:text="'hello world!'"></span></li>
<li>'hello ' + ${data} = <span th:text="'hello ' + ${data}"></span></li>
<li>리터럴 대체 |hello ${data}| = <span th:text="|hello ${data}|"></span></li>
</ul>
HTML
복사
•
소스 코드 상에서 고정된 값을 말하는 용어이다.
◦
문자 : ‘hello’
◦
숫자 : 10
◦
불린 : true, false
◦
null : null
•
타임리프에서 문자 리터럴은 항상 작은 따옴표(’’)로 감싸야한다.
(공백 없이 쭉 이어진다면 하나의 의미있는 토큰으로 인지해 생략할 수 있다.)
리터럴 대체 문법 - |…|
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
<span th:text="|Welcome to our application, ${user.name}!|">
HTML
복사
•
타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 + 로 붙여서 사용해야 한다.
•
리터럴 대체 문법을 사용하면 + 없이 편리하게 사용할 수 있다.
연산
<li>산술 연산
<ul>
<li>10 + 2 = <span th:text="10 + 2"></span></li>
<li>10 % 2 == 0 = <span th:text="10 % 2 == 0"></span></li>
</ul>
</li>
<li>비교 연산
<ul>
<li>1 > 10 = <span th:text="1 > 10"></span></li>
<li>1 gt 10 = <span th:text="1 gt 10"></span></li>
<li>1 >= 10 = <span th:text="1 >= 10"></span></li>
<li>1 ge 10 = <span th:text="1 ge 10"></span></li>
<li>1 == 10 = <span th:text="1 == 10"></span></li>
<li>1 != 10 = <span th:text="1 != 10"></span></li>
</ul>
</li>
<li>조건식
<ul>
<li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)?'짝수':'홀수'"></span></li>
</ul>
</li>
<li>Elvis 연산자
<ul>
<li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가 없습니다.'"></span></li>
<li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?:'데이터가 없습니다.'"></span></li>
</ul>
</li>
<li>No-Operation
<ul>
<li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</span></li>
<li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가 없습니다.</span></li>
</ul>
</li>
HTML
복사
•
비교 연산 : HTML 엔티티를 사용하는 부분을 주의
•
조건식 : 자바의 조건식과 유사하다.
•
Elvis 연산자 : 조건식의 편의 버전 (= if-else)
•
No-Operation : _ 인 경우 마치 타임리프가 실행되지 않는 것처럼 동작한다.
속성 값 설정
•
타임리프는 주로 HTML 태그에 th:* 속성을 지정하는 방식으로 동작한다.
•
th:* 속성을 적용하면 기존 속성을 대체한다. (기존 속성이 없으면 새로 만든다.)
<!-- 코드 -->
<input type="text" name="mock" th:name="userA" />
<!-- 렌더링 전 -->
<input type="text" name="mock"/>
<!-- 렌더링 후 -->
<input type="text" name="userA"/>
HTML
복사
•
th:attrappend : 속성 값 뒤에 값을 이어 붙인다.
•
th:attrprepend : 속성 값 앞에 값을 이어 붙인다.
•
th:classappend : class 속성에 자연스럽게 뒤에 이어 붙인다.
•
th:checked : HTML 태그에서 checked 속성은 값과 상관없이 존재하기만 해도 체크가 된다.
타임리프의 th:checked 는 값이 false인 경우 checked 속성 자체를 제거한다.
반복 - th:each
<table>
<tr>
<th>username</th>
<th>age</th>
</tr>
<tr th:each="user : ${users}">
<td th:text="${user.username}">username</td>
<td th:text="${user.age}">0</td>
</tr>
</table>
HTML
복사
•
Model에 포함된 users 컬렉션 데이터가 user 하나씩 포함되고 반복문 안에서 user을 사용할 수 있다.
•
컬렉션의 수만큼 해당 태그가 하위 태그를 포함해서 생성된다.
•
컬렉션은 Iterable, Enumeration을 구현한 모든 객체를 사용할 수 있다.
반복 상태 유지
<h1>반복 상태 유지</h1>
<table>
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
<th>etc</th>
</tr>
<tr th:each="user, userStat : ${users}">
<td th:text="${userStat.count}">username</td>
<td th:text="${user.username}">username</td>
<td th:text="${user.age}">0</td>
<td>
index = <span th:text="${userStat.index}"></span>
count = <span th:text="${userStat.count}"></span>
size = <span th:text="${userStat.size}"></span>
even? = <span th:text="${userStat.even}"></span>
odd? = <span th:text="${userStat.odd}"></span>
first? = <span th:text="${userStat.first}"></span>
last? = <span th:text="${userStat.last}"></span>
current = <span th:text="${userStat.current}"></span>
</td>
</tr>
</table>
HTML
복사
•
반복의 두번째 파라미터(userStat)를 생성해서 반복의 상태를 확인할 수 있다.
(생략하고 사용이 가능하며 생략 시 변수명 + Stat으로 사용할 수 있다.)
•
반복 상태 유지 기능
◦
index : 0부터 시작하는 값
◦
count : 1부터 시작하는 값
◦
size : 전체 사이즈
◦
even, odd : 홀수, 짝수 여부 (boolean)
◦
first, last : 처음, 마지막 여부 (boolean)
◦
current : 현재 객체
조건부 평가 - if, unless
<tr th:each="user, userStat : ${users}">
<td th:text="${userStat.count}">1</td>
<td th:text="${user.username}">username</td>
<td>
<span th:text="${user.age}">0</span>
<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
</td>
</tr>
HTML
복사
•
th:if 의 값이 true면 해당 태그를 렌더링하고 false면 해당 태그를 렌더링하지 않는다.
•
th:unless 는 if와 반대이다. (false면 렌더링 true면 렌더링 안함)
조건부 평가 - switch-case
<tr th:each="user, userStat : ${users}">
<td th:text="${userStat.count}">1</td>
<td th:text="${user.username}">username</td>
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="*">기타</span>
</td>
</tr>
HTML
복사
•
th:switch 는 변수, th:case 는 해당 변수에 들어올 수 있는 값들을 지정해서 값이 맞는 것을 렌더링한다.
•
th:case=”*” 를 통해 만족하는 값이 없더라도 렌더링하는 기본 값을 지정할 수 있다.
주석
<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>
/*/-->
HTML
복사
•
표준 HTML 주석
◦
<!—- comment -—>
◦
HTML 에서도 주석으로 적용되고 타임리프에서도 주석으로 적용되어 렌더링 하지 않고 그대로 남겨둔다.
•
타임리프 파서 주석
◦
<!—-/* comment */-—>
◦
타임리프의 진짜 주석으로 서버를 통해 렌더링 할 때 주석 부분을 제거해버린다.
•
타임리프 프로토타입 주석
◦
<!—-/*/ comment /*/-—>
◦
HTML 파일을 웹 브라우저에서 열면 HTML 주석으로 인식되어 렌더링 되지 않는다.
◦
하지만 서버를 통해 렌더링하게 되면 정상적으로 렌더링된다.
블록
<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>
<!-- 출력 결과
사용자 이름1 userA 사용자 나이1 10
요약 userA / 10
사용자 이름1 userB 사용자 나이1 20
요약 userB / 20
사용자 이름1 userC 사용자 나이1 30
요약 userC / 30
-->
HTML
복사
•
th:block 내부의 태그를 정해진 횟수만큼 반복해서 렌더링하고 th:block은 제거된다.
자바스크립트 인라인
•
타임리프는 자바스크립트에서도 타임리프를 사용할 수 있게끔 자바스크립트 인라인 기능을 제공한다.
◦
<script th:inline=”javascript”>
<!-- 자바스크립트 인라인 사용 전 -->
<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
복사
•
텍스트 렌더링
◦
var username = [[${user.username}]];
◦
인라인 사용 전 → var username = userA;
◦
인라인 사용 후 → var username = “userA”;
◦
렌더링 결과가 문자 타입인 경우 “”를 포함해 자바스크립트 문법 오류가 나지 않도록 해준다.
◦
또한 자바스크립트에서 문제가 될 수 있는 문자가 포함되면 이스케이프 처리도 해준다. (” → \”)
•
자바스크립트 네추럴 템플릿
◦
var username2 = /*[[${user.username}]]*/ “test username”;
◦
인라인 사용 전 → var username2 = /*userA*/ "test username";
◦
인라인 사용 후 → var username2 = "userA";
◦
표현식이 주석처리되어 있더라도 개발자가 의도한대로 작동하도록 해준다.
•
객체
◦
var user = [[${user}]];
◦
인라인 사용 전 → var user = BasicController.User(username=userA, age=10);
◦
인라인 사용 후 → var user = {”username”:”userA”,”age”:10};
◦
인라인 사용 전은 객체의 toString()이 호출되었고 인라인 사용 후는 객체를 JSON으로 변환했다.
자바스크립트 인라인 each
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
<!-- 출력 결과 -->
<script>
var user1 = {"username":"userA","age":10};
var user2 = {"username":"userB","age":20};
var user3 = {"username":"userC","age":30};
</script>
HTML
복사
•
th:each 또한 자바스크립트 인라인에서 사용 가능하다.
템플릿 조각
<!-- footer.html -->
<footer th:fragment="copy">
푸터 자리 입니다.
</footer>
<footer th:fragment="copyParam (param1, param2)">
<p>파라미터 자리 입니다.</p>
<p th:text="${param1}"></p>
<p th:text="${param2}"></p>
</footer>
HTML
복사
•
th:fragment 가 있는 태그는 하위 태그를 포함하며 다른 곳에 포함되는 코드 조각이다.
•
th:fragment=”copy” 와 같이 이름을 지정할 수 있다.
•
th:fragment="copyParam (param1, param2)” 와 같이 파라미터도 지정할 수 있다.
<!-- fragmentMain.html -->
<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>
HTML
복사
<!-- fragmentMain.html 출력 결과 -->
<h1>부분 포함</h1>
<h2>부분 포함 insert</h2>
<div><footer>
푸터 자리 입니다.
</footer></div>
<h2>부분 포함 replace</h2>
<footer>
푸터 자리 입니다.
</footer>
<h2>부분 포함 단순 표현식</h2>
<footer>
푸터 자리 입니다.
</footer>
<h1>파라미터 사용</h1>
<footer>
<p>파라미터 자리 입니다.</p>
<p>데이터1</p>
<p>데이터 2</p>
</footer>
HTML
복사
•
부분 포함 inster
◦
th:insert=”~{fragment 경로 :: fragment 이름}”
◦
th:insert가 포함된 태그인 <div></div> 사이에 fragment가 포함된다.
•
부분 포함 replace
◦
th:replace="~{template/fragment/footer :: copy}"
◦
th:replace가 포함된 태그인 <div></div> 를 fragment가 대체한다.
•
부분 포함 단순 표현식
◦
~{…} 를 사용하는 것이 원칙이지만 단순히 템플릿 조각을 가져오는 경우엔 생략할 수 있다.
•
파라미터 사용
◦
th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}”
◦
파라미터를 전달해서 동적으로 렌더링 할 수도 있다.
템플릿 레이아웃 1 - 유연한 레이아웃
•
템플릿 조각을 활용해 레이아웃을 미리 만들어놓고 호출 시 부분 포함을 활용해 완성시킨 레이아웃을 가져와 응답하게 된다.
<!-- base.html (미리 만들어 놓은 레이아웃) -->
<!DOCTYPE html>
<html lang="ko" 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>
HTML
복사
•
th:fragment 를 이용해 템플릿 조각으로 지정해놓고 <title>과 <link>를 파라미터로 받을 수 있게 했다.
◦
전달 받은 <title>은 레이아웃의 <title>을 대체하고
◦
th:block 을 이용해 전달 받은 <link> 들을 추가한다.
<!-- layoutMain.html (호출되는 view) -->
<!DOCTYPE html>
<html lang="ko" 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>
HTML
복사
•
th:replace 를 이용해 <head> 태그를 미리 만들어 놓은 레이아웃으로 대체한다.
◦
이 때 ~{::…} 를 이용해 파라미터로 해당 태그들을 전달한다.
<!-- 출력 결과 -->
<!DOCTYPE html>
<html lang="ko">
<head> <title>메인 타이틀</title>
<!-- 공통 -->
<link rel="stylesheet" type="text/css" media="all" href="/css/awesomeapp.css">
<link rel="shortcut icon" href="/images/favicon.ico">
<script type="text/javascript" src="/sh/scripts/codebase.js"></script>
<!-- 추가 -->
<link rel="stylesheet" href="/css/bootstrap.min.css"><link rel="stylesheet" href="/themes/smoothness/jquery-ui.css">
</head>
<body>
메인 컨텐츠
</body>
</html>
HTML
복사
•
결과를 확인하면 전달한 파라미터를 이용해 완성한 레이아웃으로 대체된 것을 확인할 수 있다.
템플릿 레이아웃 2 - 레이아웃 상속
•
<head> 같은 부분이 아닌 <html> 전체를 레이아웃으로 만들어 페이지를 상속 받을 수 있다.
<!-- layoutFile.html (미리 만들어 둔 레이아웃) -->
<!DOCTYPE html>
<html lang="ko" th:fragment="layout(title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:replace="${title}">레이아웃 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}">
<p>레이아웃 컨텐츠</p>
</div>
<footer>
레이아웃 푸터
</footer>
</body>
</html>
HTML
복사
•
th:fragment 를 이용해 템플릿 조각으로 지정해놓고 <title>과 content를 파라미터로 받을 수 있게 했다.
◦
전달 받은 <title>은 레이아웃의 <title>을 대체하고
◦
전달 받은 content는 <div th:replace=”${content}”> 를 통해 해당 태그를 대체한다.
<!-- layoutExtendMain.html (View 파일) -->
<!DOCTYPE html>
<html lang="ko" th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title}, ~{::section})}" xmlns:th="http://www.thymeleaforg">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>메인 페이지 타이틀</title>
</head>
<body>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
</body>
</html>
HTML
복사
•
th:replace 를 이용해 <html> 태그 전부를 미리 만들어 놓은 레이아웃으로 대체한다.
◦
이 때 ~{::…} 를 이용해 파라미터로 해당 태그들을 전달한다.
<!-- 출력 결과 -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>메인 페이지 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
<footer>
레이아웃 푸터
</footer>
</body>
</html>
HTML
복사
•
결과를 확인하면 전달한 파라미터를 이용해 레이아웃을 완성시켜 완전히 레이아웃으로 대체된 것을 확인할 수 있다.
입력 폼 처리 - th:object, th:field
•
사용하기 위해선 Controller에서 Model에 데이터를 담아야 한다.
<!-- 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>
...
HTML
복사
<!-- editForm.html -->
<form action="item.html" th:action th:object="${item}" method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" th:field="*{id}" class="form-control" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}" class="form-control">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" th:field="*{price}" class="form-control">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" th:field="*{quantity}" class="form-control">
</div>
...
HTML
복사
•
th:object
◦
커맨드 객체를 지정한다. (Model에 담겨 있는 데이터를 사용할 수 있게 해준다.)
◦
<form> 태그에 지정하며 해당 <form> 태그 내에서만 유효하다.
•
th:field
◦
선택 변수식 *{…} 를 통해 th:object 에서 지정한 객체에 접근할 수 있다.
*{itemName} == ${item.itemName}
◦
th:field 를 사용하게 되면 id, name, value 속성을 자동으로 만들어주므로 생략해도 된다.
렌더링 전 : <input type="text" th:field="*{itemName}" class="form-control">
렌더링 후 : <input type="text" class="form-control" id="itemName" name="itemName"
value="itemA">
체크박스
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input">
<input type="hidden" id="_open" value="on">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
HTML
복사
•
체크박스를 선택하게 되면 HTTP Form에서 체크박스에 대한 on 이라는 값이 넘어온다.
스프링은 이런 on 이라는 문자를 스프링 타입 컨버터에 의해 boolean 타입으로 변환해준다.
•
체크박스를 선택하지 않은 경우 체크박스에 대한 값 자체가 넘어오지 않는다. (null)
이 경우 체크박스 이름 앞에 _ 를 붙인 hidden 필드를 만들어 전송해야 한다 (hidden 필드는 항상 전송된다.)
스프링은 체크박스 값이 넘어오면 hidden 필드를 무시하고 hidden 필드만 넘어오면 false로 인식한다.
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" name="open" th:field="*{open}" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
<!-- 렌더링 후 -->
<div>판매 여부</div> <div>
<div class="form-check">
<input type="checkbox" id="open" class="form-check-input" name="open" value="true">
<input type="hidden" name="_open" value="on"/>
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
HTML
복사
•
하지만 히든 필드를 매번 추가하는건 상당히 번거롭다.
th:field 를 사용해 th:object 객체의 boolean 변수를 바인딩해주면 스프링은 히든 필드를 추가해준다.
다중 체크 박스
//FormItemController.java
@ModelAttribute("regions")
public Map<String, String> regions() {
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
return regions;
}
Java
복사
•
@ModelAttribute의 특별한 기능
◦
기존에는 객체 파라미터에 붙여서 객체를 생성하고 값을 바인딩한 후 model.addAttribute()를 자동으로 수행하기 위해 사용했었다.
◦
이제 컨트롤러 내에 별도의 메서드에 붙이게 되면 컨트롤러에 모든 요청에 대해 해당 메서드를 호출하고 받은 반환 값을 자동으로 model.addAttribute()하게 된다.
<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>
HTML
복사
•
th:each 를 이용해 regions 의 크기만큼 해당 <div> 태그를 반복하고 각 값을 region으로 사용할 수 있다.
•
th:field 는 ${item.region}과 같으며 id, name, value를 자동으로 추가해준다.
이 때, id는 중복되지 않아야하므로 뒤에 1부터 순서대로 숫자를 붙인다.
•
th:for=${#ids.prev(’regions’)} 는 regions 뒤에 1부터 순서대로 숫자를 붙여 <input>의 id를 매칭한다.
•
th:value 와 th:text 는 th:each가 가져온 region에서 key, value를 꺼내 사용한다.
<!-- item.html -->
<div>
<div>등록 지역</div>
<div th:each="region : ${regions}" class="form-check form-check-inline">
<input type="checkbox" th:field="${item.regions}" th:value="${region.key}" class="form-check-input" disabled>
<label th:for="${#ids.prev('regions')}" th:text="${region.value}" class="form-check-label">서울</label>
</div>
</div>
HTML
복사
•
상품 등록 후 상세페이지에선 체크박스가 disabled 여야 하고 체크했던 상태를 유지(checked)해야 한다.
◦
disabled는 <input> 태그에 추가해주면 쉽게 적용할 수 있다.
◦
checked는 타임리프가 th:value 를 처리하면서 th:field에 지정한 값과 같으면 자동으로 처리해준다.
라디오 버튼
@Getter
public enum ItemType {
BOOK("도서"), FOOD("음식"), ETC("기타");
private final String description;
ItemType(String description) {
this.description = description;
}
}
Java
복사
•
먼저 라디오 버튼에 값을 넣기 위한 Enum을 만들어준다.
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
}
Java
복사
•
Controller에 @ModelAttribute를 사용하여 itemType을 model에 자동으로 추가해 사용하도록 한다.
<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>
HTML
복사
•
이 부분은 체크 박스와 비슷하다
타임리프에서 ENUM 직접 접근
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
HTML
복사
•
SpringEL 문법으로 T()로 ENUM의 경로를 지정해준 후 프로퍼티 접근법을 통해 직접 사용할 수 있다.
셀렉트 박스
//DeliveryCode.java
@Data
@AllArgsConstructor
public class DeliveryCode {
private String code;
private String displayName;
}
//FormItemController.java
@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;
}
Java
복사
<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>
HTML
복사
•
셀렉트 박스는 <select> 태그를 쓴다는거 말곤 체크박스, 라디오버튼과 비슷하다.
타임리프 스프링 통합 검증 오류 처리
<!-- 글로벌 에러 -->
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
</div>
HTML
복사
•
th:if=”${#fields.hasGlobalErrors()}” : 글로벌 에러 처리
•
th:each="err : ${#fields.globalErrors()}" : 글로벌 에러 처리
<!-- 필드 에러 -->
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:errors="*{itemName}">상품명 오류</div>
</div>
HTML
복사
•
#fields : 타임리프가 스프링의 BindingResult가 제공하는 검증 오류에 접근할 수 있다.
•
th:errors : 해당 필드에 오류가 있는 경우 태그를 출력한다. (th:if의 편의버전)
•
th:errorclass : th:field에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.