본문 바로가기
TIL(Today I Learned)

Thymeleaf에 대한 정리

by kkkdh 2023. 7. 28.
728x90

글을 쓰는 목적에 대해서

이번 글은 thymeleaf의 기본 사용 방법과 특징 등 다양한 정보를 정리하기 위해 작성하게 되었습니다. 

 

글의 내용은 thymeleaf 공식 문서와 김영한 님의 "스프링 MVC 2편 - 백엔드 웹 개발 활용 기술" 강의에서 들은 내용을 기반으로 작성했으며, 추후에 thymeleaf를 사용하면서 내용을 개선해 나갈 예정입니다.

 

따라서 아래에 작성된 글의 내용 중 틀린 부분이 있을 수도 있어, 그러한 부분을 편하게 지적해주시면 감사할 것 같습니다.


Thymeleaf의 특징

https://www.thymeleaf.org/

 

Thymeleaf

Integrations galore Eclipse, IntelliJ IDEA, Spring, Play, even the up-and-coming Model-View-Controller API for Java EE 8. Write Thymeleaf in your favourite tools, using your favourite web-development framework. Check out our Ecosystem to see more integrati

www.thymeleaf.org

이렇게 소개가 되어있다.

정리하면 다음과 같다.

  • 서버사이드 HTML rendering (SSR)
  • 네츄럴 템플릿 (순수 HTML을 최대한 유지한다는 특징, 웹 브라우저에서 파일을 직접 열어볼 수 있음)
  • 스프링 통합 지원

성능은 JSP보다 상대적으로 떨어지는 것으로 보이나, MVC 패턴으로 개발하는 환경에서 JSP 보다 view 파트의 구현에 더 집중할 수 있다는 점Spring과의 궁합이 좋다는 점에서 최근에 많이 사용하고 있다고 한다. (natural template 임에 따라 웹 브라우저에서 바로 열어볼 수 있다는 장점 또한 존재)

 

타임리프 사용을 위해서는 html 태그에 사용 선언이 필요

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

 

text, utext

HTML의 콘텐츠에 데이터를 출력할 때에는 다음과 같이 th:text를 사용하면 된다.

<span th:text="${data}"></span>

혹은 HTML 태그 속성이 아니라 HTML content안에 직접 데이터를 출력하고 싶은 경우 다음과 같은 방법을 사용한다.

[[${data}]]

 

Escape

HTML 문서는 <, >와 같은 특수 문자 기반으로 정의된다. 따라서 뷰 템플릿으로 HTML 화면을 생성할 때는 출력하는 데이터에 이러한 특수 문자가 있는 것에 주의해서 사용해야 한다.

 

예를 들어 출력하고픈 텍스트에 사용자가 직접 b tag를 덧씌워서 강조하고자 하는 예를 보자.

data = "Hello <b>Spring!!</b>"

이렇게 데이터를 전달하는 경우 기존의 text 방식으로 thymeleaf에서 데이터를 출력하면, 다음과 같이 의도한 바와는 다르게 소스에 < → &lt, > → &gt와 같이 변경됨을 확인 가능

Hello <b>Spring!!</b> // web browser
Hello &lt;b&gt;Spring!!&lt;b&gt; // source

 

HTML 엔티티

웹 브라우저는 '<'를 HTML 태그의 시작으로 인식한다. 따라서 '<'를 문자로 표현할 수 있는 별도의 방법이 필요한데, 이를 HTML 엔티티라고 한다.

 

이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 이스케이프(escape)라고 하며, 타임리프가 제공하는 th:text, [[…]]는 기본적으로 이스케이프를 제공한다.

 

앞서 살펴본 <, > 외에도 수많은 HTML 엔티티가 존재한다.

 

Unescape

이스케이프를 사용하지 않도록 타임리프가 제공하는 두 가지 방식

  • th:utext
  • [(…)]

기본적으로는 escape 처리를 하지 않아 페이지 렌더링이 정상 작동 안 하는 케이스가 있어 escape를 적용해야 한다.

 

따라서 꼭 필요한 경우에만 unescape 처리하자.

 

SpringEL 표현식

thymeleaf는 Spring의 규칙에 따라 다양한 방식으로 전달되는 데이터 객체의 필드에 접근이 가능하다.

왼편(작성한 코드), 오른편(출력 결과)

이런 식으로 다양하게 객체의 필드 데이터에 접근이 가능하다.

 

th:with를 이용해 해당 태그 내에서 사용 가능한 지역 변수 선언 또한 가능하다.

 

기본 객체들

굳이 model을 통해 전달하지 않아도 request parameter와 같이 자주 사용되는 객체 정보들은 thymeleaf에서 기본적으로 사용할 수 있도록 제공된다. (이것 또한 스프링 통합 지원 덕택이 아닐까)

사용 예시는 위와 같다.

@를 붙여 스프링 빈에 직접 접근 또한 가능하다. (여기선 helloBean 이라는 이름으로 등록된 빈 객체를 사용)

 

 

URL 링크

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

() 괄호 내에서 query parameter 또는 path variable(경로 변수) 지정이 가능하다.

 

path variable로 지정한 경우 (중괄호로 감싸 경로 상에 끼워넣은 경우) path variable로 mapping, 그렇지 않은 경우 query parameter로 URL 링크에 삽입된다.

 

 

리터럴

타임리프의 리터럴 종류

  • 문자: ‘hello’
  • 숫자: 10
  • 불린: true, false
  • null: null

타임리프에서 문자 리터럴은 반드시 ‘(작은 따옴표)로 감싸야한다.

 

작은따옴표로 매번 감싸는 행위가 번거로워 공백 없이 쭉 이어지는 경우 하나의 의미 있는 토큰으로 인지해서 작은따옴표를 생략 가능한 경우가 존재한다.

 

룰: A-Z, a-z, 0-9, [], ., -, _

원칙 상 작은따옴표로 감싸야하며, 공백하나라도 있는 경우 의미 있는 토큰으로 인식되지 않는다.

 

<span th:text="hello world!"></span>   // make error
<span th:text="'hello world!'"></span> // success
<li>'hello' + ' world!' = <span th:text="'hello' + ' world!'"></span></li>
<li>'hello world!' = <span th:text="'hello world!'"></span></li>
<li>'hello ' + ${data} = <span th:text="'hello ' + ${data}"></span></li>
<li>리터럴 대체 |hello ${data}| = <span th:text="|hello ${data}|"></span></li>

마지막 예제처럼 '|'로 감싸서 변수를 치환해 리터럴로 대체하는 문법 또한 사용이 가능하다.

 

 

연산

다양한 연산을 th:text 내부에서 실행 가능하다.

기본적으로 사용하는 산술 연산자도 사용 가능하며, conditional operator, gt, ge 같은 약자로 대체하는 연산까지 가능하다.

 

  • 비교 연산: HTML 엔티티를 사용해야 하는 부분을 주의
    • ( >(gt), <(lt), >=(ge), <=(le), !(not), ==(eq), !=(neq, ne))
  • 조건식: 자바의 조건식과 유사
  • Elvis 연산자: 조건식의 편의 버전
  • No-operation: '_'인 경우 thymeleaf가 실행되지 않는 것처럼 동작한다. 이것을 잘 활용하면, HTML의 내용을 그대로 활용할 수 있다. (natural template 특징을 살릴 수 있을 것으로 보임)
조건식
<li> (10 % 2 == 0) ? '짝수' : '홀수' = <span th:text="(10 % 2 == 0) ? '짝수' : '홀수'"></span></li>

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

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

 No-operation 예시는 Elvis 연산자 사용 시에 null data를 인자로 넘기는 경우 '_'가 적용되어 마치 th:text 속성이 존재하지 않았던 것처럼 적용된다는 점을 주목해야 될 것 같다.

https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#the-no-operation-token

 

Tutorial: Using Thymeleaf

1 Introducing Thymeleaf 1.1 What is Thymeleaf? Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text. The main goal of Thymeleaf is to provide a

www.thymeleaf.org

 

기본적으로 큰 따옴표 내부에 작성되는 코드일반 프로그래밍 언어를 이용한 코드 작성과 동일한 방식으로 이해했다. 예외로 둘 것은 문자 리터럴을 작은따옴표로 감싼다는 점

 

속성 값 설정

태그의 속성 값을 th:*로 설정한다.

 

이때, 지정된 속성 값을 thymeleaf에 대해 설정했다면, 이 값으로 기존 속성 값이 대체된다.

 

thymeleaf가 natural template이기 때문에, 기존 HTML을 크게 건들지 않고 이런 방식으로 살짝 만 대체하며 타임리프의 요소를 더해가는 방식으로 template의 역할을 수행한다고 정리할 수 있다.

 

th:checked=true or false 사용해서 checked 속성 자체를 없애거나 생성하거나 하는 작업을 수행할 수 있다. HTML은 checked 속성 정의만 돼있어도 체크한 것으로 표시한다.

 

 

반복

th:each를 사용해 thymeleaf 내에서 반복을 구현할 수 있다.

이런 식으로 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>

위의 코드를 보면, userStat이라는 두 번째 변수를 th:each에서 사용하고 있다.

 

이 변수가 현재 반복 상태에 대한 정보를 제공하게 된다.

https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#iterable-values

두 번째 변수를 생략하는 경우 첫 번째 변수(iteration variable) 이름 + Stat이라는 이름으로 자동 생성되어 사용 가능하다고 한다.

 

반복 상태 유지 기능

위와 같은 정보를 반복 상태 변수를 통해 사용 가능

  • index: 0부터 시작하는 값
  • count: 1부터 시작하는 값
  • size: 전체 사이즈
  • even, odd: 짝수 홀수 여부
  • first, last: 처음, 마지막 여부
  • current: 현재 객체

 

조건문

thymeleaf는 조건문 구현을 위해 if, unless + switch, case 지원

unless는 if의 반대 (lt = less than, ge = greater than or equal to)

else if 나 else와 다른 느낌 (if not)

switch case는 java에서 사용하듯 똑같이 사용 가능하다. (문법상 똑같지는 않지만, 개념 동일)

 

 

주석

<h1>예시</h1>
<span th:text="${data}">html data</span>
<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>
/*/-->

브라우저로 rendering 이후 전달된 소스를 확인하면 다음과 같다.

표준 HTML 주석은 렌더링 과정 없이 그대로 전달된 것을 확인 가능

 

파서 주석은 주석 처리된 것 사이의 코드가 아예 무시되었고, 프로토타입 주석의 경우 웹 브라우저를 통해 파일을 직접 여는 경우 주석 처리 되지만, 페이지가 렌더링 되는 경우 주석 처리가 해제되는 특이한 동작 방식을 확인할 수 있다.

 

 

블록 태그

thymeleaf에서 제공하는 유일한 자체 태그 <block>

 

each만을 이용해서 반복 처리가 쉽지 않을 때 사용

여러 개의 태그를 한 번에 반복하고 싶을 때 감싸기 위한 목적으로 사용

렌더링 된 페이지 소스를 보면, block 태그는 사라져 있다.

 

물론 어떻게 block 태그 없이 사용할 수 있을 것 같은데, 어쩔 수 없이 사용해야만 하는 경우가 있다고 하며

이럴 때 사용할 수 있는 자체 제공 태그라고 생각하면 좋을 것 같다. (어디에 활용할 수 있는지에 대해서는 경험을 통해 배워야 하지 않을까 싶다..)

 

 

자바스크립트 인라인

thymeleaf는 js에서 thymeleaf를 편리하게 사용할 수 있는 javascript 인라인 기능을 제공한다.

<script th:inline="javascript">

위와 같이 자바스크립트 인라인 기능을 적용한다.

 

자바스크립트는 서버에서 실행되는 코드가 아니라 웹 브라우저 실행되는 코드이다.

 

이 자바스크립트 코드에도 natural template이 적용된다.

<!-- 자바스크립트 인라인 사용 전 -->
<script>
  var username = [[${user.username}]];
  var age = [[${user.age}]];

  //자바스크립트 내추럴 템플릿
  var username2 = /*[[${user.username}]]*/ "test username";

  //객체
  var user = [[${user}]];
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script th:inline="javascript">
  var username = [[${user.username}]];
  var age = [[${user.age}]];

  //자바스크립트 내추럴 템플릿
  var username2 = /*[[${user.username}]]*/ "test username";
  
  //객체
  var user = [[${user}]];
</script>

자바스크립트 인라인을 사용하기 이전 코드는 thymeleaf의 값을 사용하기 위해서 정수 리터럴은 몰라도 문자열 리터럴에 대해서는 큰 따옴표로 감싸거나 하는 등의 수고로움이 매 번 따르게 된다.

 

이렇게 경우에 따라서 코드에 따옴표를 추가하고 하는 작업은 벌써부터 어지럽다.

script tag에 자바스크립트 인라인을 적용하면, thymeleaf에 의해 추가적인 렌더링이 이루어져 훨씬 thymeleaf의 값을 처리하기에 수월해진다.

 

JS 인라인 사용 전 → UserA

JS 인라인 사용 후 → “UserA”

 

또한 인라인을 사용하면, 주석을 활용해 natural template으로 동작하게끔 구현 가능하다. → 주석 처리된 부분의 값이 thymeleaf에 의해 렌더링 될 때에만 적용되도록 설정 가능 (js natural template 설명 부분)

 

심지어 마지막 user를 보면, 객체를 json으로 변환해주는 결과까지 확인 가능하다.

 

인라인 each

 

<!-- javascript inline each -->
<script th:inline="javascript">
  [# th:each="user, stat : ${users}"]
  var user[[${stat.count}]] = [[${user}]];
</script>

위와 같이 each를 javascript 코드 내부에서 사용해 반복문 적용 또한 가능

 

템플릿 조각

별도의 템플릿을 이용해 웹 페이지 개발에 사용되는 공통 영역을 분리하기 위해 사용하는 기능이다.

주로 웹 페이지에서 공통으로 포함되는 footer, navigation bar 등의 영역을 조각으로 만들기 위해 사용하는 것으로 보인다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <meta charset="UTF-8">
 <title>Title</title>
</head>
<body>
<h1>부분 포함</h1>
<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>
</body>
</html>

위와 같이 템플릿 조각을 원하는 위치에 th:insert, th:replace를 이용해 끼워 넣을 수 있다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="copy">
 푸터 자리 입니다.
</footer>
<footer th:fragment="copyParam (param1, param2)">
 <p>파라미터 자리 입니다.</p>
 <p th:text="${param1}"></p>
 <p th:text="${param2}"></p>
</footer>
</body>
</html>

footer.html 파일에는 위와 같이 template fragment를 선언해 둔 상태이다. (param1, param2와 같이 인자로 원하는 값을 넘겨줄 수도 있다.)

 

fragment 추가 방식 2가지

  • insert: 해당 태그 내부에 template fragment 삽입
  • replace: 해당 태그를 template fragment로 대체

https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#including-template-fragments

 

Tutorial: Using Thymeleaf

1 Introducing Thymeleaf 1.1 What is Thymeleaf? Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text. The main goal of Thymeleaf is to provide a

www.thymeleaf.org

별개의 파일에 fragment 정의해서 재활용 가능하다.

 

~와 함께 {경로 :: fragment name} 형식으로 정의해서 사용한다.

 

템플릿 조각에 parameter를 당연히 넘겨줄 수 있다. 괄호로 감싸서 함수 정의하듯 template fragment를 정의하고, 사용할 때에도 인자(argument) 넘겨주듯 전달 가능

 

 

템플릿 레이아웃

이번에는 반대로 어떠한 큰 코드가 존재하고, 나의 코드 일부를 넘겨서 끼워 넣고 싶은 경우에 사용하는 템플릿 레이아웃에 대한 것 (인자로 태그 자체를 넘기는 개념)

 

template fragment의 인자로 tag 자체를 넘겨버리는 개념이다.

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

이렇게 코드를 선언하고, template/layout/base template fragment에 title, link tag를 모두 전달하는 구조

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

→ base.html

받아온 title, links를 이용해 각 블록을 replace 해서 갈아 끼운다.

태그에만 제한된 것이 아니라 css selector와 같이 범위 지정이 가능

 

base가 거대한 layout이고, 인자로 코드 일부를 넘겨서 내가 원하는 것으로 customizing 하는 개념

 

전체 html 파일을 layout으로 사용하고, 여기에 일부 코드를 첨가하는 방식으로의 확장 또한 가능하다.

 

필요한 부분을 해당 html 파일에 정의하고, 전체 html 태그를 통째로 layout으로 갈아 끼우는 느낌 같다.

 

layout file로 만들기 위해서는 html tag에 th:fragment 부분에 정의를 해야 하고 (전체 html 페이지에서 일부를 갈아 끼는 개념이어서 그렇지 않을까 싶다)

fragment file로 만들기 위해서는 footer tag를 선언하고 해당 tag의 th:fragment 속성을 정의해야 하는 것으로 보인다. (일부 값만 인수로 받아와서 대체하기 때문이지 않을까)

 

사실 템플릿 레이아웃, 템플릿 조각 둘은 공통부분을 조각화 해서 처리하는 방식과 범위의 차이일 뿐 문법적으로 뭐가 이렇다는 느낌은 잘 받지 못했던 것 같다.

728x90

댓글