thymeleaf (server-side template engine) 사용법 정리 - 1

 

 

저희 회사 제품인 CMS 솔루션 bizXpress Thymeleaf Template Engine 2.1.5 을 채택하여 사용하고 있습니다.

과거에는 JSP만 사용하다가 Thymeleaf 를 처음 접하면서 조금 생소하고 비교적 까다로운 사용법 때문에 적응하기 힘들었는데 지금은 모든 팀원들이 어려움 없이 잘  사용하고 있습니다.

저희들과 같이 Thymeleaf를 처음 접하는 분들에게 조금 이나마 도움이 될 수 있지 않을까 해서 자주 사용했던 기능들을 예제 위주로 정리 해보겠습니다.

Thymeleaf는 Server-side Template Engine으로 순수 HTML문서에 HTML5문법으로 Server-side 로직을 수행하고 적용시킬 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<table>
  <thead>
    <tr>
      <th th:text="#{msgs.headers.name}">Name</th>
      <th th:text="#{msgs.headers.price}">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr th:each="prod: ${allProducts}">
      <td th:text="${prod.name}">Oranges</td>
      <td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td>
    </tr>
  </tbody>
</table>
cs

위의 HTML 코드는 간단한 테이블 예제 입니다. 이 문서는 Thymeleaf Engine을 거치지 않아도 HTML 디자인에 전혀 영향을 끼치지 않습니다. 다만 데이터만 다르게 표시 될 뿐이죠.

이렇게 정적 프로토타입으로도 사용이 가능하기 때문에 개발외 다른 파트와 공동작업을 수월하게 할 수 있습니다.

표현식

1. Variable Expressions: ${...}
 
OGNL 표현식으로 해당 Context에 포함된 변수들을 사용할 수 있습니다 . JSP에서 사용했던 방식이랑 다르지 않습니다.
 
1
<p>Today is: <span th:text="${today}">13 february 2011</span>.</p>
cs
 
2. Selection Variable Expressions: *{...}

Context에 포함된 변수를 사용 하는걸로 보면 1번 표현방식과 동일하지만 가까운 DOM에 th:object로 정의된 변수가 있다면 그 변수값에 포함된 값을 나타냅니다. (해당 클래스의 property 나 map의 value 등)
이때 th:object가 정의되어 있지 않다면 1번 표현방식과 완전히 동일합니다.

Javascript 의 with 와 유사하게 동작하는것 처럼 보입니다.

1
2
3
4
5
6
<div th:object="${session.user}">
  <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>
 
cs

3. Message Expressions: #{...}

미리 정의된 message properties 파일이 존재하고 thymeleaf engine에 등록이 되었다면 #표현식으로 나타낼수 있습니다.

1
home.welcome=안녕하세요? 반갑습니다.
cs

message properties 파일에 위와 같이 정의를 했다면 아래와 같이 사용 할 수 있습니다.

1
<p th:text="#{home.welcome}">인사말</p>
cs

만약 메시지가 완전히 정적이지 않고 사용자마다 다르게 보여준다거나 할 때에는 다음과 같이 사용합니다.

1
home.welcome=안녕하세요? 반갑습니다. {0}

cs

 

1
<p th:text="#{home.welcome(${session.user.name})}">인사말(고객명)</p>
cs

위와 같이 {0},{1},{2}... 식으로 파라미터를 순서를 매겨서 정의하고 뷰 페이지에서는 함수 호출하는 방식으로 사용이 가능합니다.

4. Link URL Expressions: @{...}
@표현식을 이용하여 다음과 같이 다양하게 URL을 표현할 수 있습니다.

1
2
3
4
5
6
7
<!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html" th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>
<!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
<!-- Will produce '/gtvg/order/3/details' (plus rewriting) -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>
 
cs

 

문자 더하기

문자를 더하는 방법에는 크게 2가지 방법이 있습니다.

1
<span th:text="|Welcome to our application, ${user.name}!|">
cs

위와 같이 '|' '|' 연산자 사이에 문자와 변수 표현식을 입력하면 static 문자와 변수 문자가 이어져서 출력됩니다.

다른 방법으로는 아래와 같이 '+' 연산자를 이용해서 문자를 더할 수 있습니다.

1
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
cs

위의 두방식을 아래와 같이 혼합해서 사용할 수도 있습니다.

1
<span th:text="${onevar} + ' ' + |${twovar}, ${threevar}|">
cs

 
 
Default 연산자 (Elivs operator)

엘비스 연산자는 3항 연산자 문법의 단축형으로 Groovy언에서 사용된다고 합니다.

1
var x = f() ? f() : g()
cs

위와 같은 형식의 3항 연산자를 다음과 같이 단축해서 사용하는 방법입니다.

1
var x = f() ?: g()
cs

기본값으로 g()을 가지겠다는 의미입니다.

thymeleaf에서 는 다음과 같이 사용할 수 있습니다.

1
2
3
4
<div th:object="${session.user}">
  ...
  <p>Age: <span th:text="*{age}?: '(no age specified)'">27</span>.</p>
</div>
cs

 
속성값 설정하기
 
속성값을 설정하느 방법은 크게 2가지 방법이 있습니다. th:attr을 통해서 원하는속성을 여러개를 동시에 설정할 수 있고 th:value, th:action, th:href 등 속성을 지정해서 별도로 설정 할 수 도 있습니다.
 
1
2
<img src="../../images/gtvglogo.png" 
     th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
cs

위와 같은 방법으로 속성값을 설정하면 변수값이나 메시지값에 의해서 다음과 같이 설정이 됩니다.

1
<img src="/gtgv/images/gtvglogo.png" title="Logo de Good Thymes" alt="Logo de Good Thymes" />
cs

 
아래와 같은 속성은 각각 선택해서 값을 설정 할 수 도 있습니다.
 
th:abbr th:accept th:accept-charset
th:accesskey th:action th:align
th:alt th:archive th:audio
th:autocomplete th:axis th:background
th:bgcolor th:border th:cellpadding
th:cellspacing th:challenge th:charset
th:cite th:class th:classid
th:codebase th:codetype th:cols
th:colspan th:compact th:content
th:contenteditable th:contextmenu th:data
th:datetime th:dir th:draggable
th:dropzone th:enctype th:for
th:form th:formaction th:formenctype
th:formmethod th:formtarget th:frame
th:frameborder th:headers th:height
th:high th:href th:hreflang
th:hspace th:http-equiv th:icon
th:id th:keytype th:kind
th:label th:lang th:list
th:longdesc th:low th:manifest
th:marginheight th:marginwidth th:max
th:maxlength th:media th:method
th:min th:name th:optimum
th:pattern th:placeholder th:poster
th:preload th:radiogroup th:rel
th:rev th:rows th:rowspan
th:rules th:sandbox th:scheme
th:scope th:scrolling th:size
th:sizes th:span th:spellcheck
th:src th:srclang th:standby
th:start th:step th:style
th:summary th:tabindex th:target
th:title th:type th:usemap
th:value th:valuetype th:vspace
th:width th:wrap th:xmlbase
th:xmllang th:xmlspace
 
 
속성값 append OR prepend
 
속성값을 추가로 설정 할 수 있는데 변수 cssStyle  값이 'warning' 인 상태에서 다음과 같이 표현하면
 
1
<input type="button" value="Do it!" class="btn" th:attrappend="class=${' ' + cssStyle}" />
cs
1
<input type="button" value="Do it!" class="btn warning" />
cs

처럼 class 속성뒤에 warning이 붙어서 설정되게 됩니다. class뿐만 아니라 모든 속성을 이와 같은 방법으로 사용 할 수있으며 class는 th:classappend 를 사용하여 속성명을 생략하고 사용이 가능합니다.

 

boolean 고정값 설정

1
<input type="checkbox" name="active" th:checked="${user.active}" />
cs

위와 같은 방법으로 설정할 수 있으며 boolean 고정값을 설정할 수 있는 속성들은 다음과 같습니다.

th:async th:autofocus th:autoplay
th:checked th:controls th:declare
th:default th:defer th:disabled
th:formnovalidate th:hidden th:ismap
th:loop th:multiple th:novalidate
th:nowrap th:open th:pubdate
th:readonly th:required th:reversed
th:scoped th:seamless th:selected

 

HTML5 표준 표기법 지원

다음과 같이 th:* 방법으로 표기 하는 방법 외에 data-{prefix}-{name} 문법으로 표현이 가능합니다.

1
2
3
4
5
6
<table>
    <tr data-th-each="user : ${users}">
        <td data-th-text="${user.login}">...</td>
        <td data-th-text="${user.name}">...</td>
    </tr>
</table>
cs

 

반복문

반복 기능은 th:each 또는 data-th-each 속성으로 사용이 가능하며 반복에 사용가능한 변수 타입은 다음과 같습니다.

1. java.util.ArrayList 나 java.util.HashSet 처럼 java.util.Iterable 인터페이스를 구현한 객체

2. java.util.Map 인터페이스를 구현한 객체 (반복되는 객체는 java.util.Map.Entry가 됩니다.)

3. 모든 배열

 

아래와 같이 list형 데이터를 가지고 테이블을 표현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
<table>
      <tr>
        <th>NAME</th>
        <th>PRICE</th>
        <th>IN STOCK</th>
      </tr>
      <tr th:each="prod : ${prods}">
        <td th:text="${prod.name}">Onions</td>
        <td th:text="${prod.price}">2.41</td>
        <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
      </tr>
 </table>
cs

 
반복을 돌면서 각 index 마다 상태값을 가져올수도 있는데 iter 변수 옆에 ',' 로 추가하여 status 변수를 정의하고 사용 할 수 있습니다.
 
1
2
3
4
5
6
7
8
9
10
11
12
<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
  </tr>
  <tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
  </tr>
</table>
cs

위의 예제는 iterStat 이라느 변수로 사용할 수 있도록 정의 되어있고 odd(홀수이면) 'odd' 클래스를 적용하게 되어있습니다. odd외에 다양한 상태값을 가지는데 다음과 같습니다.

1. index - 0부터 시작하는 index

2. count- 1부터 시작하는 index

3. size - 리스트의 size

4. current - 현재 index의 변수

5. event/odd - 짝수/홀수 여부

7. first/last- 처음/마지막 여부

 

조건문

특정 조건일때만 보여지는 영역이 필요할때는 조건 속성을 통해 해당 영역을 랜더링 안할 수 있습니다.
th:if 속성을 통해 사용이 가능하며 th:if와 반대인 th:unless 도 사용이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<table>
    <tr>
        <th>NAME</th>
        <th>PRICE</th>
        <th>IN STOCK</th>
    </tr>
    <tr th:if="${#lists.size(prods)} > 0" th:each="prod,iterStat : ${prods}">
        <td th:text="${prod.name}">Onions</td>
        <td th:text="${prod.price}">2.41</td>
        <td th:text="${prod.inStock}">yes</td>
    </tr>
    <tr th:unless="${#lists.size(prods)} > 0">
        <td colspan="3">No Data.</td>
    </tr>
</table>
cs

위와 같이 prods의 size가 0인 경우에는 No Data. 문구가 포함된 TR이 노출되고 0보다 큰 경우에는 정사적으로 prods의 값들이 나타나게 됩니다.
 
th:if, th:unless외에 th:switch/th:case 속성도 다음과 같이 사용이 가능합니다.
 
1
2
3
4
5
<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>
cs

 

fragment

JSP의 include처럼 다른 templet의 특정 영역을 가져와서 나타낼 수 있습니다. 물론 현재 templet의 특정영역도 사용이 가능합니다.  홈페지의 하단에 공통적으로 들어가는 footer 영역을 별도의 html문서로 작성한뒤 /WEB-INF/templates/footer.html 명으로 저장했다고 가정 하고 해당 페이지의 특정 영역을 가져오는 방법은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
 
  <body>
  
    <div th:fragment="copy">
      &copy; 2011 The Good Thymes Virtual Grocery
    </div>
  
  </body>
  
</html>
cs

위의 html은 footer.html 입니다. 여기에서 th:fragment="copy" 라는 속성이 보입니다. 이 영역을 다른 페이지에서 사용이 가능합니다. 다음 코드는 copy라는 fragment를 include하는 예제 입니다.

1
2
3
4
5
6
7
<body>
 
  ...
 
  <div th:include="footer :: copy"></div>
  
</body>
cs

{템플릿명} :: {fragment 명} 형식으로 가져 올 수 있습니다.  이때 footer 가 탬플릿 명인데 thymeleaf 탬플릿 엔진을 초기화 할때

탬플릿 prefix를 /WEB-INF/templates/ 로 설정했고 suffix를 .html 으로 설정했기 때문에 footer라고 사용 할 수 있습니다.

만약 별도로 setPrefix, setSuffix 함수를 설정하지 않았으면 /WEB-INF/templates/footer.html :: copy 라고 정의해야 합니다.

 
th:fragment 대신 id를 세팅하고 해당 ID를 참조하여 include 할 수 있습니다.
 
1
2
3
4
5
...
<div id="copy-section">
  &copy; 2011 The Good Thymes Virtual Grocery
</div>
...
cs

위처럼 id 값이 있으면 아래와 같이 CSS 셀렉터를 이용하여 접근 할 수 있습니다.

1
2
3
4
5
6
7
<body>
 
  ...
 
  <div th:include="footer :: #copy-section"></div>
  
</body>
cs

 

th:include외에 th:replace 속성으로도 사용이 가능합니다. 둘의 차이는 다음의 코드들을 보면 바로 이해 하실 수 있습니다.

1
2
3
<footer th:fragment="copy">
  &copy; 2011 The Good Thymes Virtual Grocery
</footer>

 

 

1
2
3
4
5
6
7
8
<body>
 
  ...
 
  <div th:include="footer :: copy"></div>
  <div th:replace="footer :: copy"></div>
  
</body>
cs

위의 코드의 결과는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
<body>
 
  ...
 
  <div>
    &copy; 2011 The Good Thymes Virtual Grocery
  </div>
  <footer>
    &copy; 2011 The Good Thymes Virtual Grocery
  </footer>
  
</body>
cs

 
JSP의 include에는 parameter를 전달할 수 있죠? thymeleaf fragment도 parameter를 전달 할 수 있습니다.

parameter를 받기위해서는 fragment를 정의할때 마치 함수를 정의 하는것과 같이 정의 합니다.

1
2
3
<div th:fragment="frag (onevar,twovar)">
    <p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>
cs

위의 fragment는 문자값 2개를 onevar과 twovar 명으로 받아 '-' 연결하여 텍스트로 표시 합니다.

이 fragment를 include 하는방법은 함수를 호출하는 것처럼 사용 합니다.

1
<div th:include="::frag (${value1},${value2})">...</div>
cs

이렇게 순서에 맞춰서 value1, value2를 전달할수도 있으며 다음과 같이 순서와 상관없이 변수명=변수값 형식으로 사용 할수도 있습니다.

1
2
<div th:include="::frag (onevar=${value1},twovar=${value2})">...</div>
<div th:include="::frag (twovar=${value2},onevar=${value1})">...</div>
cs

위처럼 3가지 방식의 include 결과는 동일 합니다.

만약 다음과 같이 fragment정의에서 파라미터가 포함되어 있지 않다면 2번째 방법처럼 변수명=변수값 형식으로만 변수값을 전달하며 include가 가능합니다.

1
2
3
<div th:fragment="frag">
    <p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>
cs

위의 예제를 보면 ::frag 처럼 앞에 템플릿명이 존재하지 않는데 이건 this::frag 와 동일합니다. 현재 템프릿에서 fragment를 찾습니다.

 

assert

th:assert 속성을 사용하며 조건을 ',' 연결하여 여러 조건을 사용할 수 있습니다. 모든조건이 true가 아니면 exception을 발생 시킵니다.

1
2
3
<div th:fragment="frag">
    <p th:assert="${onevar}=='a',${twovar}=='b'" th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>
cs

위의 예제는 onevar 이 'a' 가 아니거나 twovar가 'b' 가 아니면 exception 이 떨어집니다.

 

remove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
    <td>
      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
      <a href="comments.html" 
         th:href="@{/product/comments(prodId=${prod.id})}" 
         th:unless="${#lists.isEmpty(prod.comments)}">view</a>
    </td>
  </tr>
</table>
cs

위와 같이 작성된 페이지는 thymeleaf templet engine 을 통하게 되면 전혀 문제가 되지 않습니다.
하지만 만약 thymeleaf을 처리하지않고 브라우저에서 직접 열게 된다면 테이블 header만 표시되고 body영역은 표시가 되지 않아 이것으론 현실적인 프로토타입이 될 수 없습니다.
이럴때는 하나이상의 제품정보가 나타나야 정상적으로 프로토타입 형태로 사용 할 수 있습니다.
이때 사용 할 수 있는 속성이 th:remove 입니다.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
    <td>
      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
      <a href="comments.html" 
         th:href="@{/product/comments(prodId=${prod.id})}" 
         th:unless="${#lists.isEmpty(prod.comments)}">view</a>
    </td>
  </tr>
  <tr class="odd" th:remove="all">
    <td>Blue Lettuce</td>
    <td>9.55</td>
    <td>no</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr th:remove="all">
    <td>Mild Cinnamon</td>
    <td>1.99</td>
    <td>yes</td>
    <td>
      <span>3</span> comment/s
      <a href="comments.html">view</a>
    </td>
  </tr>
</table>
cs

위의 코드를 보시면 th:remove="all" 이 되어있는 TR 2개가 보입니다. th:remove가 all이 되어있고 thymeleaf engine 을 통하게 되면 모두 제거가 됩니다. 이로써 정적인 상황에서도 완벽한 테이블의 형태로 나타낼 수 있습니다.

'all' 이외에도 4가지의 값이 더있는데 자세한 설명은 다음과 같다.

1. all - 자신을 포함한 모든 자식 노드를 제거한다.

2. body - 자신을 제외하고 모든 자식 노드를 제거한다.

3. tag - 자신은 제거하고 모든 자식은 제거하지 않는다.

4. all-but-first - 첫번째 자식 노드를 제외하고 모든 자식 노드를 제거한다.

5. none - 제거 하지 않는다. (이값은 동적인 평가(조건)를 이용해서 remove시킬때 사용 됩니다.)

1
<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>
cs

 

 

4번의 all-but-first는 왜 필요하지? 의문을 가질 수 있는데 다음 예제처럼 사용하여 th:remove="all" 속성을 중복 사용하지 않아도 됩니다..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<table>
  <thead>
    <tr>
      <th>NAME</th>
      <th>PRICE</th>
      <th>IN STOCK</th>
      <th>COMMENTS</th>
    </tr>
  </thead>
  <tbody th:remove="all-but-first">
    <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
      <td th:text="${prod.name}">Onions</td>
      <td th:text="${prod.price}">2.41</td>
      <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
      <td>
        <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
        <a href="comments.html" 
           th:href="@{/product/comments(prodId=${prod.id})}" 
           th:unless="${#lists.isEmpty(prod.comments)}">view</a>
      </td>
    </tr>
    <tr class="odd">
      <td>Blue Lettuce</td>
      <td>9.55</td>
      <td>no</td>
      <td>
        <span>0</span> comment/s
      </td>
    </tr>
    <tr>
      <td>Mild Cinnamon</td>
      <td>1.99</td>
      <td>yes</td>
      <td>
        <span>3</span> comment/s
        <a href="comments.html">view</a>
      </td>
    </tr>
  </tbody>
</table>
cs

 

이번 포스팅은 여기까지만 정리하고 다음 포스팅에서 나머지 로컬변수, 속성 우선순위,  Text inlining 등을 마저 정리 하겠습니다.

 


thymeleaf에 대한 더 자세한 내용을 살펴보고 싶다면, 아래 링크를 클릭해주세요.

▶ thymeleaf (server-side template engine) 사용법 정리 - 2


 

New Multi-Channel Dynamic CMS