이 글을 작성하고 난 뒤에는 내가 나중에 타임리프를 사용할 때에, 문법이 잘 기억이 나지 않을 때 이 글만 보아도 기본적인 타임리프 기능들을 사용할 수 있었으면 한다.
타임리프 연산은 일반적인 프로그래밍 연산과 크게 다르지 않다.
하지만 HTML 안에서 사용하는 것이기 때문에 HTML 엔티티를 사용하는 부분을 주의 해야한다.
HTML 엔티티가 사용되는 것은 대부분이 비교 연산임으로 이때는 다음과 같은 문법을 사용할 것을 권장한다.
>
(gt)<
(lt)>=
(ge)<=
(le)!
(not)==
(eq)!=
(neq, ne)물론 < , > 에 대한 엔티티만 조심한다면 ==, !, !=, >=, <= 와 같은 연산은 그냥 사용해도 무방하다.
타임리프에서는 Elvis 연산자를 지원하는데 예를 들어 서버로 부터 data라는 객체를 전달받았다고 가정 해보자.
<span th:text="${data} ? ${data} : '데이터가 없습니다.'"></span> <!-- (1) -->
<span th:text="${data} ?: '데이터가 없습니다.'"></span> <!-- (2) -->
data
변수가 참이면 data
를, 거짓이라면 데이터가 없습니다.
를 출력한다.No-Operation이란 '_'
언더스코어(밑줄)을 사용하여 마치 타임리프가 실행되지 않는 것처럼 동작하게끔 한다.
<span th:text="${data} ?: '데이터가 없습니다.'"></span> <!-- (1) -->
<span th:text="${data} ?: _">데이터가 없습니다.</span> <!-- (2) -->
위 코드는 서로 다르지만 같은 결과를 가진다.
데이터가 없습니다.
문자가 아닌 '_'
언더스코어(밑줄)을 사용했다. 타임리프를 문법을 사용하지 않고 HTML 내용을 그대로 사용하겠다는 의미이다. 즉 data
가 거짓이라면 span 내에 삽입된 데이터가 없습니다.
가 출력된다.이렇듯 No-Operation을 적절히 잘 확용 한다면 HTML의 내용을 그대로 사용할 수 있다.
타임리프에서도 HTML 속성을 동적으로 설정이 가능한데 Tag에 th:*
속성을 지정하는 방식으로 사용한다. 속성을 적용하면 기존 속성이 대체되고 없다면 새로 만든다.
속성을 새로이 설정하는 방법과 속성을 추가하는 방법, checked 처리를 하는 방법을 알아보자.
<input type="text" name="mock" th:name="userA" />
name 속성 mock이 타임리프로 렌더링 되며 userA로 변경된다.
<input type="text" class="text" th:attrappend="class='large'" /> <!-- (1) -->
<!-- @실제 렌더링시... <input type="text" class="textlarge" /> -->
<input type="text" class="text" th:attrprepend="class='large '" /> <!-- (2) -->
<!-- @실제 렌더링시... <input type="text" class="large text" /> -->
<input type="text" class="text" th:classappend="large" /> <!-- (3) -->
<!-- @실제 렌더링시... <input type="text" class="text large" /> -->
<input type="checkbox" name="active" th:checked="true" /> <!-- (1) -->
<input type="checkbox" name="active" th:checked="false" /> <!-- (2) -->
<input type="checkbox" name="active" checked="false" /> <!-- (3) -->
HTML은 checked 속성만 들어가 있어도 내부의 값을 상관하지 않고 체크상태로 표시하는데 타임리프에서는 true와 false로 이를 제어할 수 있다.
모든 템플릿에 반복이 존재하듯 타임리프에서도 반복을 수행할 수 있다. 반복문을 수행하는 타임리프 키워드는 다음과 같다.
th:each="item : ${items}"
@GetMapping("...")
public String ...(Model model) {
addUser(model);
return "...";
}
private void addUser(Model model) {
List<User> list = new ArrayList<>();
list.add(new User("userA", 10));
list.add(new User("userB", 20));
list.add(new User("userC", 30));
model.addAttribute("users", list);
}
@Data
static class User {
private String username;
private int age;
public User(String username, int age) {
this.username = username;
this.age = age;
}
}
userA, userB, userC 총 3개의 User를 가지는 list가 model에 담겨 전달되었다.
<table border="1">
<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>
위 th:each
문은 java의 다음 코드와 동일하다.
for(User user : users) {
system.out.println(user.getUsername());
system.out.println(user.getAge());
}
즉 tr Tag를 users
의 길이만큼 loop를 돌면서 생성하면서 내부에 td Tag를 생성한다. 실제 렌더된 결과는 다음과 같다.
반복중 현재 몇번째 index가 반복되고 있는지, 그 index의 count는 몇개인지, 반복되는 List(컬렉션 등)의 총 사이즈는 얼마인지, 현재 짝수 반복인지, 홀수 반복인지 기타등 편리한 기능을 제공한다.
<table border="1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
<th>etc</th>
</tr>
<tr th:each="user, userStat : ${users}"> <!-- userStat 생략 가능 -->
<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>
userStat
라는 두번째 파라미터를 지정하여 위와 같이 반복중에 필요한 상태를 확인할 수 있다.
사실 userStat를 생략할 수 있다. 기본적으로 타임리프에서 user + 'Stat' 자동으로 만들어주기 때문이다. 다음과 같이 작성하여도 userStat를 사용했을때 문제없이 동작한다.
<tr th:each="user : ${users}">
타임리프에서는 3가지 조건부 평가가 가능하다.
@GetMapping("...")
public String ...(Model model) {
addUser(model);
return "...";
}
private void addUser(Model model) {
List<User> list = new ArrayList<>();
list.add(new User("userA", 10));
list.add(new User("userB", 20));
list.add(new User("userC", 30));
model.addAttribute("users", list);
}
@Data
static class User {
private String username;
private int age;
public User(String username, int age) {
this.username = username;
this.age = age;
}
}
<table border="1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
</tr>
<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> <!-- (1) -->
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span> <!-- (2) -->
</td>
</tr>
</table>
if(!(true))
와 같은 의미이다. 해당 조건이 거짓일 때만 수행하며, 만약 조건이 참이라면 span 자체도 생성되지 않는다.<table border="1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
</tr>
<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}"> <!-- (1) -->
<span th:case="10">10살</span> <!-- (2) -->
<span th:case="20">20살</span>
<span th:case="*">기타</span> <!-- (3) -->
</td>
</tr>
</table>
${user.age}
의 값과 맞는 값을 찾는 switch문 이다.${user.age}
의 값이 10일 경우 span 태그를 생성한다.*
로 표시한다. ${user.age}
의 값이 10,20이 아니라면 해당 span 태그가 생성될 것이다.타임리프에서 사용가능한 주석은 3가지 타입이 있다.
<!-- <span>표준 HTML 주석</span> -->
표준 HTML 주석은 타임리프가 렌더링 하지 않고, 그대로 남겨둔다.
<!--/* <span>표준 HTML 주석</span> */-->
타임리프 파서 주석은 타임리프의 진짜 주석이다. 타임리프 렌더링에서 주석 부분을 제거한다.
<!--/*/ <span>표준 HTML 주석</span> /*/-->
타임리프 프로토타입은 약간 특이한데, HTML 주석에 약간의 구문을 더했다. HTML 파일을 웹 브라우저에서 그대로 열어보면 <!-- -->
을 포함하는 HTML 주석이기 때문에 이 부분이 웹 브라우저가 렌더링하지 않는다. 타임리프 렌더링을 거치면 이 부분이 정상 렌더링 된다. HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 통해 렌더링 한 경우에만 출력된다.
타임리프 블록 <th:block>
은 HTML 태그가 아닌 타임리프의 유일한 자체 태그이다. 특정한 HTML 태그에 for loop를 지정하는것이 아닌 특정 구역에 대한 loop를 돌기 위하여 사용한다.
@GetMapping("...")
public String ...(Model model) {
addUser(model);
return "...";
}
private void addUser(Model model) {
List<User> list = new ArrayList<>();
list.add(new User("userA", 10));
list.add(new User("userB", 20));
list.add(new User("userC", 30));
model.addAttribute("users", list);
}
@Data
static class User {
private String username;
private int age;
public User(String username, int age) {
this.username = username;
this.age = age;
}
}
<th:block th:each="user : ${users}">
<div>
사용자 이름 <span th:text="${user.username}"></span>
사용자 나이 <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
div 2개에 대한 loop를 돌기 위하여 th:block 이란 특수한 타임리프 태그를 만들어 반복하도록 했다. 사실 위 코드는 아래와 유사하다.
<div th:each="user : ${users}">
<div>
사용자 이름 <span th:text="${user.username}"></span>
사용자 나이 <span th:text="${user.age}"></span>
</div>
<div>
요약 <span th:text="|${user.username} / ${user.age}|"></span>
</div>
</div>
위와 같이 바깥 loop에 th:block이 아닌 div로 사용하여도 똑같은 결과를 낼 수 있지만 HTML 태그가 추가로 필요한 다른 점이 존재한다. 가능하면 태그에 for loop를 넣는것을 추천하지만 해결하기 힘든 특별한 반복이 필요한 경우에 사용하도록 하자.
타임리프 HTML파일에서 자바스크립트 사용시 편리하게 사용할 수 있는 인라인 기능을 제공한다. 인라인 기능을 사용하고자 할때는 <script th:inline="javascript"></script>
처럼 선언해주어야 한다.
<script></script>
<script th:inline="javascript"></script>
@GetMapping("...")
public String ...(Model model) {
addUser(model);
return "...";
}
private void addUser(Model model) {
List<User> list = new ArrayList<>();
list.add(new User("userA", 10));
list.add(new User("userB", 20));
list.add(new User("userC", 30));
model.addAttribute("users", list);
}
@Data
static class User {
private String username;
private int age;
public User(String username, int age) {
this.username = username;
this.age = age;
}
}
<!-- 자바스크립트 인라인 사용 전 -->
<script>
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
</script>
타임리프에서 컨텐츠를 출력하는 [[...]] 문법을 사용한다면 자바스크립트에서도 model의 데이터를 가질 수 있다.
<script>
var username = userA; //(1) Uncaught ReferenceError: userA is not defined
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = /*userA*/ "test username"; //(2)
//객체
var user = BasicController.User(username=userA, age=10); //(3)
</script>
Uncaught ReferenceError: userA is not defined
에러가 발생한다. 이유는 userA는 문자열이므로 ' ' 또는 " "로 감싸야 하지만 타임리프의 컨텐츠 출력 문법에서는 user.username 값인 userA를 그대로 저장하려 했으므로 에러가 발생한다. 이러한 문제를 해결하기 위해서는 var username = '[[${user.username}]]';
로 선언해야 한다.위 3가지 문제를 인라인 자바스크립트를 사용함으로써 해결할 수 있다.
<!-- 자바스크립트 인라인 사용 후 -->
<script th:inline="javascript">
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
</script>
</body>
</html>
script 태그안에 th:inline="javascript"
를 추가한다.
<script>
var username = "userA"; //(1)
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = "userA"; //(2)
//객체
var user = {"username":"userA","age":10}; //(3)
</script>
var username2 = /*[[${user.username}]]*/ "test username";
코드에서 타임리프에서 렌더링시 /*[[${user.username}]]*/
부분이 "test username"
부분과 치환되어 출력된다. 이렇듯 소스파일을 그대로 열었을 경우 내추럴한 결과를 유지하지만 타임리프 렌더링시 값이 치환되어 내추럴 템플릿을 가능하게끔 한다.인라인 자바크스립트 사용에서 model로 부터 전달받은 데이터를 타임리프의 th:each를 통하여 특별한 반복을 수행할 수 있다.
<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
[# th:each="user, stat : ${users}"] //(1)
var user[[${stat.count}]] = [[${user}]]; //(2)
[/]
</script>
<script>
var user1 = {"username":"userA","age":10};
var user2 = {"username":"userB","age":20};
var user3 = {"username":"userC","age":30};
</script>
매 반복마다 동적인 변수 이름을 생성하고 각 객체들이 변수에 저장 된다.
타임리프에서는 각 페이지마자 공통되는, 예를들어 head, footer, 네비게이션바 등, HTML 태그들을 효율적으로 관리하기 위하여 템플릿 조각과 레이아웃 기능을 지원한다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<!-- 템플릿 조각으로 사용하고자 할때는 th:fragment="..." 이름을 선언 해준다. -->
<footer th:fragment="copy"> <!-- (1) -->
푸터 자리 입니다.
</footer>
<footer th:fragment="copyParam (param1, param2)"> <!-- (2) -->
<p>파라미터 자리 입니다.</p>
<p th:text="${param1}"></p>
<p th:text="${param2}"></p>
</footer>
</body>
</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>
<!-- insert: div안에 fragment name copy 삽입 -->
<div th:insert="~{template/fragment/footer :: copy}"></div> <!-- (3) -->
<h2>부분 포함 replace</h2>
<!-- replace: div와 fragment name copy와 교체, 즉 div가 사라짐 -->
<div th:replace="~{template/fragment/footer :: copy}"></div> <!-- (4) -->
<h2>부분 포함 단순 표현식</h2>
<!-- ~{...} 를 사용하는 것이 원칙이지만 템플릿 조각을 사용하는 코드가 단순하면 이 부분을 생략할 수 있다. -->
<div th:replace="template/fragment/footer :: copy"></div>
<h1>파라미터 사용</h1>
<!-- 파라미터 사용: div와 fragment name copyParam와 교체하며 파라미터 사용 가능 -->
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div> <!-- (5) -->
</body>
</html>
th:fragment="copy"
를 사용함으로써 조각 이름을 copy로 지정하였다.th:insert
는 해당 div안에 tag를 삽입하겠다는 의미이며, 삽입하려는 태그는 footer.html
에 fragment의 이름이 copy인 footer tag이다.th:replace
는 해당 div를 fragment와 교체 하겠다는 의미이며, 교체하려는 태그는 footer.html
에 fragment의 이름이 copy인 footer tag이다.'데이터1', '데이터2'
가 footer.html의 copyParam fragment 파라미터로 넘어가서 출력된다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>부분 포함</h1>
<h2>부분 포함 insert</h2>
<!-- insert: div안에 fragment name copy 삽입 -->
<div><footer>
푸터 자리 입니다.
</footer></div>
<h2>부분 포함 replace</h2>
<!-- replace: div와 fragment name copy와 교체, 즉 div가 사라짐 -->
<footer>
푸터 자리 입니다.
</footer>
<h2>부분 포함 단순 표현식</h2>
<!-- ~{...} 를 사용하는 것이 원칙이지만 템플릿 조각을 사용하는 코드가 단순하면 이 부분을 생략할 수 있다. -->
<footer>
푸터 자리 입니다.
</footer>
<h1>파라미터 사용</h1>
<!-- 파라미터 사용: div와 fragment name copyParam와 교체하며 파라미터 사용 가능 -->
<footer>
<p>파라미터 자리 입니다.</p>
<p>데이터1</p>
<p>데이터2</p>
</footer>
</body>
</html>
<head>
삽입각 페이지마다 공통된 <head>
를 사용할 수 있도록 구성 해보자.
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common_header(title,links)"> <!-- (1) -->
<!--매개변수로 전달받은 title을 replace 한다.-->
<title th:replace="${title}">레이아웃 타이틀</title> <!-- (2) -->
<!-- 공통 -->
<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>
<!-- 추가 -->
<!--매개변수로 전달받은 title를 replace 한다.-->
<th:block th:replace="${links}" /> <!-- (3) -->
</head>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- template/layout/base.html의 common_header를 replace로 사용하고 title와 link tag를 매개변수로 넘긴다. -->
<head th:replace="template/layout/base :: common_header(~{::title},~{::link})"> <!-- (4) -->
<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>
~{::title}, ~{::link}
은 title,link tag를 파라미터로 넘기겠다는 의미이다. 즉 layoutMain.html
의 head tag는 base.html
의 head tag로 교체되며, 단 title,link tag들은 파라미터로 넘겨서 base.html
의 head tag에서 (2), (3)과 교체된다.<!DOCTYPE html>
<html>
<!-- template/layout/base.html의 common_header를 replace로 사용하고 title와 link tag를 매개변수로 넘긴다. -->
<head>
<!--매개변수로 전달받은 title을 replace 한다.-->
<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>
<!-- 추가 -->
<!--매개변수로 전달받은 title를 replace 한다.-->
<link rel="stylesheet" href="/css/bootstrap.min.css"><link rel="stylesheet" href="/themes/smoothness/jquery-ui.css">
</head>
<body>
메인 컨텐츠
</body>
</html>
<html>
삽입모든 레이아웃을 가지는 html 파일을 사용하고 내부의 title과 body만 교체할 수 있도록 구성해보자.
<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org"> <!-- (1) -->
<head>
<title th:replace="${title}">레이아웃 타이틀</title> <!-- (2) -->
</head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}"> <!-- (3) -->
<p>레이아웃 컨텐츠</p>
</div>
<footer>
레이아웃 푸터
</footer>
</body>
</html>
<!DOCTYPE html>
<html th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title},~{::section})}" xmlns:th="http://www.thymeleaf.org"> <!-- (4) -->
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
</body>
</html>
~{::title}, ~{::section}
은 title,section tag를 파라미터로 넘기겠다는 의미이다. 즉 layoutExtendMain.html
의 html tag는 layoutFile.html
의 html tag로 교체되며, 단 title,section tag들은 파라미터로 넘겨서 layoutFile.html
에서 (2), (3) tag와 교체된다.<!DOCTYPE html>
<html>
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
<footer>
레이아웃 푸터
</footer>
</body>
</html>