웹 개발 시에 사용할 수 있는 Template Engine으로 JSP를 많이 사용하지만 직접 사용해보니 JSP보다 Thymeleaf가 훨씬 나은 듯 하다. 기본적인 문법도 간단하고 template 파일을 그대로 브라우저에서 열어봐도 문제가 없도록 template을 작성할 수 있다는 점도 내게는 큰 장점으로 보인다. 그래서 여기에 기본적인 문법을 간단한 샘플과 함께 정리해 보려고 한다.
Thymeleaf를 처음 배우는 사람에게는 아무 것도 이해가지 않을 내용이지만 새롭게 기억을 되살려 보려고 한다면 큰 도움이 될 것이라고 생각된다. 모든 내용을 정리할 수는 없지만 많이 사용하는 기능들 위주로 최대한 성의껏 작성해 보겠다. 참고로 이 글에서 사용하는 대부분의 샘플 코드는 Thymeleaf의 공식 문서에서 발췌된 것임을 밝힌다.
아래의 예제와 같이 .을 이용하여 속성값에 접근하거나 []을 이용하여 Map이나 Array의 원소에 접근하는 것들도 가능하고 객체의 메소드 호출도 가능하다.
*{...} 표현식은 부모 태그의 th:object에 지정된 객체를 기준으로 해당 객체의 속성에 접근한다. 아래 예제를 보자.
이 표현식도 역시 *{{ ... }}형태의 문법(double-brace syntax)을 지원한다. 즉, 결과 객체를 Conversion Service를 이용하여 변환시켜주므로 참고하도록 하자.
th:href, th:src, th:action 등과 같이 URL이 지정되는 속성에 사용한다. 아래 예제와 같이 여러가지 형태의 URL을 지정할 수 있다.
Thymeleaf의 Literal과 연산자들은 아래와 같다. 일반적인 개념에서 크게 벗어나는 것은 없으므로 추가 설명은 하지 않겠다.
th:attr을 이용해서 어떤 attribute든지 값을 설정할 수 있다. tag에 이미 설정되어 있는 값은 th:attr에서 지정한 값으로 대체된다.
Array나 List같은 Collection 객체를 iteration하는 간단한 예제는 아래와 같다.
참고로 위 예제에서 prod 변수 이름에 Stat을 붙여서 'prodStat'이라는 Interaion 상태를 저장하는 로컬 변수가 자동으로 하나 생성되는데 이 변수에는 다음과 같은 상태값들이 저장된다.
th:if를 이용하면 지정된 식의 결과 값에 따라 tag 전체를 없애거나 표시되도록 할 수 있다. 아래 예제의 <a> 태그는 prod.comments가 비어있지 않으면(not empty) 표시되고 그렇지 않으면 삭제된다.
완전한 html 문서가 아닌 html 문서의 일부분을 이루는 코드 조각을 fragment라고 한다. fragment를 선언하는 방법은 아래와 같다. (footer.html)
선언된 fragment를 삽입하는 방법은 3가지가 있다. th:insert, th:replace, th:include를 사용할 수 있는데 각각의 차이점은 아래 예제를 보면 알 수 있다. 먼저 아래와 같이 fragment가 선언 되어있다고 하자.
fragment는 아래와 같이 선언하여 호출하는 쪽에서 파라메터를 받을 수 있다.
브라우저로 template 파일을 열어서 볼 때는 보이지만 processing후에는 특정 태그를 삭제하고 싶다면 th:remove를 사용한다. 사용법은 아래 예제와 같다.
th:remove의 값으로 아래와 같은 값들이 지정될 수 있다.
Thymeleaf는 특정 시점에 로컬 변수를 선언해서 사용할 수 있다.로컬 변수 선언은 th:with를 사용한다.
일반적으로 모든 언어들의 연산자에는 우선순위가 있다. Thymeleaf의 속성들 간에도 우선 순위가 있다. 아래 표를 참고하여 우선순위를 확인하도록 하자.
Thymeleaf의 주석은 여러가지가 존재한다. 각각의 특성에 따라서 브라우저에서 직접 열었을 때는 주석으로 처리되고 processing 중에는 주석이 아닌 코드로 처리되도록 할 수도 있다. 각각에 대해서 살펴보도록 하자.
<!--/*/ and /*/-->
위와 반대로 브라우저에서 직접 열었을 때는 주석처리되지만 thymeleaf의 파서에서는 주석이 아닌 코드로 인식되도록 하기 위해서 사용한다. thymeleaf의 파서는 단순히 "<!--/*/", "/*/-->" 문자열을 삭제해 버린다. 따라서 그 안에 있던 모든 코드는 그대로 남겨진 채 처리가 이루어진다.
thymeleaf가 제공하는 모든 th:xxx 속성들은 element가 아니라 attribute이기 때문에 속성을 지정할 tag가 적당치 않은 경우에 곤란할 때가 있다. 이런 경우 사용할 수 있는 태그가 th:block 태그다. 아래 예제처럼 테이블의 행 2개를 반복해야 할 때 사용할 수 있을 것이다.
스프링과 함께 사용할 때 form을 이용하여 데이터를 주고받을 때는 command 객체를 이용하게 된다. 아래 예제를 보자.
th:action은 단순히 action속성을 설정하는 것 이상의 일을 한다. CSRF 공격 등을 대비한 hidden field를 자동으로 넣어주는 등 추가적인 작업이 수행되게끔 해주므로 반드시 th:action을 사용하도록 하자.
th:object에는 model attribute의 이름만 지정해야 한다. ${seedStarter.name} 같은 값은 유효하지 않다.
일반적으로 command 객체의 각 속성값들은 form의 control들과 맵핑되어 자동으로 값을 주고 받게 된다. 이를 위해서 th:field 속성을 이용한다. 아래 예제를 보자.
스프링에서 form data는 서버로 전송되어 command 객체와 binding이 이루어지며 이 때 validation이 이루어진다. binding 결과는 bindingResult 객체에 저장되는데 thymeleaf는 이 값을 이용해서 에러가 발생했는지, 어떤 에러가 발생했는지 등등을 알 수 있으며 이 때 에러 메시지를 표시하도록 할 수 있다. 이러한 에러처리를 위한 thymeleaf 기능들에 대해 알아보자.
- #fields.hasErrors('field') : field에 에러가 있다면 true, 아니면 false. 모든 필드를 대상으로 하려면 '*' 또는 'all'을 지정한다. 특정 필드가 아닌 global에러를 검사하려면 'global'을 지정한다.
- #fields.allErrors() : #fields.errors('*')와 동일.
- #fields.hasGlobalErrors() : #fields.hasErrors('global')와 동일.
- #fields.globalErrors() : #fields.errors('global')와 동일.
- #fields.detailedErrors() : 모든 에러에 대한 상세한 정보를 담고 있는 iterable 객체를 반환. 에러 객체는 fieldName(String), message(String), global(boolean) 속성을 갖는다.
처음 계획은 간단한 설명과 예제로 Thymeleaf 예제 모음 정도의 글을 쓰려고 했는데 쓰다보니 점점 일이 커져서 꽤나 고생해서 작성한 글이 되어버렸다. 사실 위에 정리된 내용 말고도 더 많은 기능들이 있지만 실제 현업에서 사용하는 기능은 이 정도면 90% 이상 커버할 수 있지 않을까 싶다. 사실 설명이 자세하진 않지만 예제가 있으므로 충분히 도움이 될 것으로 믿는다. 내가 나중에 다시 Thymeleaf로 작업을 하게되면 인터넷 검색이 선행되는 일이 없이 이 글만 읽고 바로 작업을 시작할 수 있을 것 같아 조금은 홀가분한 마음이다. Thymeleaf를 선호하는 많은 개발자들에게도 그러하기를 기대해 본다.
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값.
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를 삽입하는 방법은 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: 아무것도 삭제하지 않고 그대로 둔다.
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> <!--*/-->
<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를 선호하는 많은 개발자들에게도 그러하기를 기대해 본다.