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 &lt;b&gt;Spring!&lt;/b&gt;</li>

-  Hello Spring! 를 원했는데,  브라우저에는 Hello <b>Spring!</b> 로 나옴

- <, > 가 escape처리가 되어서 &lt;,  &gt; 로 바뀌어서 <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&amp;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로 보내서 대체시킴

=> 전체 문서의 틀을 받아오지만, 필요한 부분은 넘겨서 수정이 가능함