본문 바로가기
Web/spring study

[Thymeleaf] Thymeleaf(타임리프)의 특징과 기본 표현식, 태그 속성

by 장인이 2023. 6. 7.

1. 개요

 이번 게시물에서는 Thymeleaf의 특징, 기본 표현식, 태그 속성들에 대해 정리해볼 것이다. 해당 게시물에서는 타임리프만의 특징들에 대해서 정리해볼 예정이며, 다음 게시물에서는 spring과 같이 사용할 경우 활용할 수 있는 다양한 기능들을 소개할 것이다. spring에서 많이 사용하는 뷰 템플릿이므로, spring study 카테고리에서 작성하게 되었다.

 

 

목차
Thymeleaf의 특징
기본 표현식
태그 속성 추가

 

 

2. Thymeleaf의 특징

1) 서버 사이드 HTML 렌더링 (SSR)

 타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링한다. 이에 관해서는 예전에 작성한 글을 첨부한다.

서버/클라이언트 템플릿 엔진

 

2) 네츄럴 템플릿

 타임리프의 장점 중 하나이다. 이는 순수 HTML을 유지하면서, 뷰 템플릿도 사용할 수 있는 것을 말한다. 이게 무슨 뜻일까? 다른 뷰 템플릿과 비교해보자.

 

 JSP와 같은 뷰 템플릿은 파일을 그대로 웹 브라우저에서 열면, JSP 코드와 HTML이 뒤섞이게 된다. 결국, 정상적인 HTML 결과를 확인할 수 없다. 즉, 서버를 통해 JSP가 렌더링되어야만, 화면을 확인할 수 있다.

 이에 반해, 타임리프로 작성된 파일을 그대로 웹 브라우저에서 열면, 정상적인 HTML 결과를 확인할 수 있다. 물론 동적으로 렌더링되는 것은 아니지만, 적어도 HTML 결과가 어떻게 되는지는 확인할 수 있다!

 

3) 스프링 통합 지원

 타임리프는 마치 스프링을 위해 만들어진 것처럼, 스프링의 다양한 기능들을 편리하게 사용할 수 있게 지원해준다. 이에 대한 내용은 다음 게시물에서 작성해볼 예정이다.

 

4) Thymeleaf를 사용하려면

 타임리프를 사용하려면, 아래와 같이 선언하면 된다.

<html xmlns:th="http://www.thymeleaf.org">

 

 

3. 기본 표현식

 타임리프에서는 다양한 기본 표현식을 제공해준다. 더 많은 내용은 아래 링크를 참고 바란다.

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax

 

 

1) 변수 표현식 ${ ... }

 타임리프에서 변수를 사용할 때는, ${ ... }이라는 변수 표현식을 사용한다. 이는 스프링 EL이라는 스프링이 제공하는 표현식을 사용할 수 있다.

 Model에 User 객체, User 객체가 저장되어 있는 List 과 Map을 담았다고 생각하고, 다음 예제를 확인해보자. th:text와 같은 속성은 아래에서 설명할 것이며, HTML 콘텐츠의 내용을 바꾸는 것이라고 생각하면 된다.

 

<ul>Object
    <li>${user.username} = <span th:text="${user.username}"></span></li>
    <li>${user['username']} = <span th:text="${user['username']}"></span></li>
    <li>${user.getUsername()} = <span th:text="${user.getUsername()}"></span></li>
</ul>

<ul>List
    <li>${users[0].username} = <span th:text="${users[0].username}"></span></li>
    <li>${users[0]['username']} = <span th:text="${users[0]['username']}"></span></li>
    <li>${users[0].getUsername()} = <span th:text="${users[0].getUsername()}"></span></li>
</ul>

<ul>Map
     <li>${userMap['userA'].username} = <span th:text="${userMap['userA'].username}"></span></li>
     <li>${userMap['userA']['username']} = <span th:text="${userMap['userA']['username']}"></span></li>
     <li>${userMap['userA'].getUsername()} = <span th:text="${userMap['userA'].getUsername()}"></span></li>
</ul>

- Object의 user.username, user['username'], user.getUsername() 모두 내부적으로 user.getUsername()을 호출한다.

List의 세가지 방법 또한 내부적으로 users[0].getUsername()을 호출한다.

Map 또한 내부적으로 userMap.get('userA').getUsername()을 호출한다.

 

Object
- ${user.username} = userA
- ${user['username']} = userA
- ${user.getUsername()} = userA

List
- ${users[0].username} = userA
- ${users[0]['username']} = userA
- ${users[0].getUsername()} = userA

Map
- ${userMap['userA'].username} = userA
- ${userMap['userA']['username']} = userA
- ${userMap['userA'].getUsername()} = userA

실제 렌더링 결과는 위와 같다.

 

 

2) URL 링크 @{ ... }

 타임리프에서 URL을 생성하고 싶으면, @{ ... }을 사용하자. 주로 th:href 태그 속성과 함께 사용된다.

 

- 단순한 URL

<a th:href="@{/hello}"></a>

렌더링 시, /hello 경로로 이동될 것이다.

 

- 쿼리 파라미터

<a th:href="@{/hello(param1=${param1}, param2=${param2})}"></a>

 렌더링 시, /hello?param1=data1&param2=data2 경로로 이동될 것이다. 소괄호 안의 부분을 쿼리 파라미터로 처리해준다.

 

- 경로 변수

<a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}"></a>

 렌더링 시, /hello/data1/data2 경로로 이동될 것이다. URL 경로에 변수가 있으면, 소괄호 안의 부분을 경로 변수로 처리한다.

 

- 쿼리 파라미터 + 경로 변수

<a th:href="@{/hello/param1(param1=${param1}, param2=${param2})}"></a>

 렌더링 시, /hello/data1?param2=data2 경로로 이동될 것이다. 이렇게 쿼리 파라미터와 경로 변수를 함께 사용 가능하다.

 

 

3) 유틸리티 객체

 타임리프는 문자, 숫자, 날짜, URI 등을 편하게 다루는 유틸리티 객체를 제공해준다.

 

- #message : 메시지, 국제화 처리

- #uris : URI 이스케이프 지원

- #dates : java.util.Date 서식 지원

- #calendars : java.util.Calendar 서식 지원

- #temporals : 자바8 날짜 서식 지원

- #numbers : 숫자 서식 지원

- #strings : 문자 관련 편의 기능

- #objects : 객체 관련 기능 제공

- #bools : boolean 관련 기능 제공

- #arrays : 배열 관련 기능 제공

- #lists , #sets , #maps : 컬렉션 관련 기능 제공

- #ids : 아이디 처리 관련 기능 제공

 

 이런 객체들이 있다 정도로만 파악하고, 필요한 경우 직접 찾아서 사용해보자.

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#appendix-b-expression-utility-objects

 

 

4) 자바8 날짜 LocalDate, LocalDateTime

 타임리프에서 LocalDate, LocalDateTime을 사용하려면 추가 라이브러리가 필요하다. 하지만 이떄 스프링 부트로 타임리프를 설치했으면, 'thymeleaf-extras-java8time'이라는 라이브러리가 자동으로 추가되고 통합되어 있을 것이다.

 자바8 날짜용 유틸리티 객체는 #temporals 이다. Model에 localDateTime이라는 이름으로 LocalDateTime.now() 값을 담았다고 생각해보자.

<li>default = <span th:text="${localDateTime}"></span></li>
<li>yyyy-MM-dd HH:mm:ss = <span th:text="${#temporals.format(localDateTime, 'yyyy-MM-dd HH:mm:ss')}"></span></li>

 

- default = 2023-06-06T17:48:44.428197
- yyyy-MM-dd HH:mm:ss = 2023-06-06 17:48:44

 

 

5) 리터럴 literals

 리터럴동적이지 않은, 고정된 값을 말하는 용어이다. 즉, "Hello"는 문자 리터럴, 10, 20은 숫자 리터럴이다. 굳이 용어를 말하면 정리하는 이유는, 타임리프에서 문자 리터럴은 항상 '(작은 따옴표)로 감싸야 한다!

<span th:text="'Hello World'"></span>

 

 근데, 여러 예제들에서 확인해 볼 수 있겠지만 작은 따옴표를 추가하지 않아도, 잘 실행되는 경우가 많다. 그 이유는 공백 없이 쭉 이어지는 문자열이라면, 작은 따옴표를 생략할 수 있다.

 - A-Z, a-z, 0-9, [], ., -, _ 가 허용된다.

 

 따라서 리터럴에 공백이 들어가는 경우를 주의하자!!

 

 

<li><span th:text="'hello ' + ${data}"></span></li>
<li><span th:text="|hello ${data}|"></span></li>

 

- hello Spring!
- hello Spring!

 

 참고로 위와 같이 리터럴과 변수를 같이 사용할 경우, | ... |에 담아서 작성하면 편리하게 사용 가능하다. 

 

 

6) 연산자

 타임리프의 연산은 자바와 크게 다르지 않다. 하지만, HTML 안에서 사용하는 것이므로 HTML 엔티티를 사용하는 부분만 주의하자!

 - >, <, >=, <=, !, ==. !=

 

<li>10 + 2 = <span th:text="10 + 2"></span></li>
<li>1 > 10 = <span th:text="1 &gt; 10"></span></li>

 

- 10 + 2 = 12
- 1 > 10 = 10

 

 

7) 조건식

<li><span th:text="(10 % 2 == 0)? '짝수' : '홀수'"></span></li>

 

- 짝수

 자바의 삼항 연산자와 유사하다.

 

 

8) Elvis 연산자

<li><span th:text="${data}?: '데이터가 없습니다'"></span></li>
<li><span th:text="${nullData}?: '데이터가 없습니다'"></span></li>

 

- Spring!
- 데이터가 없습니다.

 이런식으로 조건식을 활용하여 사용하는 것을 Elvis 연산자라고 한다.

 

 

9) No-Operation

<li><span th:text="${data}?: _">데이터가 없습니다.</span></li>
<li><span th:text="${nullData}?: _">데이터가 없습니다.</span></li>

 

- Spring!
- 데이터가 없습니다.

_ 인 경우, 마치 타임리프가 실행되지 않은 것처럼 HTML 콘텐츠의 내용을 그대로 사용한다.

 

 

4. 태그 속성 추가

 타임리프는 기본적으로 HTML 태그의 속성에 기능을 정의해서 동작한다. 다양한 태그들을 살펴보자.

 

1) th:text, [[ ... ]]

<li th:text="${data}">가나다라</li>
<li>[[HTML 콘텐츠에서 출력하기 = ${data}]]</li>

 

- Hello Spring!
- HTML 콘텐츠에서 출력하기 = Hello Spring!

 HTML의 콘텐츠에 데이터를 출력할 때는, th:text를 사용하자. 위에서 기본 표현식을 확인할때 계속 사용한 태그이다. 만일 HTML 콘텐츠 안에서 직접 사용하고 싶다면, [[ ... ]]을 사용하자.

 

 

2) escape, th:utext, [( ... )]

 HTML 문서는 <, > 과 같은 특수 문자를 기반으로 정의된다. 그런데, 전달되는 정보에 이와 같은 특수문자가 들어가있다면? 처음에 의도한 웹페이지대로 렌더링되지 않을 것이다.

 

 따라서 위에서 배운 th:text, [[ ... ]]은 기본적으로 이스케이프(escape)를 제공해준다. escape란, < 와 같이 HTML에서 사용하는 특수문자를 자동으로 HTML 엔티티로 변경해주는 것을 말한다.

- ex) < 를 &lt; 로, >를 &gt; 로

 

 평상시에는 어떤 데이터가 입력될지 모르므로 th:text를 사용하자. 하지만 만일 가끔씩 이스케이프 기능을 활용하고 싶지 않는 경우가 있을 수 있는데, 이때 타임리프에서는 th:utext, [( ... )] 를 제공해준다.

 

<li>th:text = <span th:text="${data}"></span></li>
<li>th:utext = <span th:utext="${data}"></span></li>
<li><span th:inline="none">[[...]] = </span>[[${data}]]</li>
<li><span th:inline="none">[(...)] = </span>[(${data})]</li>

 

- th:text = <b>Hello Spring!</b>
- th:utext = Hello Spring!
- [[...]] = <b>Hello Spring!</b>
- [(...)] = Hello Spring!

 

 

3) th:with

 th:with를 사용하면 지역 변수를 선언해서 사용할 수 있다. 이는 local variable이므로, 선언한 태그 안에서만 사용할 수 있다.

 

<div th:with="first=${users[0]}">
	<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>

 

처음 사람의 이름은 userA

 

 

4) th:href

 url 태그를 동적으로 변경하고 싶을때 사용한다. 보통 위에서 언급한 @{ ... }와 함께 사용한다.

<li><a th:href="@{/hello}">basic url</a></li>

 

 

5) 속성 값 설정 (th:name, th:attrappend, th:checked, ...)

 타임리프는 HTML 태그에 th:* 속성을 지정하는 방식으로 동작한다. 이렇게 속성을 적용하면, 기존 속성을 대체하며 만일에 없는 경우 새롭게 만들게 된다.

 

- th:name

<input type="text" name="mock" th:name="userA" />

 

<input type="text" name="userA" />

 타임리프를 렌더링하게 되면, 위 코드가 아래 코드로 바뀌게 된다.

 

- th:attrappend, th:attrprepend, th:classappend

 기존 속성에 값을 추가할 수 있다. 

th:attrappend = <input type="text" class="text" th:attrappend="class=' large'" /><br/>
th:attrprepend = <input type="text" class="text" th:attrprepend="class='large '" /><br/>
th:classappend = <input type="text" class="text" th:classappend="large" /><br/>

 

th:attrappend = <input type="text" class="text large" /><br/>
th:attrprepend = <input type="text" class="large text" /><br/>
th:classappend = <input type="text" class="text large" /><br/>

 위 코드를 렌더링하면, 아래처럼 바뀌게 된다. 하나하나씩 살펴보자.

 

th:attrappend는 속성 값 뒤에 값을 추가한다.

  class="text"에 "class=' large'"을 추가해서 class="text large"가 된다.

th:attrprepend는 속성 값 앞에 값을 추가한다.

  class="text"에 "class='large '"을 추가해서 class="large text"가 된다.

- th:classappend는 class 속성 뒤에 자연스럽게 값을 추가해준다.

  class="text"에 "large"를 추가해서 class="text large"가 되었다.

 

 여기서 눈여겨볼 점은, th:attrappend와 th:attrprepend는 직접 띄어쓰기를 해주었다. 하지만, th:classappend는 2개의 속성과 다르게 띄어쓰기를 직접 해주지 않아도, 알아서 적용되었다.

 

- th:checked 체크박스 처리

- checked=false <input type="checkbox" name="active" checked="false" /><br/>

<실행 결과>

 HTML의 체크박스 같은 경우 checked라는 속성이 있기만 해도 체크가 된다. 이때, 속성의 값과는 전혀 관계가 없다. 위 예시에서 checked="false"로 지정했음에도 불구하고, 체크 표시가 있게 된다.

 이는 true, false를 사용하는 개발자의 입장에서는 상당히 불편하다. 타임리프의 th:checked는 값이 false라면, 렌더링 시 checked 속성 자체를 제거해준다.

 

checked o <input type="checkbox" name="active" th:checked="true" /><br/>
checked x <input type="checkbox" name="active" th:checked="false" /><br/>

 

checked o <input type="checkbox" name="active" th:checked="true" /><br/>
checked x <input type="checkbox" name="active" /><br/>

<실행 결과>

 

6) th:each (반복)

타임리프에서 반복은 th:each를 사용한다.

 

<table border="1">
    <tr>
        <th>username</th>
        <th>age</th>
    </tr>
        <tr th:each="user : ${users}">
        <td th:text="${user.username}">username</td>
        <td th:text="${user.age}">0</td>
    </tr>
</table>

<실행 결과>

 ${users}의 값을 하나씩 꺼내서 왼쪽 로컬 변수 user에 담아, 태그를 반복 실행한다. 이때, th:each는 List, 배열, Iterable, Enumeration을 구현한 모든 객체를 사용 가능하다.

 마치 자바에서 for each문을 생각하면 된다.

 

 반복을 하다보면 현재 반복을 몇번 했는지, 전체 사이즈는 몇인지 알고싶은 경우가 있다. 이를 위해 타임리프에서 제공해주는 다음과 같은 기능을 사용하자.

 

<tr th:each="user, userStat : ${users}">

 위 코드와 같이 두 번째 파라미터를 정의하자. 참고로 두 번째 파라미터를 생략해도 가능하며, 생략할 경우 지정한 변수명 + Stat 이 이름이 된다. 위 예시로 예를 들어보면 생략했을 경우 똑같이 userStat이라는 이름으로 사용 가능하다.

 

 해당 파라미터로 아래 데이터들에 대해 접근할 수 있다.

index: 0부터 시작하는 값

count: 1부터 시작하는 값

size: 전체 사이즈

even, odd: 홀수, 짝수 여부 (return값 boolean type)

first, last: 처음, 마지막 여부 (return값 boolean type)

current: 현재 객체

 

 

7) th:if, th:unless, th:switch (조건문)

<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>

 th:if 는 해당 조건이 이면, 태그를 렌더링한다. 반대로 th:unless는 해당 조건이 거짓이어야, 태그를 렌더링한다. 여기서 핵심은 조건에 부합하지 않으면, <span> ... </span> 부분 자체가 렌더링 되지 않고 아예 사라진다.

<렌더링 되지 않음>

<td th:switch="${user.age}">
    <span th:case="10">10살</span>
    <span th:case="20">20살</span>
    <span th:case="*">기타</span>
</td>

 th:switch 는 모든 case에 break가 붙어있는 자바의 switch문과 비슷하게 생각하면 된다. 값이 * 인 부분만족하는 조건이 없을 경우, 사용되는 디폴트 경우이다.

 

 

8) th:block

 th:block은 HTML 태그에 없는, 타임리프 만의 유일한 자체 태그이다. 위에서 살펴본 바로도 알 수 있지만, 타임리프의 특성상 HTML 태그 안에 속성으로 기능을 정의하는데, 기능을 사용해야 하지만 넣을 자리가 애매한 경우에 사용된다.

 

<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>

 위와 같이 반복을 해야하는데, table은 아니라 th:each를 놓을 곳이 애매한 경우에 사용된다.

 

 

 


위 내용은 김영한 님의 인프런 강의 "스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술"의 내용과 강의자료를 토대로 작성된 게시글입니다.

강의 링크:

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2

댓글