Search

Thymeleaf

타임리프란?

모던 서버 사이드 자바 템플릿 엔진
백엔드 서버에서 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 이스케이프 지원
#dates : java.util.Date 서식 지원
#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&param2=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 &gt; 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 정보를 추가한다.