타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용된다. 타임리프로 작성한 파일은 HTML을 유지하기 때문에 웹 브라우저에서 파일을 직접 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.
이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿(natural templates)이라 한다.
타임리프를 사용하려면 namespace를 선언 하면 된다.
<html lang="en" xmlns:th="http://www.thymeleaf.org">
viewResolver)가 화면을 찾아서 처리한다.resources:templates/ + {viewName} + .html참고: spring-boot-devtools 라이브러리를 추가하면, html 파일을 컴파일만 해주면 서버 재시작 없이 View 파일 변경이 가능하다.
타임리프는 기본적으로 HTML 태그의 속성에 기능을 정의해서 동작한다.
<span th:text="${data}">
<span>[[${data}]]</span>
<span th:utext="'Hello <b>Spring!</b>'"</span>
[('Hello <b>Spring!</b>')]
th:with 를 사용하면 지역 변수를 선언해서 사용할 수 있다. 지역 변수는 선언한 테그 안에서만 사용할 수 있다.
<div th:with="first=${users[0]}">
<p>첫번째 회원의 이름은 <span th:text="${first.username}"> 입니다.</span></p>
</div>
타임리프는 다양한 기본 객체들을 제공한다.
<ul>
<li>request = <span th:text="${#request}"></span></li>
<li>response = <span th:text="${#response}"></span></li>
<li>session = <span th:text="${#session}"></span></li>
<li>sevletContext = <span th:text="${#sevletContext}"></span></li>
<li>locale = <span th:text="${#locale}"></span></li>
</ul>
${param.paramData}
${session.sessionData}
${@helloBean.hello('Spring!)}
타임리프는 문자, 숫자, 날짜, URI 등을 편리하게 다루는 다양한 유틸리티 객체들을 제공한다.
<span th:text=${#temporals.format(localDateTime, 'yyyy-MM-dd HH:mm:ss')}"></span>
<li>${#temporals.day(localDateTime)} = <span th:text="${#temporals.day(localDateTime)}"></span></li>
<li>${#temporals.month(localDateTime)} = <span th:text="${#temporals.month(localDateTime)}"></span></li>
<li>${#temporals.monthName(localDateTime)} = <span th:text="${#temporals.monthName(localDateTime)}"></span></li>
<li>${#temporals.monthNameShort(localDateTime)} = <span th:text="${#temporals.monthNameShort(localDateTime)}"></span></li>
<li>${#temporals.dayOfWeek(localDateTime)} = <span th:text="${#temporals.dayOfWeek(localDateTime)}"></span></li>
<li>${#temporals.dayOfWeekName(localDateTime)} = <span th:text="${#temporals.dayOfWeekName(localDateTime)}"></span></li>
<li>${#temporals.dayOfWeekNameShort(localDateTime)} = <span th:text="${#temporals.dayOfWeekNameShort(localDateTime)}"></span></li>
<li>${#temporals.hour(localDateTime)} = <span th:text="${#temporals.hour(localDateTime)}"></span></li>
<li>${#temporals.minute(localDateTime)} = <span th:text="${#temporals.minute(localDateTime)}"></span></li>
<li>${#temporals.second(localDateTime)} = <span th:text="${#temporals.second(localDateTime)}"></span></li>
<li>${#temporals.nanosecond(localDateTime)} = <span th:text="${#temporals.nanosecond(localDateTime)}"></span></li>
타임리프에서 URL을 생성할 때는 @{link} 문법을 사용하면 된다.
<ul>
// -> /hello
<li><a th:href="@{/hello}">basic url</a></li>
// -> /hello?param1=data1¶m2=data2
<li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">hello query param</a></li>
// -> /hello/data1/data2
<li><a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>
// -> /hello/data1?param2=data2
<li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>
</ul>
소스 코드상에 고정된 값을 리터럴이라고 한다. 타임리프에서 문자 리터럴은 항상 '(작은 따옴표)로 감싸야 한다. 타임리프에는 다음과 같은 리터럴이 있다.
단, 문자열이 공백 없이 쭉 이어진다면 하나의 의미있는 토큰으로 인지해서 작은 따옴표를 생략할 수 있다.
<!-- OK -->
<span th:text="HelloSpring">
<!-- ERROR -->
<span th:text="Hello Spring">
<!-- OK -->
<span th:text="'Hello Spring'">
타임리프에서 제공하는 리터럴 대체 문법을 사용하면 전체 문자열을 하나의 문자열로 인식하고, 치환해야 할 부분들만 자동으로 치환해준다.
<!-- 리터럴 대체 문법 사용 -->
<span th:text="|hello ${data}|"></span>
<!-- 리터럴 대체 문법 사용 X -->
<span th:text="'hello " + ${data}"></span>
타임리프 연산은 자바와 크게 다르지 않다. HTML 안에서 사용하기 때문에 HTML 엔티티를 사용하는 부분만 주의하자.
<ul>
<li>1 > 2 = <span th:text="1 > 2"></span></li>
<li>1 gt 2 = <span th:text="1 gt; 2"></span></li>
<li>1 >= 2 = <span th:text="1 >=; 2"></span></li>
<li>1 ge 2 = <span th:text="1 ge; 2"></span></li>
<li>1 == 2 = <span th:text="1 ==; 2"></span></li>
<li>1 != 2 = <span th:text="1 !=; 2"></span></li>
</ul>
<span th:text="(4 % 2 == 0)? '짝수' : '홀수'"></span>
<!-- 데이터가 있으면 데이터를 출력하고 없으면 문자열 출력 -->
<span thLtext="${data}?: '데이터 없음'"></span></li>
<!-- 데이터가 없으면 문자열 출력 -->
<span th:text="${nullData}?: '데이터 없음'"></span></li>
<!-- 타임리프 태그가 동작하지 않음. 기본 HTML 출력 -->
<span th:text="${nullData}?: _">데이터 없음</span></li>
타임리프는 주로 HTML 태그에 th: 속성을 지정하는 방식으로 동작한다. th:로 속성을 적용하면 기존 속성을 대체한다. 이 때 기존 속성이 존재하지 않다면 속성을 새로 만들어준다.
<!-- name 속성 값이 th:name에 지정한 값으로 대체된다 -->
<!-- 랜더링 전 -->
<input type="text" name="userName" th:name="HongGildong"/>
<!-- 랜더링 후 -->
<input type="text" name="HongGildong"/>
<input type="text"
<!-- th:attrappend 랜더링 전 (띄어쓰기 주의) -->
<input type="text" class="text" th:attrappend="class=' large'"/>
<!-- 랜더링 후 -->
<input type="text" class="text large"/>
<!-- th:attrprepend 랜더링 전 (띄어쓰기 주의) -->
<input type="text" class="text" th:attrprepend="class='large '"/>
<!-- 랜더링 후 -->
<input type="text" class="large text"/>
<!-- th:classappend 랜더링 전 -->
<input type="text" class="text" th:classappend="large"/>
<!-- 랜더링 후 -->
<input type="text" class="text large"/>
HTML에서는 checked 속성 값이 true이던 false이던 상관없이 checked 속성이 있다면 무조건 checked 처리가 되어버린다. 반면 타임리프의 th:checked를 사용하면 값이 false인 경우 checked 속성을 제거해준다.
<!-- 랜더링 전 -->
<input type="checkbox" name="test" th:checked="false"/>
<!-- 랜더링 후 -->
<input type="checkbox" name="test"/>
타임리프에서 반복문을 처리하기 위해 제공되는 문법에는 th:each 가 있다. 아래 예시에서는 users에 담긴 값들을 하나씩 꺼내서 지정한 변수(user)에 담아서 반복 실행한다.
<tr th:each="user : ${users}">
<td th:text="${user.username}">username</td>
<td th:text="${user.age}">age</td>
</tr>
추가로 th:each의 2번째 파라미터를 설정해서 반복문의 다양한 상태 정보를 확인 할 수 있다. 2번째 파라미터를 생략하면 "변수명 + Stat" 으로 변수명이 자동으로 만들어진다. (userStat)
<tr th:each="user, status : ${users">
<!-- 0부터 시작하는 인덱스 -->
<td th:text="${status.index}"></td>
<!-- 1부터 시작하는 인덱스 -->
<td th:text="${status.count}"></td>
<!-- 전체 사이즈 -->
<td th:text="${status.size}"></td>
<!-- 홀수, 짝수 여부 -->
<td th:text="${status.even}"></td>
<td th:text="${status.odd}"></td>
<!-- 처음, 마지막 여부 -->
<td th:text="${status.first}"></td>
<td th:text="${status.last}"></td>
<!-- 현재 객체 -->
<td th:text="${status.current}"></td>
</tr>
타임리프에서 조건문을 처리하기 위해 제공되는 문법에는 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>
Java의 switch문과 동일하다.
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살span>
<!-- *은 만족하는 조건이 없을 때 실행된다 -->
<span th:case="*">기타</span>
</td>
자바스크립트의 표준 HTML 주석은 타임리프가 랜더링 하지 않고, 그대로 남겨둔다.
<!--
<span th:text="${data}">html data</span>
-->
랜덩링에서 주석 부분을 제거한다.
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->
HTML 파일을 그대로 열어보면 주석처리가 되지만, 랜더링 과정을 거쳤을 경우에는 정상적으로 출력이 된다.
<!--/*/
<span th:text="${data}">html data</span>
/*/-->
타임리프에서 제공하는 타임리프의 자체 태그이고, 랜더링 과정을 거치면 속성이 제거된다.
<th:block th:each"user : ${users}">
<div>
사용자 이름 <span th:text="${user.username"></span>
</div>
<div>
사용자 이름 + 사용자 나이 <span th:text="${user.username} + ' / ' + ${user.age}"></span>
</div>
</th:block>
타임리프는 자바스크립트에서 타임리프를 편리하게 사용할 수 있는 자바스크립트 인라인 기능을 제공한다. 자바스크립트 인라인 기능은 다음과 같이 적용하면 된다.
<script th:inline="javascript">
</script>
자바스크립트 인라인 기능을 사용하기 전과 후를 비교해보자.
<!-- 자바스크립트 인라인 사용 전 -->
<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>
자바스크립트 인라인 사용 전 결과
<script>
var username = userA;
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = /*userA*/ "test username";
//객체
var user = BasicController.User(username=userA, age=10);
</script>
자바스크립트 인라인 사용 후 결과
<script>
var username = "userA";
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = "userA";
//객체
var user = {"username":"userA","Age":10};
</script>
위 예제를 통해 자바스크립트 인라인을 사용하지 않았을 때 발생하는 3가지 문제점을 확인할 수 있다.
자바스크립트 인라인은 each를 지원하는데 다음과 같이 사용한다.
웹 페이지를 개발할 때는 공통 영역이 많이 있다. 예를 들어서 상단 영역이나 하단 영역, 좌측 카테고리 등등 여러 페이지에서 함께 사용되는 영역들이 있다. 이런 부분을 코드를 복사해서 사용한다면 변경시 여러 페이지를 모두 수정해야 하므로 상당히 비효율 적이다. 타임리프는 이러한 문제점을 해결하기 위해 템플릿 조각과 레이아웃 기능을 지원한다.
예를 들어 해당 기능을 통해 head 태그에 공통으로 사용되는 css, javascript 같은 정보들을 한 곳에 모아둘 수 있고, 각 페이지마다 개별로 적용할 css, javascript 정보를 더 추가해서 사용할 수도 있다.
// dependecy 추가
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
<!-- header.html -->
<th:block xmlns:th="http://www.thymeleaf.org" th:fragment="header">
<p>header</p>
</th:block>
<!-- footer.html -->
<th:block xmlns:th="http://www.thymeleaf.org" th:fragment="footer">
<p>footer</p>
</th:block>
<!-- defaultLayout.html -->
<!DOCTYPE html>
<html lagn="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8" />
<title>My Website</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- 공통 css -->
<link rel="stylesheet" th:href="@{/css/style.css}" />
<!-- 개별 css -->
<th:block layout:fragment="css"></th:block>
</head>
<body>
<!-- header -->
<th:block th:replace="fragments/header :: header"></th:block>
<!-- content -->
<th:block layout:fragment="content"></th:block>
<!-- footer -->
<th:block th:replace="fragments/footer :: footer"></th:block>
<!-- Bootstrap Js -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<!-- fontawesome Js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" integrity="sha512-fD9DI5bZwQxOi7MhYWnnNPlvXdp/2Pj3XSTRrFs5FQa4mizyGLnJcN6tuvUS6LbmgN1ut+XGSABKvjN0H6Aoow==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- 공통 js -->
<script type="text/javascript" th:src="@{/js/main.js}"></script>
<!-- 개별 js -->
<th:block layout:fragment="script"></th:block>
</body>
</html>
<!-- page.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layouts/defaultLayout}">
<!-- 사용자 CSS -->
<th:block layout:fragment="css"></th:block>
<!-- Content -->
<div layout:fragment="content">
<h1>Content</h1>
</div>
<!-- 사용자 스크립트 -->
<th:block layout:fragment="script"></th:block>
</html>
footer: template/fragment/footer.html 템플릿에 있는 th:fragment="footer"라는 부분을 템플릿 조각으로 가져와서 사용한다는 의미이다.<!-- div 태그 안에 지정한 조각이 추가된다 -->
<div th:insert="~{template/fragment/footer :: footer}"></div>
<!-- div 태그가 사라지고 지정한 조각으로 대체된다 -->
<div th:insert="~{template/fragment/footer :: footer}"></div>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
<footer th:fragment="copyParam (param1, param2)">
<p th:text="${param1}></p>
<p th:text="${param1}></p>
</footer>
참조
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2