스프링 MVC-타임리프

타임리프

Thymeleaf는 뷰 템플릿 엔진으로 서버 사이드에서 html을 동적으로 렌더링하는 역할을 한다. 또한 타임리프는 네츄럴 템플릿으로 원본 html 파일을 최대한 유지시켜 파일을 그냥 열어서 내용을 확인할 수 있는 특징을 가진다.
이제부터 타임리프가 제공하는 기본 기능들에 대해 하나씩 알아보자.

기본 표현식

아래는 타임리프가 제공하는 기본 표현식들이다.

• 간단한 표현:
◦ 변수 표현식: ${…}
◦ 선택 변수 표현식: *{…}
◦ 메시지 표현식: #{…}
◦ 링크 URL 표현식: @{…}
◦ 조각 표현식: ~{…}
• 리터럴
◦ 텍스트: ‘one text’, ‘Another one!’,…
◦ 숫자: 0, 34, 3.0, 12.3,…
◦ 불린: true, false
◦ 널: null
◦ 리터럴 토큰: one, sometext, main,…
• 문자 연산:
◦ 문자 합치기: +
• 산술 연산:
◦ 리터럴 대체: |The name is ${name}|
◦ Binary operators: +, -, *, /, %
◦ Minus sign (unary operator):
• 불린 연산:
◦ Binary operators: and, or
◦ Boolean negation (unary operator): !, not
• 비교와 동등:
◦ 비교: >, <, >=, <= (gt, lt, ge, le)
◦ 동등 연산: ==, != (eq, ne)
• 조건 연산:
◦ If-then: (if) ? (then)
◦ If-then-else: (if) ? (then) : (else)
◦ Default: (value) ?: (defaultvalue)
• 특별한 토큰:
◦ No-Operation: _

텍스트

타임리프에서 텍스트를 출력하는 방법은 2가지가 있다. 첫 번째는 ‘th:text’ 태그를 사용하는 것이고, 컨텐츠 안에서 직접 출력하고 싶다면 ’[[…]]’를 사용하면 된다. 실 사용례는 아래와 같다.

  
<li>th:text 사용 <span th:text="${data}"></span></li>
<li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li>

data라는 이름의 변수를 모델에서 받아와 출력하는 코드이다. 그런데 한 가지 주의할 점이 있다.
html은 <>를 사용한 태그를 기반으로 작동하기 때문에, <>를 포함한 몇몇 문자는 텍스트로서 출력하기 위해 적절한 렌더링 과정을 통해 HTML 엔티티로 변경하여야 한다.
그리고 이렇게 HTML 엔티티로 변경하는 과정을 이스케이프라고 한다. 타임리프는 기본적으로 이스케이프를 지원하기 때문에
‘<’ -> ‘&lt’
‘>’ -> ‘&gt’
와 같은 식으로 변경된다. 따라서 몇몇 특수문자를 이스케이프 하지 않고 그대로 사용하고 싶다면 조금 다른 출력방식을 사용하면 되는데, ‘th:utext’ 또는 ’[(…)]’ 를 사용하면 된다.

변수

타임리프의 변수 표현식은 ’${…}’의 형식으로 사용 가능하다. 아래의 사용례를 통해 어떻게 사용하는지 알아보자.

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

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

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

변수로는 객체 뿐만 아니라 컬렉션들도 받을 수 있는데, 위의 3줄은 객체에서 프로퍼티에 접근하는 방법을, 아래는 컬렉션을 받아 컬렉션 내부의 객체에 접근하는 방법을 나타낸 코드이다.

지역변수

타임리프 내부에서 지역변수를 선언하여 사용할 수도 있다. ‘th:with’를 사용하면 되는데 사용례는 아래와 같다.

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

자바 코드에서 하듯이 필요하다면 지역 변수를 선언하여 사용이 가능하고, 지역변수는 선언한 태그 내에서만 사용이 가능하다.

기본 객체와 편의 객체

스프링부트 3.0 전까지는 타임리프에서 #request, #response 등의 기본 객체들을 지원했었다. 하지만 스프링부트 3.0을 기점으로 더이상 지원하지 않고, 대신 편의객체들은 아직 지원하고 있다.
예를 들어 쿼리 파라미터에 접근하기 위해서는 원래는 HttpServletRequest 객체를 받아 getParameter() 등의 메서드를 사용하여 접근해야 한다.
이러한 번거로움을 방지하기 위해 있는 것이 편의 객체이다. 편의 객체들은 다음과 같다.

  • 쿼리 파라미터 접근 : param
  • HTTP 세션 접근 : session
  • 스프링 빈 접근 : @빈이름

실 사용례는 아래와 같다.

  
    <li>Request Parameter = <span th:text="${param.paramData}"></span><li>
    <li>session = <span th:text="${session.sessionData}"></span></li>
    <li>spring bean = <span th:text="${@helloBean.hello('Spring!')}"></span></li>

유틸리티 객체

타임리프는 문자, 숫자, 날짜, url 등을 편리하게 다룰 수 있는 여러 유틸리티 객체들을 지원한다. 외울 필요는 없고 있다는 것은 알고 있다가 나중에 필요할 때 찾아서 사용하도록 하자. 목록은 다음과 같다.

  • #message : 메시지, 국제화 처리
  • #uris : URI 이스케이프 처리
  • #dates : java.util.Date 서식 지원
  • #temporals : 자바8 날짜 서식 지원
  • #numbers : 숫자 서식 지원
  • #strings : 문자 관련 편의 기능
  • #objects : 객체 관련 기능 제공
  • #bools : boolean 관련 기능 제공
  • #arrays : 배열 관련 기능 제공
  • #lists, #sets, #maps : 컬렉션 관련 기능 제공
  • #ids : 아이디 처리 관련 기능 제공

URL 링크

타임리프에서 URL 링크를 생성할 때엔 ’@{…}’를 사용하면 된다. 또한 쿼리 파라미터, PathVariable과 같은 기능들을 사용할 수 있는데, 아래의 사용례를 보며 알아보자.

  
    <li><a th:href="@{/hello}">basic url</a></li>
    <li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">hello query param</a></li>
    <li><a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>
    <li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>

첫 줄은 기본적인 링크의 사용법이다. 링크는 ‘/hello’로 연결된다.
두번째 줄과 같이 () 안에 있는 부분은 쿼리 파라미터로 처리된다. 따라서 실제 링크는 ‘/hello?param1=data1&param2=data2’과 같이 처리된다.
세 번째 줄은 PathVariable을 사용하는 예시이다. 경로 상에 변수가 있다면 () 내부는 PathVariable로 처리된다. 따라서 실제 링크는 ‘/hello/data1/data2’과 같다.
네 번째 줄은 쿼리 파라미터와 PathVariable을 모두 사용하는 예시이다. 위와 같이 경로에 변수가 있으면서 ()내에 변수를 그 이상으로 입력하면 남은 변수는 쿼리 파라미터로 처리된다. 따라서 실제 링크는 ‘/hello/data1?param2=data2’과 같다.

리터럴

타임리프에는 문자, 숫자, boolean, null과 같은 다양한 리터럴들이 있다. 대개 그냥 사용하면 되지만 한 가지 주의할 점이 있는데, 문자 리터럴은 ‘‘로 감싸야한다는 것이다.

  
    <span th:text="hello world!"></span>

예를 들어 위와 같은 코드는 오류가 난다. hello, world! 두 문자 리터럴로 이루어져 있기 때문이다.

  
    <span th:text="'hello world!'"></span>

위와 같이 따옴표로 감싸주면 정상 동작한다. 그런데 한 가지 더 번거로운 점이 있다.
타임리프에서 리터럴과 변수는 따로 취급된다. 따라서 +등으로 묶어줘야 하는데, 예를 들어 data란 변수 값을 ‘hello world!’ 사이에 넣어 hello ${data} world! 처럼 표현하고 싶다고 가정해보자. 그렇다면 아래와 같이 작성해야 한다.

  
    <span th:text="'hello ' + ${data} + 'world!'"></span>
매우 번거롭기 때문에 이를 위해 나온 것이 리터럴 대체 문법이다. 사용법은 간단하다.   로 감싸주기만 하면 된다. 위의 문장에 사용해보자.
  
    <span th:text="|hello ${data} world!|"></span>

따옴표도, +기호도 생략하고 매우 편리하게 사용이 가능하므로 자주 쓰도록 하자.

연산

타임리프의 연산은 자바의 그것과 거의 유사하다. 대신 위에서 설명했던 HTML 엔티티를 사용하는 부분만 주의하자.
조건문 연산도 매우 유사하지만 타임리프에서는 조건문을 간략화한 Elvis 연산자와 특수한 기능을 하는 No-Operation이 존재한다.

먼저 Elvis 연산자는 ‘?:’과 같이 사용되는데, 이전 항이 참이 아니라면 뒤의 항이, 참이라면 이전 항이 실행되는 식이다. 아래의 사용례를 확인하자.

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

data가 null이라면 ‘데이터가 없습니다’ 라는 문구가, 아니라면 data의 값이 출력되는 구조이다.

No-Operation은 ‘_‘으로 사용되는데, 조건문 중에 No-Operation이 실행되는 경우에는 타임리프가 실행하지 않는 것처럼 동작한다. 즉 html 원본의 기능을 수행한다. 자세한건 아래의 사용례를 보자.

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

저 위의 코드와 기능은 비슷하지만 No-Operation을 통해 구현하였다. data의 값이 있다면 data의 값을 출력하고, 없다면 _가 선택되므로 타임리프가 실행되지 않는 것처럼 원래 있던 html 문구인 ‘데이터가 없습니다’를 출력하는 것이다.

속성

타임리프는 ‘th:*‘를 이용하여 기존 html 속성을 덮어 씌우는 식으로 동작한다. 기존 속성이 없다면 새로 만든다. 사용례는 아래와 같다.

  
 <h1>속성 설정</h1>
 <input type="text" name="mock" th:name="userA" />

 <h1>속성 추가</h1>
 - 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/>

첫 줄을 보면 위에서 설명했던 대로 속성을 덮어씌우기 때문에 렌더링 후에는 ‘<input type=”text” name=”userA”>’ 과 같이 변경된다.
속성을 덮어씌워 변경하는 것 외에도 값을 추가할 수도 있는데, 아래의 문법을 사용하면 된다.

  • th:attrappend : 속성값 뒤에 값을 추가한다.
  • th:attrprepend : 속성값 앞에 값을 추가한다.
  • th:classappend : class 속성에 자연스럽게 추가한다.

반복

타임리프에서는 ‘th:each’를 사용하여 반복을 처리한다. 또한 반복 도중 현재 반복 상태에 대해 알 수 있는 여러 상태값을 지원한다. 아래의 사용례를 확인하자.

  
 <tr th:each="user, userStat : ${users}">
        <td th:text="${userStat.count}">username</td>
        <td th:text="${user.username}">username</td>
        <td th:text="${user.age}">0</td>
        <td>
            index = <span th:text="${userStat.index}"></span>
            count = <span th:text="${userStat.count}"></span>
            size = <span th:text="${userStat.size}"></span>
            even? = <span th:text="${userStat.even}"></span>
            odd? = <span th:text="${userStat.odd}"></span>
            first? = <span th:text="${userStat.first}"></span>
            last? = <span th:text="${userStat.last}"></span>
            current = <span th:text="${userStat.current}"></span>
        </td>
    </tr>

th:each=”user, userStat : ${users} 와 같이 작성하면 users라는 컬렉션에서 값을 하나씩 꺼내 user라는 변수에 담아 반복한다. List, Map, 배열 등 반복할 수 있는 대부분의 컬렉션에서 사용이 가능하다.

userStat과 같이 두번째 파라미터를 넣으면 반복의 상태값을 확인할 수 있다. index, count, size 등 현재의 반복 상태에 대한 여러 상태값을 볼 수 있다.

조건식

타임리프의 조건식은 if, unless를 사용하는데 특이한 것은 값이 false라면 아예 해당 태그가 렌더링 되지 않는다. 만약 아래와 같이 사용한다면

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

false일 시 해당 파트가 통째로 렌더링 되지 않고 사라진다.

주석

타임리프의 주석은 3가지 종류가 있다.

  • 표준 html 주석
  • 타임리프 파서 주석
  • 타임리프 프로토타입 주석

각각의 사용례는 다음과 같다.

  
 <h1>1. 표준 HTML 주석</h1>
 <!-
<span th:text="${data}">html data</span>-->
 <h1>2. 타임리프 파서 주석</h1>
 <!--/* [[${data}]] */-->
 <!--/*-->
 <span th:text="${data}">html data</span>
 <!--*/-->
<h1>3. 타임리프 프로토타입 주석</h1>
 <!--/*/
 <span th:text="${data}">html data</span>
 /*/-->

표준 html 주석은 타임리프가 렌더링하지 않고 그대로 남겨둔다.
타임리프 파서 주석은 렌더링될 때 주석 부분이 제거된다.
타임리프 프로토타입 주석이 조금 특이한데 html 파일을 그대로 열면 주석처리가 되어 보이지 않지만, 타임리프가 렌더링하면 정상적으로 렌더링되어 웹에 표시된다.

블록

th:block은 html이 아닌 유일한 타임리프의 자체태그이다. 타임리프는 한 태그 내에 속성으로 기능을 정의하여 사용하는데, 하나의 태그 내에 기능을 정의하면 해당 태그에만 기능이 적용된다.
따라서 여러 태그에 하나의 기능을 적용시키고 싶다면 th:block을 사용하여 블록으로 묶어서 실행하면 된다. 자세한 사용례는 아래와 같다.

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

div 내에 each를 사용하여 반복한다면 해당 태그 내에만 적용된다. 2개의 div 태그에 모두 반복을 적용시키기 위해 block을 사용한 코드이다.

템플릿 조각과 레이아웃

만약 카테고리, 상단메뉴 등의 모든 웹에서 사용하는 공통 부분을 코드를 복사하여 사용한다면 수정이 필요할 때 모든 파트를 일일이 수정해야 할 것이다.
타임리프는 이러한 일을 방지하기 위해 자바의 인터페이스와 같은 템플릿 조각과 레이아웃 기능을 제공한다. 복잡한 부분이니 실 사용례를 보며 알아보자.

  
<footer th:fragment="copy">
푸터 자리 입니다.
</footer>

<footer th:fragment="copyParam (param1, param2)">
    <p>파라미터 자리 입니다.</p>
    <p th:text="${param1}"></p>
    <p th:text="${param2}"></p>
</footer>

템플릿 조각은 위와 같이 <footer> 태그 내에 th:fragment로 템플릿 조각을 만들고 이름을 붙인다. 이렇게 만든 템플릿 조각은 다른 html 파일에서 사용이 가능하다.

  
 <h2>부분 포함 insert</h2>
 <div th:insert="~{template/fragment/footer :: copy}"></div>
 <h2>부분 포함 replace</h2>
 <div th:replace="~{template/fragment/footer :: copy}"></div>
 <h2>부분 포함 단순 표현식</h2>
 <div th:replace="template/fragment/footer :: copy"></div>
 <h1>파라미터 사용</h1>
 <div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>

위에서 정의한 템플릿 조각의 이름, 그리고 파라미터를 이용하여 다른 파일에서 해당 템플릿 조각들을 사용하는 모습이다.

insert를 사용하면 현재 태그(div) 내부에 추가하고, replace를 사용하면 현재 태그를 대체한다. 파라미터가 있는 조각이라면 파라미터를 입력하여 동적으로 조각을 렌더링할 수도 있다.
이렇게 하나의 레이아웃을 여러 곳에서 자유롭게 사용할 수 있는 것을 확인할 수 있다.

레이아웃은 템플릿 조각이 더욱 확장된 개념이다. 템플릿 조각은 코드 조각을 가지고 와 사용했다면, 레이아웃에서는 코드 조각을 레이아웃에 넘겨 사용한다.
예를 들어 <head>에 공통으로 사용하는 css, javascript 등을 한곳에 모아 공통으로 사용하되, 필요하다면 정보를 추가하면서 사용하고 싶을 때 레이아웃을 사용한다. 자세한건 아래의 코드로 알아보자.

  
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common_header(title,links)">

<title th:replace="${title}">레이아웃 타이틀</title>
    
 <!-- 공통 -->
<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
<link rel="shortcut icon" th:href="@{/images/favicon.ico}">
<script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>

 <!-- 추가 -->
 <th:block th:replace="${links}" />
</head>

위의 코드가 예시에서 말한 css, javascript 등을 모아놓은 코드이다. 레이아웃 코드라고 볼 수 있다.
여기에 필요한 title, link 등의 정보를 넘겨 사용하게 된다. 이제 해당 레이아웃을 사용하는 코드를 보자.

  
 <!DOCTYPE html>
 <html xmlns:th="http://www.thymeleaf.org">
 <head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
    <title>메인 타이틀</title>
    <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
    <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
 </head>
 <body>
메인 컨텐츠
</body>
 </html>

레이아웃을 사용하는 코드이다. <head> 태그 내에서 replace 한다는 것은 해당 태그 내부가 전부 위의 레이아웃 코드로 대체된다는 뜻이다.
대신 레이아웃에 필요한 title, link 등의 정보를 담아서 넘기는 역할을 한다.
이제 레이아웃과 레이아웃을 사용하는 코드가 결합되어 어떻게 결과물이 나오는지 확인하자.

  
 <!DOCTYPE html>
 <html>
 <head>
 <title>메인 타이틀</title>
 <!-- 공통 -->
 <link rel="stylesheet" type="text/css" media="all" href="/css/awesomeapp.css">
<link rel="shortcut icon" href="/images/favicon.ico">
 <script type="text/javascript" src="/sh/scripts/codebase.js"></script>
 <!-- 추가 -->
 <link rel="stylesheet" href="/css/bootstrap.min.css">
 <link rel="stylesheet" href="/themes/smoothness/jquery-ui.css">
 </head>
 <body>
메인 컨텐츠
</body>
 </html>

<head> 내부는 완전히 레이아웃의 코드로 대체되었다. 하지만 넘겨준 title, link는 레이아웃에서 지정한 위치에 정확히 등재된 것을 볼 수 있다.
이렇게 하면 자신의 메인 컨텐츠는 그대로 냅두면서, 원하는 타이틀, 원하는 링크를 달아 정해진 레이아웃으로 제목 부분을 꾸밀 수 있게 된다.
하나의 레이아웃을 여러 군데에서 사용하기 때문에 수정도 편리할 것이다.

개념을 더 확장하여 <head>가 아닌 <html>을 통째로 치환하여 레이아웃 위에 필요한 내용만 전달하는 식으로 사용도 가능하지만 거의 같은 내용이므로 따로 적지는 않았다.

정리

지금까지 타임리프의 기본적인 기능들에 대해 알아보았다. 크게 유저의 인터랙티브한 동작이 필요없는 간단한 웹페이지는 타임리프로 충분히 만들 수 있다고 강의 도중 얘기하셨는데, 내가 계획중인 개인 프로젝트에도 충분히 사용이 가능할 듯 싶다.
잘 배워두고 정리해두면 분명 나중에 써먹을 일이 있을 것 같다.