Thymeleaf(타임리프)는 JSP와 같은 템플릿 엔진의 하나이다.
(서버 사이드) 템플릿 엔진은 HTML에 문서에 동적인 데이터가 들어가는 부분이 있을텐데 그걸 좀 더 쉽고 깔끔하게 표현할 수 있도록 돕고 이를 합성하여 출력하는 소프트웨어 또는 소프트웨어 컴포넌트이다.
공부하면서 등장하는 몇 가지 간단한 문법을 정리하고
밑에서부터는 더 심화된 내용을 정리해보려고 한다!
<html xmlns:th="http://www.thymeleaf.org">
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
th:text는 태그 안에 있는 내용을 서버에서 받은 데이터로 대체할 때 사용한다.
<td><a href="item.html" th:href="@{/basic/items/{itemId}
(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
저 회원id 부분에 서버로부터 받은 데이터 item.id를 넣을 것이다.
태그 속성이 아닌 HTML 영역 안에 Model 데이터 넣는 방법[[${}]]
<li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li>
th:value도 th:text와 같이 모델 데이터를 담고 화면에 뿌릴 때 렌더링
<input type="text" id="quantity" name="quantity" class="form-control"
value="10" th:value="${item.quantity}" readonly>
th:action 뒤에 URL이 없으면 현재 URL로 데이터를 보낸다.
th:action은 클라이언트로부터 받은 데이터를 서버로 보내는 역할을 한다.
<form action="item.html" th:action method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="formcontrol"
placeholder="이름을 입력하세요">
</div>
현재 URL은 http://localhost:8080/basic/items/add인데 이것은 GetMapping이고 같은 URL로 가되 PostMapping으로 가게 되어 다른 메서드를 실행한다.
지역변수 선언 th:with
Model에 users라는 데이터를 담았다고 가정했을 때
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
변수 표현식 ${}은 모델에 포함된 값이나 타임리프 변수로 선언한 값을 조회한다.
즉 ${item.id}의 의미는 item.getId()로 프로퍼티 접근법이다.
<!-- 타임리프 변수 itemId의 값을 조회 -->
<td><a href="item.html" th:href="@{/basic/items/{itemId}
(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
<!-- 프로퍼티 접근법도 사용됨 itemId=${item.id} -->
@GetMapping("/variable")
public String variable(Model model){
User userA = new User("userA",10);
User userB = new User("userB",20);
List<User> list = new ArrayList<>();
list.add(userA);
list.add(userB);
Map<String, User> map = new HashMap<>();
map.put("userA", userA);
map.put("userB", userB);
model.addAttribute("user",userA);
model.addAttribute("users",list);
model.addAttribute("userMap",map);
return "basic/variable";
}
<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>
List나 Map에 접근할 때 배열처럼 []를 사용해서 접근한다.Model에 "Hello <b>Spring!</b>"
을 넣는다는 것은 굵은 글씨로 Spring! 되기를 원한다는 것이다. 그럴려면 HTML은 <b>
의 '<'와 '>'를 태그의 시작과 끝으로 인식해야하지만 실제로는 HTML 엔티티에 의해서 '무엇보다 작다'와 '무엇보다 크다'로 변경한다. 이렇게 원래 특수한 문자를 HTML 엔티티로 변경하는 기능을 Escape라고 한다.
th:text,[[...]] 는 Escape 기능을 하기 때문에
<li>th:text = <span th:text="${data}"></span></li>
위와 같이 출력된다.
따라서 Unescape 기능을 하는 th:utext,[(...)]을 사용해야 한다.
<ul>
<li>th:text = <span th:text="${data}"></span></li>
<li>th:utext = <span th:utext="${data}"></span></li>
</ul>
<h1><span th:inline="none">[[...]] vs [(...)]</span></h1>
<ul>
<li><span th:inline="none">[[...]] = </span>[[${data}]]</li>
<li><span th:inline="none">[(...)] = </span>[(${data})]</li>
</ul>
추가로
th:inline="none"
는 [[...]],[(...)]를 타임리프가 해석하지 말라는 뜻 그냥 문자로 보라는 의미이다.${#request}, ${#response}, ${#session}, ${#servletContext} : 스프링 부트 3.0부터 제공하지 않는다. -> 그냥 model에 저장해서 불러와야 한다.
${#locale}
#어쩌구의 경우 객체 자체가 그대로 제공됨
예를 들어 #request의 경우 request.getParameter("data")로 해야 데이터 접근이 가능함 request 자체는 HttpServletRequest객체가 그대로 제공된다.
이에 대한 보완으로 편의 객체 제공
HTTP 요청 파라미터 접근 ${param.paramData}
HTTP 세션 접근 ${session.sessionData}
@GetMapping("/basic-objects")
public String basicObjects(HttpSession session) {
session.setAttribute("sessionData","Hello Session");
return "basic/basic-objects";
}
스프링 빈 접근 ${@helloBean.hello('Spring!')}
@Component("helloBean")
static class HelloBean{
public String hello(String data){
return "Hello"+data;
}
}
타임리프는 문자, 숫자, 날짜, URI 등을 편리하게 다루기 위한 다양한 유틸리티 객체들을 제공하며 표현식은 #{...}
그 중에서 자바8 날짜 서식을 지원하는 #temporals 유틸리티 객체를 살펴본다.
@GetMapping("/date")
public String date(Model model){
model.addAttribute("localDateTime", LocalDateTime.now());
return "basic/date";
}
model에 LocalDataTime.now()로 현재 날짜에 대한 데이터를 저장하여 뷰템플릿에 전달한다.
뷰템플릿에서는 #temporial를 이용해서 이 날짜에 대한 데이터에서 필요한 내용을 뽑아서 사용할 수 있다. (mysql의 date_format처럼)
<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>
타임리프를 통해서 URL를 넣을 때는
<li><a th:href="@{/hello}">basic url</a></li>
a 태그 안에 th:href="@{}"를 사용한다.
또한 타임리프는 쿼리 파라미터나 경로변수를 넣는 방법들을 제공하는데 그 방법에 대해서 알아보자
Controller에서 model에 쿼리파미터나 경로변수가 될 값들을 저장하고 뷰 템플릿에 보내자.
@GetMapping("/link")
public String link(Model model){
model.addAttribute("param1","data1");
model.addAttribute("param2","data2");
return "basic/link";
}
여기서 눈여겨 봐야할 문법은 쿼리 파라미터의 선언은 ()이고
경로변수에 대한 선언은 {}임을 기억하자
쿼리 파라미터 thymeleaf 문법
<li><a th:href="@{/hello(param1=${param1},param2=${param2})}">hello query param</a></li>
저 URL은 http://localhost:8080/hello?param1=data1¶m2=data2
이 될 것이다. 쿼리 파라미터를 넣을 때 ?의 역할과 그 경계를 타임리프는 ()를 통해서 나타낸다고 생각하자.
경로 변수에 대한 thymeleaf 문법
<li><a th:href="@{/hello/{param1}/{param2}(param1=${param1},param2=${param2})}">path variable</a></li>
URL은 http://localhost:8080/hello/data1/data2
이다.
이상하다 분명 ()를 통해서 쿼리 파라미터를 넣어주었다. 그러나 실제 URL에는 쿼리파라미터가 없다. 그 이유는 경로변수에 선언된 변수(param1, param2)는 쿼리 파라미터로 선언될 수 없기 때문이다. 이 부분을 기억하자!!
따라서 하나의 예시를 더 보자
<li><a th:href="@{/hello/{param1}(param1=${param1},param2=${param2})}">path variable + query parameter</a></li>
URL은 http://localhost:8080/hello/data1?param2=data2
이다.
경로변수에 param2가 없기 때문에 쿼리 파라미터로 param2가 선언되었다.
리터럴이란 소스 코드상 고정된 값
Thymeleaf의 리터럴을 표현할 때 문자는 무조건 작은 따옴표 '로 감싸줘야 한다.
그러나 공백 없이 쭉 이어진 문자는 하나의 토근으로 인식할 수 있다.
<!--가능-->
<span th:text="hello">
<!--불가능 : 공백이 있기 때문이다.-->
<span th:text="hello world!"></span>
<!--가능 : 공백이 있기 때문에 '으로 감싸준다.'-->
<span th:text="'hello world!'"></span>
Contoller를 통해서 model에 "data"의 변수에 "Spring!"라는 값을 넣어줬다고 가정한다.
<li>'hello '+ ${data}=<span th:text="'hello '+${data}"></span></li>
<li>리터럴 대체 |hello ${data}|=<span th:text="|hello ${data}|"></span></li>
보면 th:text="'hello '+${data}"
이 부분을 보자. 문자에 공백이 있어 ''로 감싸주었고 문자와 외부변수를 연결하기 위해서 +를 사용했다. 문법도 어렵고 보기에도 불편하다. 따라서 이를 보완한 리터럴 대체라는 ||라는 문법을 제공한다.
th:text="|hello ${data}|"
를 보면 ''와 + 없이 깔끔하게 사용이 가능하다.
타임리프를 이용해서 연산을 할 수 있다.
자바와 같이 사칙연산, %, ? A:B, 비교연산 등의 연산이 가능하다.
먼저 Controller에서 model에 어떤 값을 넣었는지 확인하자.
@GetMapping("/operation")
public String operation(Model model){
model.addAttribute("nullData",null);
model.addAttribute("data","Spring!");
return "basic/operation";
}
산술 연산
<li>10 + 2 = <span th:text="10 + 2"></span></li>
<li>10 % 2 == 0 = <span th:text="10 % 2 == 0"></span></li>
비교 연산
<ul>
<li>1 > 10 = <span th:text="1 > 10"></span></li>
<li>1 gt 10 = <span th:text="1 gt 10"></span></li>
<li>1 >= 10 = <span th:text="1 >= 10"></span></li>
<li>1 ge 10 = <span th:text="1 ge 10"></span></li>
<li>1 == 10 = <span th:text="1 == 10"></span></li>
<li>1 != 10 = <span th:text="1 != 10"></span></li>
</ul>
모두 가능한 표현이다. true 또는 false로 결과값이 나온다.
조건식
조건식도 사용이 가능하다.
<li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)?'짝수':'홀수'"></span></li>
Elvis 연산자
조금 생소한 이름이라서 찾아보았다.
엘비스 연산자는 ?:로 표현하며, ?:의 왼쪽 객체가 non-null이면 그 객체의 값이 리턴되고, null이라면 ?:의 오른쪽 값을 리턴합니다.
<li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가 없습니다.'"></span></li> <li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?:'데이터가 없습니다.'"></span></li>
No-Operation + Elvis 연산자
자바에 없는 개념이다.
""는 마치 타임리프가 실행되지 않은 것처럼 동작한다고 한다.
Elvis 연산자와 ""를 활용해서 어떤 결과가 나오는지 확인해보자
<li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</span></li>
<li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가 없습니다.</span></li>
보면 Elvis 연산자에 의해서 데이터가 있는 경우는 왼쪽이 실행된다.
하지만 데이터가 없는 경우 오른쪽에 있는 _가 실행되어서 마치 타임리프가 없는 것처럼 "데이터가 없습니다"가 출력된다.
기존 HTML의 속성을 대체하는 타임리프의 th:*
에 대해서 알아본다.
그 중에서 기존 HTML 속성 값에 내용을 추가하는 방법
<h1>속성의 값에 내용을 추가</h1>
-th:attrappend = <input type="text" class="text" th:attrappend="class='large'"/><br/>
<!-- 결과 : class="text large"-->
-th:attrprepend = <input type="text" class="text" th:attrprepend="class='large'"/><br/>
<!-- 결과 : class="large text"-->
-th:classappend = <input type="text" class = "text" th:classappend="large"/><br/>
<!-- 결과 : class="text large"-->
th:checked 속성
HTML의 checkbox,radio의 경우 checked라는 속성이 선언되면 속성 값이 true인지 false인지 상관없이 기본값으로 체크해버린다.
따라서 th:checked속성을 이용해서 HTML의 값을 덮어버리나 check유무를 조절할 수 있다.
- checked o <input type="checkbox" name="active" th:checked="true" />
- checked x <input type="checkbox" name = "active" th:checked="false"/><br/>
- checked=false <input type="checkbox" name="active" checked="false" /><br/>
<!--결과: checked 속성이 false라고 선언되었음에도 불구하고 체크해버린다-->
여기서 하나 주의할 것이 있는데 th: each
로 하면
An error happened during template parsing이라는 오류가 뜬다.
그러나 th:each 이렇게 공백 없이 작성해야 한다.
먼저 th:each라는 건 많이 보았기 때문에 익숙하지만 반복할 때의 상태를 확인할 수 있는 기능이 있다.
<tr th:each="user,userStat : ${users}">
위와 같이 반복문의 파라미터를 2개를 설정하면 두 번째 파라미터는 반복의 상태를 알려주는 속성이 된다.
또한 이 두 번째 파라미터를 생략이 가능하고 상태를 확인할 때는 (첫번째 파라미터)+Stat으로 선언하여 사용한다.
<h1>반복 상태 유지</h1>
<table border="1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
<th>etc</th>
</tr>
<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>
</table>
따라서 위 예시의 두 번째 파라미터 userStat도 생략이 가능하며 사용할 때는 첫번째 파라미터의 명과 Stat를 합쳐서 사용한다.
몇 가지 기능을 확인한다
-count : 0부터 시작
-index: 1부터 시작
-size: 반복 전체 횟수
-even: 저장된 객체의 값이 홀수인가
-odd: 저장된 객체의 값이 짝수인가
-first: 첫 번째 값인가
-last : 마지막 값인가
-current: 현재 객체에 저장된 값
조건에 부합하면 출력하는 것이다.
출력해야할 내용은 th:text에 담긴 것을 조건에 부합하면 출력한다.
<tr th:each="user,userStat : ${users}">
<td th:text="${userStat.count}">1</td>
<td th:text="${user.username}">username</td>
<td>
<span th:text="${user.age}">0</span>
<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
</td>
</tr>
switch의 경우 java 문법에도 등장하는 switch와 같은 기능을 한다.
마찬가지로 case와 짝궁이다.
<tr th:each="user,userStat : ${users}">
<td th:text="${userStat.count}">1</td>
<td th:text="${user.username}">username</td>
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="*">기타</span>
</td>
</tr>
아무런 case에도 해당되지 않는다라는 의미에서 *를 사용한다.
HTML 주석
<!--
<span th:text="${data}">html data</span>
-->
타임리프 파서 주석 = 타임리프의 진짜 주석= 렌더링 해도 안보임
<!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->
타임리프 프로토타입 주석
<!--/*/
<span th:text="${data}">html data</span>
/*/-->
HTML 파일 자체로 열면 주석처리 되어 보이지 않지만 서버를 통해서 열면 타임리프로 렌더링된다.
왜 있는 기능인지 잘 모르겠다
타임리프의 자체적인 태그가 있는데 그게 바로 th:block
이다.
타임리프가 제공하는 기능들은 HTML 태그 안에 속성으로 들어가게 되는데
그러지 못하는 상황에서는 타임리프의 자체 태그인 th:block
을 사용한다.
<th:block th:each="user:${users}">
<div>
사용자 이름1 <span th:text="${user.username}"></span>
사용자 이름2 <span th:text="${user.age}"></span>
</div>
<div>
요약 <span th:text="${user.username}+'/'+ ${user.age}"></span>
</div>
</th:block>
위와 같이 두개의 <div>
태그를 묶어줄 HTML의 자체 태그가 존재하지 않아 반복문을 실행할 수 없는 상황에서는 타임리프 자체 태그인 th:block
를 만들어서 th:each
를 속성으로 넣어주는 것이다.
보면 타임리프의 자체 태그이기 때문에 개발자 도구를 통해 실제 웹브라우저에 전송된 내용에 th:block
가 없음을 확인할 수 있다.
이 내용을 알기 전에 두 가지 개념을 알아야 한다.
<script>
태그 : HTML 내부에 자바스크립트와 같은 클라이언트 사이드 스크립트를 정의할 때 사용한다.
내추럴 템플릿(Natural Templete) : 타임리프는 순수 HTML을 유지하는 특성을 가지고 있다. 마치 브라우저에서 타임리프로 만든 html 파일을 파일경로로 오픈해도 html이 잘 나오는 것을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.
JSP를 포함한 다른 뷰 템플릿들은 해당 파일을 그래도 웹 브라우저에서 열어도 정상적인 HTML 결과를 확인할 수 없다. 오직 서버를 통해 jsp 화면이 렌더링 되고 응답 결과를 받아야 한다.
이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿으로도 사용할 수 있는 타임리프의 특징을 내추럴 템플릿이라고 한다. 출처: https://chinggin.tistory.com/589
타임리프는 자바스크립트에서 타임리프를 편리하게 사용할 수 있는 자바스크립트 인라인 기능 제공한다.
타임리프는 적용하기 전과 후를 비교해보자
<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>
렌더링 결과를 보자
var username ="userA"로 선언되어져야 하는데 타임리프의 인라인을 사용하지 않는경우
변수명이 그대로 와버려서 오류가 발생했다.
또한 타임리프의 내추럴 템플릿의 기능은 자바스크립트 인라인 기능을 통해서 사용할 수 있다. 주석을 활용해서 이 기능을 이용할 수 있다.
인라인 적용전에는 주석에 렌더링된 것으로 확인된다.
인라인 적용 후에는 이 주석 부분이 제거되어 렌더링 된 것을 확인할 수 있다.
객체를 가져올 때 인라인 기능을 사용하면 JSON의 형태로 객체를 변환해서 가져온다.
<script th:inline="javascript">
[# th:each="user,stat:${users}"]
var user[[${stat.count}]] = [[${user}]]
[/]
</script>
신기하다 변수명 자체에도 들어가서 사용이 가능하다니
다음에 꼭 이 기능을 사용하는 서비스를 만들어보고 싶다.
말 그대로 템플릿을 조각내서 가져다가 쓰는것
전체적인 흐름은 아래와 같다.
1. th:fragment
속성으로 태그 묶음에 이름을 붙여준다.
2. 조각난 템플릿 조각을 가져올 때는 th:inser
나 th:replace
를 이용하는데 조각난 템플릿의 경로와 이름을 넣어 가져온다.
3. "~{template/fragment/footer ::copy}"
{경로 :: 묶음 이름}
<!-- 조각 1-->
<footer th:fragment="copy">
푸터 자리 입니다.
</footer>
<!-- 조각2 -->
<footer th:fragment="copyParam (param1, param2)">
<p>파라미터 자리 입니다.</p>
<p th:text="${param1}"></p>
<p th:text="${param2}"></p>
</footer>
(참고로 예시의 <footer>
태그는 웹페이지의 하단 영역을 표현할 때 사용하는 태그이다.)
<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>
th:insert는 가져온 템플릿 조각을 <div>
태그 안에 넣는 것이고
th:replace는 가져온 템플릿 조각이 <div>
를 대체한다고 보면 된다.
또한 쿼리 파라미터처럼 조각 난 템플릿을 불러올 때 쿼리를 넣고 불러올 때 그 값에 렌더링해서 가져온다.
<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1','데이터2')}"></div>
앞서 배운 템플릿 조각은 공통적으로 사용되는 조각들을 나누어서 필요한 곳이 있다면, 곳곳에 사용한 느낌이라면
이번에는 정해진 레이아웃, 그 위치에 태그들을 끼워맞추는 방식으로 진행된다.
이 예시는 <head>
태그 자체를 끼워 맞출 것이다.
코드를 보면 저 <head>
태그는 template/layout/base 경로에 있는 th:fragment="common_header (title,link)"가 붙은 <head>
로 완전히 대체될 것이다.
그러나 대체될 때 현 <head>
안에 있는 <title>
태그와 <link>
태그는 "common_header (title,link)"에 파라미터 형식으로 값을 전달해주고 그 값이 채워진 형태로 대체될 것이다.
이 뭉텅이 자체가 저 위에 있는 html 파일 head로 대체된다. 대체될 때 저 파란색으로 표시된 부분은 파라미터로 선언되어져 있고 앞서 말했듯이
대체될 때 현
<head>
안에 있는<title>
태그와<link>
태그는 "common_header (title,link)"에 파라미터 형식으로 값을 전달해주고 그 값이 채워진 형태로 대체될 것이다.
이 부분을 지키며 대체된다.
앞서 표현한 뭉텅이가 대체되었고 파란색 형광펜 부분도 대체되었다!
비슷하게 <html>
태그 자체도 대체할 수 있다!