서버 사이드 렌더링(SSR) - 백엔드 서버에서 HTML을 동적으로 렌더링하는 용도로 사용한다.
네츄럴 템플릿이다. 즉 순수한 HTML을 최대한 유지한다. 웹 브라우저에서 HTML 파일을 직접 열어도 내용을 확인 할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변환된 결과를 확인 할 수 있다.
JSP를 포함한 다른 뷰 템플릿들은 해당 파일을 열면 웹 브라우저에서 정상적인 HTML 결과를 확인 할 수 없다. 오직 서버를 통해서 템플릿이 렌더링 되고 HTML 응답 결과를 받아야 화면을 확인.
스프링 통합 지원
타임 리프는 스프링에서 추천하는 뷰 템플릿이다. 따라서 스프링의 다양한 기능을 편리하게 사용 할 수 있게 지원한다.
타임리프 선언
<html xmlns:th="http://www.thymeleaf.org">
타임리프롤 사용하려면 html에 위와 같은 타임리프 사용 선언을 해줘야 한다.
<span th:text="${data}">data로변환</span>
<span>컨텐츠 안에서 직접 출력 = [[${data}]]</span>
이스케이프
HTML 문서는 <,>
같은 특수문자 사용에 조심해야된다.
<, >
는 <, >
로 변환된다.
<b>강조</b>
를 사용한다면 의도한것은 강조라는 부분이 볼드가 되어 나오기를 원했을것이다. 하지만 실제 HTML 화면에는 그대로 나온다.
HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 이스케이프라고 한다. 타임리프가 제공하는 th:text
, [[...]]
는 기본적으로 이스케이프(escape)를 제공 한다.
왜 이스케이프를 제공할까?
만약 이스케이프를 제공하지 않고, 그냥 태그상태로 들어오면 큰 문제가 생길 수 있다. HTML 태그로 인식되서 화면에 어떤 것이 생길지모른다.
th:utext
, [(...)]
는 기본적으로 이스케이프를 제공하지 않는다.
기본적으로 th:text를 사용하면 되고, 꼭 필요할때만 unescape를 사용하라.
타임리프에서 변수를 사용할 때는 변수 표현식을 사용한다. SpringEL이라는 스프링이 제공하는 변수식도 있다.
//model에서 user를 그냥 보내줬을경우
<span th:text="${user.username}"></span>
<span th:text="${user['username']}"></span>
<span th:text="${user.getUsername()}"></span>
//리스트로 보내줬을 경우 인덱스로 지정가능.
<span th:text="${users[0].username}"></span>
<span th:text="${users[0]['username']}"></span>
<span th:text="${users[0].getUsername()}"></span>
//맵으로 보내줬을 경우 키로 꺼내야된다.
<span th:text="${userMap['userA'].username}"></span>
<span th:text="${userMap['userA']['username']}"></span>
<span th:text="${userMap['userA'].getUsername()}"></span>
위의 출력 결과는 저부 같음. 보통 첫번째 방법이 제일 편하고 쓰기 쉬운 편이다.
지역 변수 - th:with
<div th:with="first=${users[0]}">
<span th:text="${first.usernname}"></span>
</div>
선언한 Scope안에서만 활용할 수 있다. 여기선 div의 scope에서만 사용 가능
타임리프는 기본 객체들을 제공한다
스프링 부트 3.0이라면 직접 model 에 해당 객체를 추가해서 사용해야 한다.
스프링 부트 3.0 미만에서 사용법
<span th:text="${#request}"></span>
<span th:text="${#response}"></span>
<span th:text="${#session}"></span>
<span th:text="${#servletContext}"></span>
<span th:text="${#locale}"></span>
편의객체
// java class
@GetMapping("/basic-objects")
public String basicObjects(HttpSession session) {
session.setAttribute("sessionData", "Hello Session");
return "basic/basic-objects";
}
@Component("helloBean")
static class HelloBean {
public String hello(String data) {
return "Hello " + data;
}
}
//html
편의 객체
//요청 파라미터 접근: param , ?paramData=data를 바로 꺼내씀
<span th:text="${param.paramData}"></span>
//HTTP 세션 접근 : 클래스에 sessionData를 받아오는 것.
<span th:text="${session.sessionData}"></span>
//Spring Bean에 접근 : @helloBean의 빈에 hello 메서드에 'Spring'을 data로 넣어서 리턴
<span th:text="${@helloBean.hello('Spring!')}"></span>
스프링 부트 3.0 이상이라면
@GetMapping("/basic-objects")
public String basicObjects(Model model, HttpServletRequest request,
HttpServletResponse response, HttpSession session) {
session.setAttribute("sessionData", "Hello Session");
model.addAttribute("request", request);
model.addAttribute("response", response);
model.addAttribute("servletContext", request.getServletContext());
return "basic/basic-objects";
}
위 처럼 request, response, servletContext등을 전부 넣어서 보내줘야 된다.(더 불편해졌네..)
이것 말고도 더 있음. 다만 주요 사용되는 것들은 이런것들.
URL 링크
<ul>
/hello
<li><a th:href="@{/hello}">basic url</a></li>
/hello?param1=${param1}&pram2=${param2}
<li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">hello query param</a></li>
/hello/{param1}/{param2}
<li><a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=$ {param2})}">path variable</a></li>
/hello/{param1}?param2={param2}
<li><a th:href="@{/hello/{param1}(param1=${param1}, param2=$ {param2})}">path variable + query parameter</a></li>
</ul>
Literals - 리터럴은 소스 코드상에 고정된 값을 말하는 용어이다.
타임리프는 다음과 같은 리터럴이 있다.
타임리프의 연산은 자바와 크게 다르지 않다. 다만 HTML안에 사용하는 것이기에 HTML 태그를 주의하자
산술 연산
<span th:text="10 + 2"></span>
<span th:text="10 % 2 == 0"></span>
비교 연산
<span th:text="1 > 10"></span> > >
<span th:text="1 gt 10"></span> > gt
<span th:text="1 >= 10"></span>
<span th:text="1 ge 10"></span> >= ge
<span th:text="1 == 10"></span>
<span th:text="1 != 10"></span>
조건식
<span th:text="(10 % 2 == 0)? '짝수':'홀수'"></span>
Elvis 연산자
<span th:text="${data}?: '데이터가 없습니다.'"></span> ${data}가 있을시, 없을시 데이터가 없습니다
<span th:text="${nullData}?:'데이터가 없습니다.'"></span>
<span th:text="${data}?: _">데이터가 없습니다.</span> ${data}가 없을 시 아무것도 안함.(기존의 text출력)
<span th:text="${nullData}?: _">데이터가 없습니다.</span>
타임태그는 주로 HTML태그에 th:*
속성을 지정하는 방식으로 동작한다. th:*
로 속성을 지정하면 기존 속성을 대체한다. 기존 속성이 없으면 새로 만든다.
속성값 대체
<input type="text" name="mock" th:name="userA">
속성 추가
//띄어쓰기 신경써야됨 append 뒤에 붙임
<input type="text" class="text" th:attrappend="class=' large'" />
//띄어쓰기 신경 prepend 앞에 붙임
<input type="text" class="text" th:attrprepend="class='large '" />
//class 이름의 경우 띄어쓰기 신경쓰기 귀찮음. classappend를 쓰면 자동으로 클래스 이름 구분해서 붙여줌
<input type="text" class="text" th:classappend="large" />
체크 처리
- checked o <input type="checkbox" name="active" th:checked="true" />
- checked x <input type="checkbox" name="active" th:checked="false" />
- checked=false <input type="checkbox" name="active" checked="false" />
// checked="false"라고 해도 checked 속성이 있는것으로 판단됨.
// 타임리프의 경우 th:checked="false" 의경우 checked 속성이 없다.
타임리프에서의 반복은 th:each
를 사용하면 된다.
<tr th:each="user : ${users}">
<td th:text="${user.username}">username</td>
<td th:text="${user.age}">0</td>
</tr>
th:each
는 List 뿐만 아니라 배열, java.util.Iterable , java.util.Enumeration 을 구현한 모든 객체를 반복에 사용할 수 있습니다. Map 도 사용할 수 있는데 이 경우 변수에 담기는 값은 Map.Entry 입니다.
반복 상태 유지
<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)는 생략 가능한데, 생략하면 지정한 변수명(user)+(Stat)이 됩니다.
즉 파라미터를 생략해도 userStat으로 사용 가능.
if
, unless
라는 조건문을 제공한다. switch - case
문도 제공
<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>
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="*">기타</span>
</td>
<span>...<span>
부분 자체가 렌더링 되지 않고 사라진다.<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
*
은 만족하는 조건이 없을 때 사용하는 디폴트이다.<!-- 표준 HTML 주석 -->
<!--/* Tag */-->
<!--/*-->
Tag
<!--*/-->
타임리프 프로토타입 주석
HTML 파일을 그대로 열면 주석으로 보이는데, 타임리프 랜더링을 거치면 주석이 사라지고 안에 내용이 보임.
<!--/*/
Tag
/*/-->
<th:block>은 HTML이 아닌 타임리프가 제공하는 태그
<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>
블록 내부의 것을 반복. 타임리프의 특성상 HTML 태그안에 속성으로 기능을 정의해서 사용하는데, 그렇게 사용하기 애매한 경우에 사용하면 된다. th:block은 렌더링시 제거된다.
자바스크립트에서 타임리프를 편리하게 사용할 수 있는 자바스크립트 인라인 기능을 제공한다.
<script th:inline="javascript">
<!-- 자바스크립트 인라인 사용 전 -->
<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; 변수명으로 들어감
인라인 사용 후 var username = "userA"; 문자열로 들어감
내추럴 템플릿
인라인 사용 전 var username2 = /*[[${user.username}]]*/ "test username";
그대로 나옴
인라인 사용 후 var username2 = /*[[${user.username}]]*/ "test username";
user.useranme이 나옴.
객체
인라인 사용 전 var user = [[${user}]]; - toString() 호출
인라인 사용 후 var user = [[${user}]]; - JSON으로 변환해줌.
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
타임리프의 방식을 그대로 사용 가능해진다.
웹 페이지를 개발할 때는 공통 영역이 많이 있다. 상단 영역, 하단 영역, 좌측 카테고리 등등 여러 페이지에서 함께 사용하는 영역들이 있다. 이런 부분들을 코드 복사해서 사용한다면 변경시 여러 페이지를 다 수정해야 하므로 비효율적이다. 이런 문제를 해결하기 위해 템플릿 조각과 레이아웃 기능을 지원한다.
우선 템플릿 조각을 사용해보자
footer.html
같은 하단 영역을 만들어둔다.
<footer th:fragment="copy">
푸터 자리 입니다.
</footer>
<footer th:fragment="copyParam (param1, param2)">
<p>파라미터 자리 입니다.</p>
<p th:text="${param1}"></p>
<p th:text="${param2}"></p>
</footer>
fragment는 사용되는 조각이라 보면 된다.
<div> 태그 안에 들어가는 insert
<div th:insert="~{template/fragment/footer :: copy}"></div>
아예 태그 자체가 교체되버리는 replcae
<div th:replace="~{template/fragment/footer :: copy}"></div>
replace 단순 표현식(복잡해지면 사용 어려움)
<div th:replace="template/fragment/footer :: copy"></div>
파라미터 사용
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
~{경로 :: fragment 이름(pram..)}
템플릿 조각은 일부 코드를 조각해서 가지고왔다면, 코드 조각을 레이아웃에 넘겨서 사용하는 방법을 알아보자.
큰 모양이 있고 내 코드를 조각에 맞춰 넣는다고 생각하면 된다.
base.html
<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>
"common_header(title,links)"
이 핵심이다. layout.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>
~{::title}, ~{::link}
title과 link는 base.html에 보내져 replace 된다.위의 개념을 더 확장해 Header 뿐만 아니라 HTML 전체를 바꿀수도 있다.
<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<title th:replace="${title}">레이아웃 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}">
<p>레이아웃 컨텐츠</p>
</div>
<footer>
레이아웃 푸터
</footer>
</body>
</html>
타이틀과 컨텐츠 쪽만 바꾸도록 해놓음.
<!DOCTYPE html>
<html th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title},~{::section})}"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
</body>
</html>
html에 th:replace가 붙어 있는 것이 보일것이다. 전부 교체된다는 뜻이다. 그때, title 과section은 파라미터로 넘겨준다는 뜻이다.
참조 : 김영한님의 스프링 강의를 공부하며 정리한 것입니다.