https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
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
Thymeleaf 특징
- 서버 사이드 HTML 렌더링 : 백엔드 서버에서 HTML을 동적으로 렌더링
- 내추럴 템플릿 : 순수 HTML을 최대한 유지하면서 뷰 템플릿의 역할도 할 수 있음, Thymeleaf로 작성한 HTML 파일은 그냥 브라우저로 열어도 내용을 확인할 수 있음(JSP는 불가능), 서버의 렌더링을 통해서 동적으로 변경된 결과물을 얻을 수도 있음
- 스프링 지원 : 스프링과 잘 연동됨, 스프링과 연동된 편리한 기능들 보유
Thymeleaf 선언
<html xmlns:th="http://www.thymeleaf.org">
- Thymeleaf는 기본적으로 HTML의 태그에 속성으로 들어가서 동작함
텍스트
- th:text, th:utext, [[${data}]], [(${data})]
- HTML의 콘텐츠 영역에 데이터를 출력
model.addAttribute("data", "Hello!!");
<p th:text="${data}"></p>
<li>[[${data}]]</li>
=> model에 들어있는 data를 가져와 출력하기
Escape
- HTML 문서는 <, > 같은 특수문자로 이루어져 있음
model.addAttribute("data", "Hello <b>Spring!</b>");
<li th:text="${data}"></li>
<li>Hello <b>Spring!</b></li>
- Hello Spring! 를 원했는데, 브라우저에는 Hello <b>Spring!</b> 로 나옴
- <, > 가 escape처리가 되어서 <, > 로 바뀌어서 <b>의 역할(굵은 글씨)을 수행 못함
- th:text, [[...]] 은 기본적으로 escape를 제공함
HTML 엔티티
- 브라우저는 <를 HTML 태그의 시작으로 인식함, <를 태그의 시작이 아닌 그냥 문자로 인식시키려면 unescape가 필요함( th:utext, [(${data})] )
<li th:utext="${data}"></li>
<li>[(${data})]</li>
=> 기본적으로 escape를 사용하고, 필요한 경우에만 unescape를 사용하자
SpringEL
- 변수 표현식 : ${...}
@GetMapping("/variable")
public String variable(Model model) {
User userA = new User("userA", 22);
User userB = new User("userB", 26);
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";
}
@Data
static class User {
private String username;
private int age;
public User(String username, int age) {
this.username = username;
this.age = age;
}
}
<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>
지역변수 : th:with
<div th:with="first=${users[0]}">
<p>첫번째 사람 : <span th:text="${first.username}"></span></p>
</div>
=> users중 첫 번째 User객체를 first로 지정해서 사용
기본 객체
${#request} : HttpServletRequest
${#response}
${#session}
${#servletContext}
${#locale}
- 클라이언트 요청 파라미터는 따로 Controller에서 다시 model로 넘길 필요 없이 param을 사용 하서 간편하게 접근이 가능함 ( ${param.파라미터명} )
- 요청 : http://localhost:8080/basic/basic-objects?paramData=Hello
<span th:text="${param.paramData}"></span>
@GetMapping("/basic-objects")
public String basicObject(HttpSession session) {
session.setAttribute("sessionData", "Hello Session");
return "basic/basic-objects";
}
@Component("helloBean")
static class Hello {
public String hello(String data) {
return "Hello" + data;
}
}
<li>Request Parameter = <span th:text="${param.paramData}"></span></li>
<li>session = <span th:text="${session.sessionData}"></span></li>
<li>spring bean = <span th:text="${@helloBean.hello('Spring!')}"></span></li>
=> ${param.~~} : 요청 파라미터 접근
=> ${session.~~} : HTTP 세션 접근
=> ${@helloBean.hello('Spring!')} : 스프링 빈 접근
URL 링크
- URL 링크 생성 : @{...}
@GetMapping("/link")
public String link(Model model) {
model.addAttribute("param1", "data1");
model.addAttribute("param2", "data2");
return "basic/link";
}
<ul>
<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>
</ul>
=> 결과
<ul>
<li><a href="/hello">basic url</a></li>
<li><a href="/hello?param1=data1&param2=data2">hello query param</a></li>
<li><a href="/hello/data1/data2">path variable</a></li>
<li><a href="/hello/data1?param2=data2">path variable + query parameter</a></li>
</ul>
1) 단순 URL
2) 쿼리 파라미터 생성 : (..)에 있는 부분은 쿼리 파라미터로 생성됨
3) PathVariable 생성 : URL 경로상에 변수가 있으면 (..)에 있는 부분은 PathVariable로 생성됨
4) 쿼리 파라미터 + PathVariable 생성
리터럴
- 리터럴은 소스 코드상에 고정된 값
- 문자 리터럴은 항상 ' 로 감싸야함
<span th:text="'hello'"></span>
- 항상 ' 로 감싸는 건 귀찮으므로, 공백 없이 이어진다면 '를 생략 가능
<span th:text="hello"></span>
- 공백이 있으면 '를 작성해야 함
<span th:text="'hello world!'"></span>
리터럴 대체
- |...|
<span th:text="'hello ' + ${data}"> <!-- 이거 대신 -->
<span th:text="|hello ${data}|"></span> <!-- 리터럴 대체 사용하는 것이 간편 -->
속성 값 설정(대체)
- th:name, th:classappend, th:checked="false"
- Thymeleaf는 보통 HTML 태그에 th:*라는 속성을 지정해서 동작하는데, th:*를 작성하면 기존의 속성을 대체해 버림
<body>
<h1>속성 설정</h1>
<input type="text" name="mock" th:name="userA" />
<h1>속성 추가</h1>
- 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/>
<h1>checked 처리</h1>
- checked o <input type="checkbox" name="active" th:checked="true" /><br/>
- checked x <input type="checkbox" name="active" th:checked="false" /><br/>
- checked=false <input type="checkbox" name="active" checked="false" /><br/>
</body>
=> 결과
<body>
<h1>속성 설정</h1>
<input type="text" name="userA" />
<h1>속성 추가</h1>
- th:attrappend = <input type="text" class="text large" /><br/>
- th:attrprepend = <input type="text" class="large text" /><br/>
- th:classappend = <input type="text" class="text large" /><br/>
<h1>checked 처리</h1>
- checked o <input type="checkbox" name="active" checked="checked" /><br/>
- checked x <input type="checkbox" name="active" /><br/>
- checked=false <input type="checkbox" name="active" checked="false" /><br/>
</body>
=> th:name="userA"가 기존의 name="mock"을 대체
=> large라는 클래스 속성 값을 추가, attrappend, attrprepend는 앞뒤로 띄어쓰기를 작성해줘야 함, classappend는 알아서 붙임
=> th:checked="false"로 checked 속성을 제거함 (HTML <input type="checkbox>는 checked라는 속성만 있으면 true/false 관계없이 checked 되어있음)
반복
- th:each
@GetMapping("/each")
public String each(Model model) {
addUser(model);
return "basic/each";
}
private void addUser(Model model) {
ArrayList<User> list = new ArrayList<>();
list.add(new User("aaa", 12));
list.add(new User("bbb", 22));
list.add(new User("ccc", 32));
model.addAttribute("users", list);
}
<body>
<h1>기본 테이블</h1>
<table border="1">
<tr>
<th>username</th>
<th>age</th>
</tr>
<tr th:each="user : ${users}">
<td th:text="${user.username}">username</td>
<td th:text="${user.age}">0</td>
</tr>
</table>
=> ${users}의 값을 하나씩 꺼내서 user에 담는 동작을 반복
<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으로 사용하면 됨
- index : 0부터 시작하는 값
- count : 1부터 시작하는 값
- size : 전체 사이즈
- even, odd : 홀수, 짝수 여부( boolean ) (count 기준으로)
- first, last : 처음, 마지막 여부( boolean )
- current : 현재 객체
조건부 평가
- Thymeleaf의 조건식 : th:if, th:unless, th:switch + th:case
@GetMapping("/condition")
public String condition(Model model) {
addUser(model);
return "basic/condition";
}
private void addUser(Model model) {
ArrayList<User> list = new ArrayList<>();
list.add(new User("aaa", 12));
list.add(new User("bbb", 22));
list.add(new User("ccc", 32));
model.addAttribute("users", list);
}
<tr th:each="user : ${users}">
<td>
<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
</td>
</tr>
=> 조건에 맞지 않으면 아예 해당 태그는 무효화됨
<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>
=> switch-case로 선택적으로 렌더링 가능, * 는 java의 default와 동일
블록
- th:block
- HTML 태그의 속성에 사용되지 않는 Thymeleaf 자체 태그
- 일반적인 반복이 어려운 경우 사용
<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>
자바스크립트 인라인
- <script th:inline="javascript"> : Javascript에 Thymeleaf를 적용하여 넘어온 값을 편리하게 사용할 수 있도록 도움
<script th:inline="javascript">
@GetMapping("/javascript")
public String javascript(Model model) {
model.addAttribute("user", new User("userA", 23));
addUser(model);
return "basic/javascript";
}
private void addUser(Model model) {
ArrayList<User> list = new ArrayList<>();
list.add(new User("aaa", 12));
list.add(new User("bbb", 22));
list.add(new User("ccc", 32));
model.addAttribute("users", list);
}
<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 = 23;
//자바스크립트 내추럴 템플릿
var username2 = /*userA*/ "test username";
//객체
var user = BasicController.User(username=userA, age=23);
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script>
var username = "userA";
var age = 23;
//자바스크립트 내추럴 템플릿
var username2 = "userA";
//객체
var user = {"username":"userA","age":23};
</script>
=> 사용 전 : userA에 " 가 없어서 오류가 발생함 > var username = "[[${user.username}]]"; 직접 " 로 감싸줘야 하는 번거로움
=> 사용 후 : Thymeleaf가 알아서 문자 타입으로 인식하여 " 를 포함시켜 줌, javascript에서 문제가 될 수 있는 문자는 escape 처리도 해줌
model에는 user라는 이름으로 User객체가 담겨있는데,
=> 사용 전 : 그냥 toString으로 생성됨 ( var user = BasicController.User(username=userA, age=23); )
=> 사용 후 : 객체를 json형태로 바꿔줌 ( var user = {"username":"userA","age":23}; )
자바스크립트 인라인 each
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
=> 결과
<!-- 자바스크립트 인라인 each -->
<script>
var user1 = {"username":"aaa","age":12};
var user2 = {"username":"bbb","age":22};
var user3 = {"username":"ccc","age":32};
</script>
템플릿 조각
- th:fragment, th:insert, th:replae
- 웹 페이지들에는 서로 동일한 공통 영역이 많음(상단바, 좌측 nav 등등)
@GetMapping("/fragment")
public String fragment() {
return "template/fragment/fragmentMain";
}
footer.html
<!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>
fragmentMain.html
<!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>
=> Controller에서 return "template/fragment/fragmentMain"; 을 해서 fragmentMain.html이 생성될 때 footer.html에 템플릿 조각으로 지정된 ( <footer th:fragment="copy"> ~ </footer> )가 fragmentMain.html의 th:insert, th:replace로 들어옴
=> ~{template/fragment/footer :: copy} 의미 : footer.html의 th:fragment="copy"라는 템플릿 조각을 가져옴
- th:insert는 해당 태그 안에 템플릿 조각을 삽입
- th:replace는 해당 태그를 템플릿 조각으로 대체
=> 이때, fragmentMain의 th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')} 으로
footer의 <footer th:fragment="copyParam (param1, param2)">로 데이터를 넘길 수 있음
템플릿 레이아웃 1
- 템플릿 조각에 해당 페이지가 원하는 정보를 넣어서 사용하기
ex) <head>를 공통으로 하여 템플릿 조각(base.html)으로 가져오는데, 특정 페이지(layoutMain.html)에서는 <head>에 필요한 것을 추가
@GetMapping("/layout")
public String layout() {
return "template/layout/layoutMain";
}
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>
layoutMain.html
<!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>
=> 결과
<!DOCTYPE html>
<html>
<head>
<!-- 교체 -->
<title>메인 타이틀</title>
<!-- 공통 -->
<link rel="stylesheet" type="text/css" media="all" href="/css/awesomeapp.css">
<link rel="shortcut icon" href="/images/favicon.ico">
<script type="text/javascript" src="/sh/scripts/codebase.js"></script>
<!-- 추가 -->
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/themes/smoothness/jquery-ui.css">
</head>
<body>
메인 컨텐츠
</body>
</html>
=> layoutMain에서는 <head th:fragment="common_header(title,links)"> ~ </head>를 th:replace로 가져옴
=> 이때 common_header(~{::title}, {~::link})를 사용해서 layoutMain의 <title>과 <link> 2개를 base의 th:replace에 로 보내서 대체시킴
템플릿 레이아웃 2
- <html>에 th:fragment을 적용하면 전체 문서를 대체해버릴 수도 있음
@GetMapping("/layoutExtend")
public String layoutExtend() {
return "template/layoutExtend/layoutExtendMain";
}
layoutFile.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>
layoutExtendMain.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>
=> layoutExtendMain의 <html>을 layoutFile의 <hrml>로 대체 => 전체 문서가 layoutFile로 바뀌어 버림
=> 대신 layoutExtendMain의 :: layout(~{::title},~{::section})를 사용하여 <title>과 <section>을 layoutFile의 th:replace로 보내서 대체시킴
=> 전체 문서의 틀을 받아오지만, 필요한 부분은 넘겨서 수정이 가능함
'WEB > spring' 카테고리의 다른 글
[Spring] 메시지, 국제화 (0) | 2022.01.23 |
---|---|
[Spring] Thymeleaf + Spring, 입력 <form> 다루기(체크박스) (0) | 2022.01.20 |
[Spring] Redirect vs Forward (0) | 2022.01.02 |
[Spring] WEB-INF 디렉토리 (0) | 2022.01.02 |
[Spring] Spring Bean Scope, Provider, 프록시 (0) | 2021.12.30 |