[Thymeleaf] 타임리프, 기본적인 기능 정리

https://www.thymeleaf.org/
기본 기능: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
공식 메뉴얼 - 스프링 통합: https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

 

 

- 타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용하는 템플릿 엔진이다.

다른 대표적인 SSR(서버사이드 렌더링) 언어인 jsp보다 더 스프링 친화적이기도 하고 jsp와 달리 순수 HTML형태를 유지하기 때문에 서버를 거치지 않고도 동적으로 렌더링된 결과를 확인하기에 좋다.

 

 

- 기본적으로 타임리프는 기존 HTML태그에 th:*속성을 지정하는 방식으로 동작한다. th:*와 같이 속성을 지정하면 해당 속성이 기존 속성을 대체하고, 기존 속성이 존재하지 않으면 새로 만든다.

<input type="text" name="n" th:name="thn" />

위와 같이 HTML 코드를 입력하면

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

브라우저에서 렌더링되면 이렇게 변환된다.

 

- 웹 브라우저는 th 속성을 모른다. 타임리프가 처리해주는 것으로, 파일을 직접 열면 th속성은 동작하지 않음

 

 

 

사용 선언

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

타임리프를 사용 선언하기 위해서는 html태그에 위와 같이 th="" 와같이 선언을 해주면 된다.

<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>

 

 

 

텍스트 출력

@GetMapping("url")
public String text(Model model) {
    model.addAttribute("data", "Hello Spring!");
    return "htmlFile";
}
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

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

</body>
</html>

addAttribute로 넘어간 데이터를 타임리프를 이용하여 출력하기 위해서는

1. th:text="" 와 ${}를 이용하여 데이터를 넘겨받거나,
2. 대괄호 2개 [[]] 안에 ${}를 이용하여 데이터를 직접 받아서 출력할 수 있다.

주의할 점은 HTML은 <,>와 같은 특수문자를 기반으로 정의된다는 점이다. 따라서 뷰 템플릿으로 HTML화면을 생성할 때는 데이터에 이러한 특수문자가 들어갈 때 웹 브라우저는 특수문자를 문자(HTML의 엔티티)로 자동으로 변경(escape)해 준다. 따라서 데이터를 HTML에 들어갈 것을 고려해서 태그를 넣어 보낸다고 실제 HTML에서 태그가 작동하지는 않는다.

 

이러한 escape기능이 작동하지 않도록 하려면

1. th:utext=""를 이용하거나,
2. 대괄호와 소괄호 [()]를 이용하면 escape기능이 작동하지 않게 된다.

 

 

 

 

변수

타임리프에서 변수를 사용할 때는 변수 표현식(${...})을 사용한다.

@GetMapping("url")
public String textBasic(Model model) {

    Car carA = new Car();
    List<Car> list = new ArrayList<>();
    list.add(carA);
    Map<String, Car> map = new HashMap<>();
    map.put("carA", carA);

    model.addAttribute("car", carA);
    model.addAttribute("cars", list);
    model.addAttribute("carMap", map);    
    
    return "htmlFile";
}

위와 같이 컨트롤러에서 addAttribute로 다양한 객체들을 넘길 수 있는데, 타임리프에서 이렇게 받은 객체들을 사용할 수 있다.

${car.carname}
${cars[0].carname}
${carMap['carA'].carname}

 

 

 

 

기본 식 객체

타임리프를 이용하면 request/response, 세션 등의 자주 사용하는 기본 객체를 아래와 같이 바로 접근할 수 있다.

${#request}
${#response}
${#session}
${#servletContext}
${#locale}

기본 객체 뿐만 아니라 요청 파라미터나 세션으로 넘어온 데이터, 스프링 빈에 직접 접근할 수 있다. 

${param.paramData}
${session.sessionData}
${@myBean.func('data')}

또한 숫자, 문자, 배열 등을 편리하게 다룰수 있는 유틸리티 객체들도 제공한다.

 https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#appendix-b-expression << 필요할때 들어가서 ctrl+f 'utility' 참고

 

 

 

링크

타임리프에서 URL을 생성할 때는 다음과 같은 문법을 사용하면 된다.

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

기본적인 문법에 더해서 매개변수를 이용해서 사용할 수도 있다.

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

1. 기초적인 사용법

2. model을 이용해서 받은 데이터를 쿼리 파라미터로 활용하기 - () 안에 변수 설정하고 경로에 {}추가X

3. 받은 데이터로 경로 설정 - () 안에 변수 설정하고 경로에 {}로 추가

4. 받은 데이터로 경로 설정 + 쿼리 파라미터로 활용 - () 안에 변수 설정하고 경로에 {}추가하지 않으면 쿼리 파라미터로 전달된다.

> 경로에 추가하고 파라미터로도 전달하는, 동일한 변수를 이중으로 사용하는 것은 지원하지 않는다고 한다.

 

 

 

리터럴

문자, 숫자, boolean, null값을 리터럴로 지정할 수 있다. 타임리프에서 리터럴 표현을 위해서는 항상 ' '로 감싸주어야 한다. (th:text="text"와 같이 사용해서는 안된다)

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

기본적으로 아래와 같이 변수로 리터럴 치환이 가능한데, || 사이에 문자를 넣으면 ${} 변수를 넘겨받아서 리터럴로 자동 치환해준다. (변수를 사용하지 않더라도 ''를 안쓰고 ||으로 통일해도 되긴 한다)

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

 

 

연산

자바의 연산과 크게 다르지 않다. HTML안에서 사용하기 때문에 HTML엔티티를 사용하는 부분을 조심하자.

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

비교연산, 조건식, elvis연산자(삼항연산자) 등의 연산을 활용 가능하다.

 

 

 

타임리프 태그 속성

기본적으로 th:*와 같이 속성을 지정하면 타임리프는 기존 html 속성을 타임리프로 설정한 속성으로 대체한다. 이 때 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/>

또한 HTML의 checked와 다르게 th:checked옵션을 사용하여 checkbox를 만들면 값이 false인 경우 checked속성 자체를 제거한다(HTML의 checked는 값이 false이던 true이던 어떤 값만 가지면 체크된다)

 

 

반복

타임리프에서 반복은 th:each를 사용하며, 반복에서 사용할 수 있는 여러 상태값이 있다.

@GetMapping("/each")
public String each(Model model) {
	addUsers(model);
	return "basic/each";
}
private void addUsers(Model model) {
	List<User> list = new ArrayList<>();
	list.add(new User("userA", 10));
	list.add(new User("userB", 20));
	list.add(new User("userC", 30));
	model.addAttribute("users", list);
}
<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태그에서 ${users}의 값을 하나씩 꺼내서 왼쪽 변수 user에 담아서 태그를 반복 실행한다.

th:each는 리스트뿐 아니라 Iterable가능한 모든 자바 객체를 반복에 사용할 수 있다. 또한 반복의 두번째 파라미터(userStat)을 이용하여 반복의 상태(index, count, size, even, odd, first, last, current)를 확인할 수도 있다.

 

 

조건식

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

위와 같이 사용하면 addAttribute로 넘겨준 user의 age가 1. 20 미만일경우 미성년자 표시, 2. 20 이상이 아닐경우 미성년자 표시. (if와 unless를 이용하여 동일한 의미를 나타내었다)

<div th:if="${errors?.containsKey('globalError')}">
    <p th:class="field-error" th:text="${errors['globalError']}">전체 오류 메세지</p>
</div>

위처럼 ${객체?.속성}과 같이 사용하면 객체가 null일 경우 속성에 접근하지 않고 종료. 사실상 삼항 연산자인데 ':'부분이 생략될 수 있다고 한다. (https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#conditional-expressions)

 

블록

<th:block>은 HTML태그가 아닌 타임리프만의 유일한 자체 태그이다. block은 주로 th:each만으로 해결이 어려울 경우 부차적으로 사용한다.

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

th:each태그만 이용하면 두개의 <div></div> 안에 넣고 사용할 수 없기 때문에 th:block을 이용하여 두개의 <div></div>태그가 반복되도록 하였다.

 

 

템플릿 조각

웹 페이지에는 상/하단 영역이나 카테고리 같은 공통 영역이 많이 있는데., 이런한 부분을 코드를 복사해서 사용한다면 유지보수에 어려움이 있을 수 있으므로 비효율적이다. 타임리프는 이러한 문제를 해결하기 위해 템플릿 조각이라는 기능을 지원한다.

<footer th:fragment="copy">
	FOOTER
</footer>

<footer th:fragment="copyParam (param1, param2)">
	<p>PARAMETER</p>
	<p th:text="${param1}"></p>
	<p th:text="${param2}"></p>
</footer>
<div th:insert="~{template/fragment/footer :: copy}"></div>
<div th:replace="~{template/fragment/footer :: copy}"></div>
<div th:replace="template/fragment/footer :: copy"></div>

div태그를 사용하면 template/fragment/footer의 th:fragment - th:replace태그가 붙은<footer></footer>태그의 내용을 끌어와서 사용할 수 있다. (th:fragment가 붙은 태그는 다른곳에 포함될(넘겨질) 코드 조각으로 이해하면 된다)

- insert: div태그 유지

- replace: 기존 태그 대체

 

 

템플릿 레이아웃

기존에는 일부 코드 조각을 갖고와서 사용했다면, 코드 조각을 레이아웃에 넘겨서 사용할 수도 있다.

<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
    <title>MAIN TITLE</title>
    <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
    <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>

위 코드는 <head>에서 공통으로 사용하는 css, javascript같은 정보들이 있는데, 이러한 공통정보들을 한 곳에 모아두고 관리하기 위해서 이런 식으로 관리한다.

 

 

 

정리

기본적으로 라이브러리인 만큼 홈페이지에서 필요할 때 찾아쓰면서 익숙해지도록 적응하면 된다.