Post

Thymeleaf 문법 정리

Thymeleaf 사용 선언

1
2
<!-- 맨 윗줄에 선언 -->
<html xmlns:th="http://www.thymeleaf.org">

텍스트 출력

1
2
3
4
5
6
7
<!-- 이스케이프가 제공되는 표현 -->
<li><span th:text="${data}"></span></li>
<li>다른 텍스트와 함께 사용 = [[${data}]]</li>

<!-- 이스케이프가 제공되지 않는 표현 -->
<li><span th:utext="${data}"></span></li>
<li><span th:inline="none">[(...)] = </span>[(${data})]</li>

th:inline="none" 은 타임리프 표현식([(...)])을 해석하지 말라는 뜻이다.

Escape 와 HTML 엔티티
Controller 에서 넘어온 데이터에는 특수문자가 포함될 수 있다. 그러한 특수문자 중 HTML 에서 사용하는 특수문자를 별도의 문자로 표현한 것을 HTML 엔티티 라고 하며, 특수문자를 HTML 엔티티로 변경하는 과정을 이스케이프(escape) 라고 한다.

  • model에 담긴 데이터 : Hello <b>Spring!</b>
  • 의도한 표현 : Hello Spring!
  • 출력 : Hello <b>Spring!</b>
  • 소스보기 : Hello &lt;b&gt;Spring!&lt;/b&gt;

여기서 &lt; 를 HTML 엔티티라고 하고 <&lt; 로 변경하는 것을 이스케이프라고 한다.

변수 표현식 : ${…}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Object -->
<li th:text="${user.username}"></li>
<li th:text="${user['username']}"></li>
<li th:text="${user.getUserName()}"></li>

<!-- List -->
<li th:text="${user[0].username}"></li>
<li th:text="${user[0]['username']}"></li>
<li th:text="${user[0].getUserName()}"></li>

<!-- Map -->
<li th:text="${user['userA'].username}"></li>
<li th:text="${user['userA']['username']}"></li>
<li th:text="${user['userA'].getUserName()}"></li>

타임리프 변수 : th:with

1
2
3
<div th:with="first=${users[0]}">
    <p>첫번째 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>

기본 객체

  • ${#request} - 3.0 버전 이상에서는 제공하지 않음
  • ${#response} - 3.0 버전 이상에서는 제공하지 않음
  • ${#session} - 3.0 버전 이상에서는 제공하지 않음
  • ${#servletContext} - 3.0 버전 이상에서는 제공하지 않음
  • ${#locale}
  • ${param} - request 파라미터 접근 (Controller 에 @RequestParam 을 해주지 않아도 타임리프가 쿼리스트링을 param 객체에 담아서 넘겨준다.)
  • ${session} - 세션 접근
  • ${@helloBean.hello('Spring!')} - 스프링 빈 접근

3.0 이상에서 제공하지 않는 객체는 직접 model 에 넣어줘야 한다.

유틸리티 객체

필요할 때 찾아보고 사용하기. 링크 참조

Java 8 날짜 : ${#temporals}

1
2
3
4
5
6
7
8
9
10
11
12
13
<li>yyyy-MM-dd HH:mm:ss = <span th:text="${#temporals.format(localDateTime,'yyyy-MM-dd HH:mm:ss')}"></span></li>
<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>

URL 링크 : @{...}

1
2
3
4
<li><a th:href="@{/hello}">basic url</a></li>
<li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">query param</a></li>
<li><a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>
<li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>

리터럴과 리터럴 대체

타임리프에서 문자는 항상 ' (작은 따옴표) 로 감싸야 한다.

하지만 룰로 연결된 문자라면 작은 따옴표를 생략할 수 있다. (룰 : A-Z , a-z , 0-9 , [] , . , - , _ )

혹은 리터럴 대체를 사용하면 된다.

1
2
3
4
5
6
7
<!-- 주석을 풀면 예외가 발생함
<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>

연산

  • 산술연산

    1
    2
    
      <li>10 + 2 = <span th:text="10 + 2"></span></li>
      <li>10 % 2 = <span th:text="10 % 2"></span></li>
    
  • 비교연산

    1
    2
    3
    
      <li>1 gt 10 = <span th:text="1 gt 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>
    
  • 조건식

    1
    
      <li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)?'짝수':'홀수'"></span></li>
    
  • Elvis 연산자

    1
    
      <li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?: '데이터가 없습니다.'"></span></li>
    
  • No-Operation(_)

    1
    2
    3
    4
    5
    
      <li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가 없습니다.</span></li>
      <!--
      출력
      ${nullData}?: _ = 데이터가 없습니다.
      -->
    

    No-Operation 은 데이터가 null 이면 타임리프 속성을 무시한다.

속성 설정 : th:*

타임리프는 th:* 로 속성을 지정하면 기본 HTML 속성을 대체한다. 이 때, 기존 속성이 없다면 새로 만든다.

1
2
3
4
5
6
7
8
<!-- 기존 속성 뒤에 추가 -->
<input type="text" class="A" th:attrappend="class=' large'"/>
<!-- 기존 속성 앞에 추가 -->
<input type="text" class="A" th:attrprepend="class='large '"/>
<!-- class 추가 -->
<input type="text" class="A" th:classappend="large"/>
<!-- checked 처리 -->
<input type="checkbox" name="active" th:checked="true"/>

반복 : th:each

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<tr th:each="user, userStat : ${users}">
    <td th:text="${user.username}"></td>
    <td th:text="${user.age}"></td>
    <td>
        <span th:text="${userStat.index}"></span>  <!-- 0부터 시작하는 값 -->
        <span th:text="${userStat.count}"></span>  <!-- 1부터 시작하는 값 -->
        <span th:text="${userStat.size}"></span>   <!-- 전체 사이즈 -->
        <span th:text="${userStat.even}"></span>   <!-- 홀수 여부 (boolean) -->
        <span th:text="${userStat.odd}"></span>    <!-- 짝수 여부 (boolean) -->
        <span th:text="${userStat.first}"></span>  <!-- 처음 여부 (boolean) -->
        <span th:text="${userStat.last}"></span>   <!-- 마지막 여부 (boolean) -->
        <span th:text="${userStat.current}"></span><!-- 현재 객체 -->
    </td>
</tr>

반복상태변수(userStat) 명이 반복변수(user) + Stat 와 같으면 반복상태변수(userStat) 는 생략할 수 있고, ...+Stat 로 사용하고 싶지 않다면 새롭게 정의하면 된다.

조건문 : th:if , th:unless , th:switch

1
2
3
4
5
6
7
8
9
<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>

th:if 는 조건이 참일 때 태그를 렌더링하고, th:unless 는 조건이 거짓일 때 태그를 렌더링한다.

1
2
3
4
5
6
7
8
9
<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>

주석

1
2
3
4
5
6
7
8
9
10
11
12
<p>타임리프 한줄 주석</p>
<!--/* <span>[[${data}]]</span> */-->

<p>타임리프 여러줄 주석</p>
<!--/*-->
<span th:text="${data}">data</span>
<!--*/-->

<p>타임리프 프로토타입 주석</p>
<!--/*/
<span th=text="${data}">data</span>
/*/-->

프로토타입 주석의 경우 HTML 파일로 보면 주석이지만 서버에서 렌더링 시 정상 출력되는 특이한 주석이다.

블록 : <th:block>

속성이 아닌 태그임에 주의

1
2
3
4
5
6
7
8
9
<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> 태그안에 포함된 것들을 하나로 묶어주는 역할을 한다. 조건, 반복 시 태그안에 포함된 것들이 함께 처리된다.

자바스크립트 인라인

실행 전

1
2
3
4
5
6
7
8
<script th:inline="javascript">
    var username = [[${user.username}]];
    var age = [[${user.age}]];
    //자바스크립트 내추럴 템플릿
    var username2 = /*[[${user.username}]]*/ "test username";
    //객체
    var user = [[${user}]];
</script>

실행 후

1
2
3
4
5
6
7
8
<script>
    var username = "userA";
    var age = 10;
    //자바스크립트 내추럴 템플릿
    var username2 = "userA";
    //객체
    var user = {"username":"userA","age":10};
</script>

JSP 처럼 ${…}' 를 붙여줄 필요가 없으며, 객체는 자동으로 object 로 변환해준다.

실행 전

1
2
3
4
5
6
<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
    [# th:each="user, stat : ${users}"]
    var user[[${stat.count}]] = [[${user}]]
    [/]
</script>

실행 후

1
2
3
4
5
6
7
8
<!-- 자바스크립트 인라인 each -->
<script>
    
    var user1 = {"username":"userA","age":10}
    var user2 = {"username":"userB","age":20}
    var user3 = {"username":"userC","age":30}
    
</script>

each 를 통해 반복처리도 가능하다.

템플릿 조각 : th:fragment , ~{…}

템플릿 조각은 다른 html 파일에서 사용하기 위한 컴포넌트 같은 역할을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<!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>

th:fragment 가 적용된 태그는 다른 html 에서 호출해서 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>
  • th:insert : 적용된 태그의 자식에 fragment 를 적용.
  • th:replace : 적용된 태그를 fragment 로 대체.

예제1

1
2
3
4
5
6
7
8
9
10
11
12
<head th:fragment="common_header(title, links)">
    <!-- title replace -->
    <title th:replace="${title}">Title</title>

    <!-- common area -->
    <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomapp.css}">
    <link rel="shortcut icon" th:href="@{/images/favicon.ico}">
    <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>

    <!-- custom area -->
    <th:block th:replace="${links}"/>
</head>
1
2
3
4
5
6
7
8
<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>
main contents
</body>

예제2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" 
      th:fragment="layout(title, content)">
<head>
    <title th:replace="${title}">Title</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}">
    <p>레이아웃 컨텐츠</p>
</div>
<footer>
    레이아웃 푸터
</footer>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      th:replace="template/layoutExtend/layoutFile::layout(~{::title},~{::section})">
<head>
    <title>Title</title>
</head>
<body>
<section>
    <p>메인 페이지 콘텐츠</p>
    <div>메인 페이지 포함 내용</div>
</section>
</body>
</html>

타임리프 레이아웃 적용 시 보통 위 예제들처럼 fragment 를 가져올 때 내가 원하는 태그만 넘겨 레이아웃을 적용한다.

This post is licensed under CC BY 4.0 by the author.