웹 개발 시에 사용할 수 있는 Template Engine으로 JSP를 많이 사용하지만 직접 사용해보니 JSP보다 Thymeleaf가 훨씬 나은 듯 하다. 기본적인 문법도 간단하고 template 파일을 그대로 브라우저에서 열어봐도 문제가 없도록 template을 작성할 수 있다는 점도 내게는 큰 장점으로 보인다. 그래서 여기에 기본적인 문법을 간단한 샘플과 함께 정리해 보려고 한다.
Thymeleaf를 처음 배우는 사람에게는 아무 것도 이해가지 않을 내용이지만 새롭게 기억을 되살려 보려고 한다면 큰 도움이 될 것이라고 생각된다. 모든 내용을 정리할 수는 없지만 많이 사용하는 기능들 위주로 최대한 성의껏 작성해 보겠다. 참고로 이 글에서 사용하는 대부분의 샘플 코드는 Thymeleaf의 공식 문서에서 발췌된 것임을 밝힌다.
1. Message Expression: #{ ... }
...
<p th:text="#{home.welcome}">Welcome to our grocery store!</p>
...
message source로부터 home.welcome 키에 지정된 메시지를 가져와서 해당 tag의 text를 대체한다. th:text는 태그의 text를 지정된 값으로 대체하는 기능을 한다. 예를 들어 messages.properties에 아래와 같은 값이 있다면
...
home.welcome=¡Bienvenido a nuestra tienda de comestibles!
...
아래와 같은 결과를 얻게될 것이다.
...
<p>¡Bienvenido a nuestra tienda de comestibles!</p>
...
만약 문자열의 내용을 escape처리하고 싶지 않다면
th:utext를 사용한다. '<' 기호가 "<" 로 변환되지 않고 그대로 남아있게 된다.
...
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
...
message 문자열이 아래와 같이 파라메터를 받도록 되어있다면
...
home.welcome=¡Bienvenido a nuestra tienda de comestibles, {0}!
...
아래와 같이 괄호를 넣어서 파라메터를 넘길 수 있다.
...
<p th:utext="#{home.welcome(${session.user.name})}">
Welcome to our grocery store, Sebastian Pepper!
</p>
...
2. Variable Expression: ${ ... }
...
<p>Today is: <span th:text="${today}">13 february 2011</span>.</p>
...
모델에 저장된
today 속성값을 문자열로 변환(toString())하여 span tag의 text값을 대체한다.
아래의 예제와 같이
.을 이용하여 속성값에 접근하거나
[]을 이용하여 Map이나 Array의 원소에 접근하는 것들도 가능하고 객체의
메소드 호출도 가능하다.
${person.father.name}
${person['father']['name']}
${countriesByCode.ES}
${personsByName['Stephen Zucchini'].age}
${personsArray[0].name}
${person.createCompleteName()}
${person.createCompleteNameWithSeparator('-')}
참고로
${{ ... }}형태의 문법(double-brace syntax)이 있는데 이 문법은 최종적으로 산출된 결과 객체를 등록된 Conversion Service를 이용해서 formatting하도록 한다. 스프링과 함께 사용할 때는 스프링의 Conversion Service를 사용하므로 스프링에 익숙하다면 상당히 편리한 기능이다.
3. Selection Variable Expression: *{ ... }
*{...} 표현식은 부모 태그의
th:object에 지정된 객체를 기준으로 해당 객체의 속성에 접근한다. 아래 예제를 보자.
...
<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>
...
위의 코드는 아래 코드와 정확히 동일하다.
...
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>
...
지정된 객체(th:object)가 없는 경우 ${...}와 *{...}는 완전히 동일한 기능을 하며 지정된 객체가 있는 경우에도 ${ ... } 표현식을 섞어서 사용하는 데 문제가 없다.
이 표현식도 역시
*{{ ... }}형태의 문법(double-brace syntax)을 지원한다. 즉, 결과 객체를 Conversion Service를 이용하여 변환시켜주므로 참고하도록 하자.
4. Link URL Expression: @{ ... }
th:href,
th:src,
th:action 등과 같이 URL이 지정되는 속성에 사용한다. 아래 예제와 같이 여러가지 형태의 URL을 지정할 수 있다.
<!-- Page-relative URL. 일반적인 상대 경로 URL -->
<a href="login.html" th:href="@{user/login.html}">login</a>
<!-- Context-relative URL. context name이 URL 앞에 자동으로 붙어서 생성된다. -->
<a href="login.html" th:href="@{/login.html}">login</a>
<!-- Server-relative URL. 서버의 다른 context에 접근할 수 있다. -->
<a href="login.html" th:href="@{~/other/login.html}">login</a>
<!-- Protocol-relative URL. -->
<a href="login.html" th:href="@{//www.other.com/login.html}">login</a>
괄호를 사용해서 파라메터를 넘겨서 URL 파라메터를 지정하거나 URL path를 구성하는데 사용할 수 있다.
<!-- '/gtvg/order/details?orderId=3'을 생성. URL 파라메터로 사용됨. -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
<!-- '/gtvg/order/3/details'를 생성. orderId를 local변수로 사용하여 URL path를 생성. -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>
5. Literals & Operators
Thymeleaf의 Literal과 연산자들은 아래와 같다. 일반적인 개념에서 크게 벗어나는 것은 없으므로 추가 설명은 하지 않겠다.
- Literals
- Text literals: 'one text', 'Another one!',…
- Number literals: 0, 34, 3.0, 12.3,…
- Boolean literals: true, false
- Null literal: null
- Literal tokens: one, sometext, main,…
- Text operations
- String concatenation: +
- Literal substitutions: |The name is ${name}|
- Arithmetic operations
- Binary operators: +, -, *, /, %
- Minus sign (unary operator): -
- Boolean operations
- Binary operators: and, or
- Boolean negation (unary operator): !, not
- Comparisons and equality
- Comparators: >, <, >=, <= (gt, lt, ge, le)
- Equality operators: ==, != (eq, ne)
- Conditional operators
- If-then: (if) ? (then)
- If-then-else: (if) ? (then) : (else)
- Default: (value) ?: (defaultvalue)
6. Attribute 값 설정하기
th:attr을 이용해서 어떤 attribute든지 값을 설정할 수 있다. tag에 이미 설정되어 있는 값은 th:attr에서 지정한 값으로 대체된다.
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe me!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
위 예제는 Processing후에 아래와 같이 될 것이다.
<form action="/gtvg/subscribe">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="¡Suscríbeme!"/>
</fieldset>
</form>
하지만 html의 각 attribute에 대해서는 Thymeleaf에도 일대일 대응되는 th:xxx 형태의 속성이 있기 때문에 th:attr은 거의 사용하지 않는다. 이 경우 아래 예제와 같이 값을 지정한다. th:attr과 달리 "name=" 부분은 따로 지정할 필요가 없기때문에 훨씬 간단하게 표현된다.
<form action="subscribe.html" th:action="@{/subscribe}">
checkbox 타입 버튼의 checked 속성이 check 상태를 표시하는데 이와 같이 boolean값의 역할을 하는 속성의 경우 값이 없이 속성만 지정되면 true의 역할을 하게된다. (값이 "checked"로 설정되어도 된다.) 이런 속성들은 th:checked 속성에 지정된 조건을 평가하여 true로 판단되는 경우 checked 속성이 남게되고 false인 경우 checked 속성자체가 사라지게 된다.
<input type="checkbox" name="active" th:checked="${user.active}" />
위 예제에서 ${user.active}가 true냐 false냐에 따라 각각 아래와 같이 생성될 것이다.
<input type="checkbox" name="active" checked="checked" />
<input type="checkbox" name="active" />
7. Iteration
Array나 List같은 Collection 객체를 iteration하는 간단한 예제는 아래와 같다.
...
<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>
...
th:each를 이용해서 원소의 갯수만큼 반복할 수 있으며 Iterable 구현 객체, Map 구현 객체 (이 경우 java.util.Map.Entry객체), Array 등의 객체를 사용할 수 있다.
참고로 위 예제에서 prod 변수 이름에 Stat을 붙여서 'prodStat'이라는 Interaion 상태를 저장하는 로컬 변수가 자동으로 하나 생성되는데 이 변수에는 다음과 같은 상태값들이 저장된다.
- prodStat.index: 현재 index. (0부터 시작)
- prodStat.count: 현재 index. (1부터 시작)
- prodStat.size: 전체 원소 갯수
- prodStat.current: 현재 원소 객체. (prod와 같음)
- prodStat.even/odd: 현재 iteration이 짝수/홀수 인지를 나타내는 boolean값.
- prodStat.first: 현재 iteration이 첫번째 인지를 나타내는 boolean값.
- prodStat.last: 현재 iteration이 마지막 인지를 나타내는 boolean값.
'prodStat' 변수의 이름을 다른 이름으로 지정하려면 <tr th:each="prod, iterStat : ${prods}">와 같이 prod 뒤에 comma를 찍고 이름을 써주면 된다.
8. Conditional Evaluation
th:if를 이용하면 지정된 식의 결과 값에 따라 tag 전체를 없애거나 표시되도록 할 수 있다. 아래 예제의 <a> 태그는 prod.comments가 비어있지 않으면(not empty) 표시되고 그렇지 않으면 삭제된다.
...
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
...
th:if와 반대로 평가하는
th:unless도 있다. th:if의 결과식에 not을 적용한 것과 같다. 아래 예제는 위 예제와 동일하게 동작한다. (위의 th:if에는 not이 이미 있으므로 그것을 없앤 것과 같다)
...
<a href="comments.html"
th:href="@{/comments(prodId=${prod.id})}"
th:unless="${#lists.isEmpty(prod.comments)}">view</a>
...
9. Template Layout
완전한 html 문서가 아닌 html 문서의 일부분을 이루는 코드 조각을 fragment라고 한다. fragment를 선언하는 방법은 아래와 같다. (footer.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
</html>
위 예제는 footer.html 파일에 copy fragment를 선언한 것이다. fragment는
th:fragment를 이용하여 선언한다. 이렇게 선언된 fragment는 아래 예제와 같이 다른 문서의 일부분으로 삽입될 수 있다.
<body>
...
<div th:insert="~{footer :: copy}"></div>
</body>
<div>태그에 footer.html에 선언된 copy fragment를 삽입한다는 뜻이다.
~{ ... } 를 fragment expression이라고 하는데 아래 형태로 사용될 수 있다.
- "~{ templatename :: selector }"
template 파일의 이름과 그 안에 선언된 Markup selector를 참조한다. 사실 Markup selector는 CSS selector와 비슷한 문법이 있지만 fragment를 참조하는 경우에는 fragment 이름만 써주면 된다. 대부분의 경우 이렇게 사용할 것이다. CSS selector와 비슷하기 때문에 <div id="myid" ... > 형태로 선언된 태그를 ~{ template :: #myid } 형태로 참조할 수 있다.
- "~{ templatename }"
전체 template 파일을 참조한다.
- "~{ ::selector }" or "~{ this :: selector }"
같은 파일 내에 있는 selector를 참조한다는 점을 제외하고 1번과 같다. 마찬가지로 selector로는 fragment 이름을 써주면 된다.
공식적인 문법은 위와 같지만 단순한 fragment 참조의 경우에는 ~{ }를 생략할 수 있다. 아래 예제들은 대부분 ~{ }를 생략한 형태를 사용하고 있으므로 참고하도록 하자.
선언된 fragment를 삽입하는 방법은 3가지가 있다.
th:insert,
th:replace,
th:include를 사용할 수 있는데 각각의 차이점은 아래 예제를 보면 알 수 있다. 먼저 아래와 같이 fragment가 선언 되어있다고 하자.
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
아래와 같이 각각의 방법으로 fragment를 삽입하면
<body>
...
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
</body>
아래와 같은 결과가 생성된다.
<body>
...
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
참고로 문서에서는 th:insert와 th:replace를 사용토록 권장하고 있다.
fragment는 아래와 같이 선언하여 호출하는 쪽에서 파라메터를 받을 수 있다.
<div th:fragment="frag (onevar,twovar)">
<p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>
이렇게 선언된 fragment를 호출할 때는 아래와 같이 파라메터를 넣어서 호출하면 된다.
<div th:replace="::frag (${value1},${value2})">...</div>
<div th:replace="::frag (onevar=${value1},twovar=${value2})">...</div>
만일 위 예제의 두번째 방법으로 호출할 때 fragment에는 선언된 파라메터가 없다면 두 파라메터는 단순히 로컬변수가 되어 참조가 될 수 있다. 즉 아래와 같은 호출과 동일한 호출이 된다.
<div th:replace="::frag" th:with="onevar=${value1},twovar=${value2}">
10. Removing Template
브라우저로 template 파일을 열어서 볼 때는 보이지만 processing후에는 특정 태그를 삭제하고 싶다면
th:remove를 사용한다. 사용법은 아래 예제와 같다.
<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>
</table>
template을 브라우저로 직접 열어보면 2개의 행이 보이겠지만 thymeleaf engine이 처리 후에는 prods의 원소 갯수만큼의 행이 생성되고 마지막 행은 삭제되어 보이지 않게될 것이다.
th:remove의 값으로 아래와 같은 값들이 지정될 수 있다.
- all: th:remove가 선언된 태그와 그 자식 태그들도 모두 삭제한다.
- body: th:remove가 선언된 태그는 그대로 두고 자식 태그들만 삭제한다.
- tag: th:remove가 선언된 태그만 삭제하고 자식 태그들은 그대로 둔다.
- all-but-first: 첫 번째 자식 태그만 두고 나머지 자식 태그는 모두 삭제한다.
- none: 아무것도 삭제하지 않고 그대로 둔다.
마지막 none의 경우는 th:remove의 값으로 조건 표현식을 사용하여 조건에 따라 삭제를 할지 말지 결정할 때 사용될 수 있다.
11. Local Variables
Thymeleaf는 특정 시점에 로컬 변수를 선언해서 사용할 수 있다.로컬 변수 선언은
th:with를 사용한다.
<div th:with="firstPer=${persons[0]},secondPer=${persons[1]}">
<p>
The name of the first person is <span th:text="${firstPer.name}">Julius Caesar</span>.
</p>
<p>
But the name of the second person is
<span th:text="${secondPer.name}">Marcus Antonius</span>.
</p>
</div>
로컬 변수는 위와 같이 한 번에 2개 이상을 선언할 수도 있다. 로컬 변수는 자식 태그뿐 아니라 같은 태그 안에서도 사용할 수 있으며 심지어 th:with 선언 안에서도 첫 번째 변수를 두 번째 변수에서 사용해도 문제가 없다.
12. Attribute Precedence
일반적으로 모든 언어들의 연산자에는 우선순위가 있다. Thymeleaf의 속성들 간에도 우선 순위가 있다. 아래 표를 참고하여 우선순위를 확인하도록 하자.
Order | Feature | Attributes |
1 | Fragment inclusion | th:insert
th:replace |
2 | Fragment iteration | th:each |
3 | Conditional evaluation | th:if
th:unless
th:switch
th:case
|
4 | Local variable definition | th:object
th:with |
5 | General attribute modification | th:attr
th:attrprepend
th:attrappend |
6 | Specific attribute modification | th:value
th:href
th:src
... |
7 | Text (tag body modification) | th:text
th:utext |
8 | Fragment specification | th:fragment |
9 | Fragment removal | th:remove |
13. Comments and Blocks
Thymeleaf의 주석은 여러가지가 존재한다. 각각의 특성에 따라서 브라우저에서 직접 열었을 때는 주석으로 처리되고 processing 중에는 주석이 아닌 코드로 처리되도록 할 수도 있다. 각각에 대해서 살펴보도록 하자.
- <!-- and -->
일반적인 HTML의 주석이다. 이 주석은 브라우저에서나 thymeleaf에서나 무시된다. 하지만 processing후 삭제되는 것은 아니고 생성된 결과파일에도 그대로 복사되어 표시된다.
- <!--/* and */-->
thymeleaf의 파서에서 주석으로 처리된다. 즉 파서가 template로부터 이 부분을 삭제하고 processing을 하게된다. 위의 HTML 주석과 별 차이가 없어보이지만 특정 부분을 브라우저에서 직접 열었을 때는 보이지만 thymeleaf가 처리중일 때는 보이지 않도록 할 수 있다. 아래 예제를 보면 어떻게 그렇게 할 수 있는지 알 수 있을 것이다..
<!--/*-->
<div> ...
</div>
<!--*/-->
<!--/*/ and /*/-->
위와 반대로 브라우저에서 직접 열었을 때는 주석처리되지만 thymeleaf의 파서에서는 주석이 아닌 코드로 인식되도록 하기 위해서 사용한다. thymeleaf의 파서는 단순히 "<!--/*/", "/*/-->" 문자열을 삭제해 버린다. 따라서 그 안에 있던 모든 코드는 그대로 남겨진 채 처리가 이루어진다.
thymeleaf가 제공하는 모든 th:xxx 속성들은 element가 아니라 attribute이기 때문에 속성을 지정할 tag가 적당치 않은 경우에 곤란할 때가 있다. 이런 경우 사용할 수 있는 태그가
th:block 태그다. 아래 예제처럼 테이블의 행 2개를 반복해야 할 때 사용할 수 있을 것이다.
<table>
<th:block th:each="user : ${users}">
<tr>
<td th:text="${user.login}">...</td>
<td th:text="${user.name}">...</td>
</tr>
<tr>
<td colspan="2" th:text="${user.address}">...</td>
</tr>
</th:block>
</table>
하지만 th:block 태그는 브라우저에서는 인식하지 못하는 태그이다. 결국 template 파일을 브라우저에서 열었을 때도 그럴 듯하게 보이도록 하려는 원래 의도가 망가질 수 있다. 하지만 바로 앞에서 설명했던 브라우저에서만 주석이 되도록하는 3번째 주석(<!--/*/ ... /*/-->)으로 th:block 태그 부분만 각각 감싸게되면 브라우저에서는 th:block 태그는 안보이게 되므로 원래 의도가 유지될 수 있다.
14. Spring Form
스프링과 함께 사용할 때 form을 이용하여 데이터를 주고받을 때는 command 객체를 이용하게 된다. 아래 예제를 보자.
<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post">
...
</form>
위 예제는 전형적인 form태그의 예이다. action속성은 반드시 th:action을 사용하도록 하고 th:object에 command 객체를 지정한다.
th:action은 단순히 action속성을 설정하는 것 이상의 일을 한다. CSRF 공격 등을 대비한 hidden field를 자동으로 넣어주는 등 추가적인 작업이 수행되게끔 해주므로 반드시 th:action을 사용하도록 하자.
th:object에는 model attribute의 이름만 지정해야 한다. ${seedStarter.name} 같은 값은 유효하지 않다.
일반적으로 command 객체의 각 속성값들은 form의 control들과 맵핑되어 자동으로 값을 주고 받게 된다. 이를 위해서
th:field 속성을 이용한다. 아래 예제를 보자.
<input type="text" th:field="*{datePlanted}" />
th:field가 선언된 위 예제는 아래 예제와 비슷하다고 할 수 있다.
<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}" />
th:field는 input type에 맞게 적절히 필요한 속성을 알아서 처리해주며 필드의 객체를 표시하는데 Conversion이 필요하다면 알아서 Conversion Service를 이용하여 변환작업도 수행해준다.
15. Validation & Error Messages (String)
스프링에서 form data는 서버로 전송되어 command 객체와 binding이 이루어지며 이 때 validation이 이루어진다. binding 결과는 bindingResult 객체에 저장되는데 thymeleaf는 이 값을 이용해서 에러가 발생했는지, 어떤 에러가 발생했는지 등등을 알 수 있으며 이 때 에러 메시지를 표시하도록 할 수 있다. 이러한 에러처리를 위한 thymeleaf 기능들에 대해 알아보자.
-
#fields.hasErrors('field') : field에 에러가 있다면 true, 아니면 false. 모든 필드를 대상으로 하려면 '*' 또는 'all'을 지정한다. 특정 필드가 아닌 global에러를 검사하려면 'global'을 지정한다.
<input type="text" th:field="*{datePlanted}"
th:class="${#fields.hasErrors('datePlanted')}? fieldError" />
-
#fields.errors('field') : field의 모든 에러 메시지를 iteration할 수 있는 iterable 객체 반환. 모든 필드를 대상으로 하려면 '*' 또는 'all'을 지정한다. 특정 필드가 아닌 global 에러 메시지를 가져오려면 'global'을 지정한다.
<ul>
<li th:each="err : ${#fields.errors('datePlanted')}" th:text="${err}" />
</ul>
-
th:errors : 지정된 필드의 모든 에러 메시지를 <br /> 태그로 분리된 문자열로 반환. global 에러 메시지를 가져오려면 *{global}을 지정한다.
<input type="text" th:field="*{datePlanted}" />
<p th:if="${#fields.hasErrors('datePlanted')}" th:errors="*{datePlanted}">Incorrect date</p>
-
th:errorclass : th:field에 지정된 필드에 에러가 있는 경우 class 속성에 지정된 값을 추가해 준다. 아래 예제에서 에러가 있다면 class는 "small fieldError"가 될 것이다.
<input type="text" th:field="*{datePlanted}" class="small" th:errorclass="fieldError" />
-
#fields.hasAnyErrors() : #fields.hasErrors('*')와 동일.
-
#fields.allErrors() : #fields.errors('*')와 동일.
-
#fields.hasGlobalErrors() : #fields.hasErrors('global')와 동일.
-
#fields.globalErrors() : #fields.errors('global')와 동일.
-
#fields.detailedErrors() : 모든 에러에 대한 상세한 정보를 담고 있는 iterable 객체를 반환. 에러 객체는 fieldName(String), message(String), global(boolean) 속성을 갖는다.
<ul>
<li th:each="e : ${#fields.detailedErrors()}" th:class="${e.global}? globalerr : fielderr">
<span th:text="${e.global}? '*' : ${e.fieldName}">The field name</span> |
<span th:text="${e.message}">The error message</span>
</li>
</ul>
마무리...
처음 계획은 간단한 설명과 예제로 Thymeleaf 예제 모음 정도의 글을 쓰려고 했는데 쓰다보니 점점 일이 커져서 꽤나 고생해서 작성한 글이 되어버렸다. 사실 위에 정리된 내용 말고도 더 많은 기능들이 있지만 실제 현업에서 사용하는 기능은 이 정도면 90% 이상 커버할 수 있지 않을까 싶다. 사실 설명이 자세하진 않지만 예제가 있으므로 충분히 도움이 될 것으로 믿는다. 내가 나중에 다시 Thymeleaf로 작업을 하게되면 인터넷 검색이 선행되는 일이 없이 이 글만 읽고 바로 작업을 시작할 수 있을 것 같아 조금은 홀가분한 마음이다. Thymeleaf를 선호하는 많은 개발자들에게도 그러하기를 기대해 본다.