저희 회사 제품인 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
|
<p>Today is: <span th:text="${today}">13 february 2011</span>.</p>
|
cs |
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 |
엘비스 연산자는 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 |
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 |
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 |
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 |
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">
© 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 라고 정의해야 합니다.
1
2
3
4
5
|
...
<div id="copy-section">
© 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">
© 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>
© 2011 The Good Thymes Virtual Grocery
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</body>
|
cs |
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 |
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
'유용한 정보' 카테고리의 다른 글
Tizen studio에서 Wearable기기 Web Application 만들기 (0) | 2017.08.04 |
---|---|
마이크로 소프트 윈도우의 역사 (0) | 2017.06.07 |
The State of Front-End Tooling - 2016 (0) | 2017.03.08 |
STAN - Structure Analysis for JAVA 자바 소스 visual 분석 툴 (0) | 2017.01.13 |
GitLab Issue와 Milestones개념과 사용법 (0) | 2017.01.03 |