Thymeleaf - Basic
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의
웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있
www.inflearn.com

Thymeleaf는 가이드 문서가 잘 정리 되어 있다.
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
타임리프 사용 선언
<html xmlns:th="http://www.thymeleaf.org">
...
</html>
xmlns : 해당 문서를 위한 XML 네임스페이스(Namespace)를 명시한다.
텍스트 - text, utext
- HTML 엔티티 : '<' 와 같은 HTML의 특수 문자를 문자로 표현하는 방법이 필요한데, 이를 HTML 엔티티라 한다.
- Escape : 특수 문자를 HTML 엔티티로 변경하는 것을 Escape라 한다.
- Escape (th:text, [[...]]), Unescape (th:utext, [(...)])
1. 태그 안에서 속성으로 정의
<ul>
<li>th:text = <span th:text="${data}"></span></li>
<li>th:utext = <span th:utext="${data}"></span></li>
</ul>
2. 콘텐츠 안에 직접 데이터 입력
<ul>
<li><span th:inline="none">[[...]] = </span>[[${data}]]</li>
<li><span th:inline="none">[(...)] = </span>[(${data})]</li>
</ul>
SpringEL (Expression Language)
Thymeleaf에서는 변수 표현식 ${...} 으로 변수를 표현하는데여기에 스프링이 제공하는 표현식(SpringEL)을 사용할 수 있다.
다양한 표현식 사용 방법이 있지만, 다음의 표현식만 사용해야겠다.
// Object
th:text="${user.username}"
// List
th:text="${users[0].username}
// Map
th:text="${userMap['userA'].username}"
지역 변수 선언
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
기본 객체들
request, response, 세션, 빈 등도 접근이 가능하지만, 현재로서는 필요해 보이는 것이 없어 Skip. HTTP 요청 파라미터 접근만 알면 될 것 같다.
// url?paramData=hello
<ul>
<li>Request Parameter = <span th:text="${param.paramData}"></span></li>
</ul>
유틸리티 객체와 날짜
#message : 메시지, 국제화 처리
#uris : URI 이스케이프 지원
#dates : java.util.Date 서식 지원
#calendars : java.util.Calendar 서식 지원
#temporals : 자바8 날짜 서식 지원
#numbers : 숫자 서식 지원
#strings : 문자 관련 편의 기능
#objects : 객체 관련 기능 제공
#bools : boolean 관련 기능 제공
#arrays : 배열 관련 기능 제공
#lists , #sets , #maps : 컬렉션 관련 기능 제공
#ids : 아이디 처리 관련 기능 제공, 뒤에서 설명
유틸리티에서 필요한건 나중에 필요할 때 찾아보도록 링크만 남겨야겠다.
#temporals 사용법 일부만 정리
<li>yyyy-MM-dd HH:mm:ss = <span th:text="${#temporals.format(localDateTime, 'yyyy-MM-dd HH:mm:ss')}"></span></li>
<li>${#temporals.day(localDateTime)} = <span th:text="${#temporals.day(localDateTime)}"></span></li>
<li>${#temporals.month(localDateTime)} = <span th:text="${#temporals.month(localDateTime)}"></span></li>
<li>${#temporals.monthName(localDateTime)} = <span th:text="${#temporals.monthName(localDateTime)}"></span></li>
<li>${#temporals.monthNameShort(localDateTime)} = <span th:text="${#temporals.monthNameShort(localDateTime)}"></span></li>
URL
<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>
() 에 있는 부분은 쿼리 파라미터로 처리가 되는데, 경로상에 PathVariable이 있으면 경로 변수에 먼저 할당되고 남은 파라미터만 쿼리 파라미터가 된다.
상대경로, 절대경로, 프로토콜 기준(참고 링크)을 표현할 수 도 있다.
리터럴
리터럴 : 소스 코드상에서 고정된 값을 말한다 (ex. 'hello', 10, true, false, null)
타임리프에서 문자 리터럴은 항상 ' (작은 따옴표)로 감싼야 한다. 공백이 없다면 " " 로 감싸도 하나의 의미있는 토큰으로 인지하는데, 굳이 생략하지 말고 문자 리터럴은 항상 ' ' 로 감싸도록 하자
예시 : <span th:text="'hello world!'"></span>
> 잘 보면 " " 안에 ' ' 로 감싼 문자열이다.
리터럴 대체 (Literal subsitutions)
이건 많이 편해보인다. 변수와 같이 문자열을 만들 때 "'hello' + &{data}" 이렇게 만들 수도 있는데, 리터럴 대체 문법을 사용하면 편리하다. | | 사이에 변수가 포함 된 문자열을 작성한다.
예시 : <span th:text="|hello ${data}|">
연산
타임리프의 연산은 자바와 크게 다르지 않지만, HTML 엔티티를 사용하는부분만 조심하면 되겠다.
<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>
첫번째 예시 > 마저도 연산되어 false가 출력됐다. 작은 따옴표로 감싸지 않으면 연산해버리는 것 같다.
삼항 연산도 가능하고, Elvis 연산도 가능하다. Elvis 연산은 true case가 없는 삼항 연산 모양이다. 조건 데이터가 null이 아니면 조건의 데이터를 출력하고 null이면 false case를 출력한다.
이 연산들의 예시는 No-Operation ( _ ) 으로도 표현 가능하다. No-Operation을 사용하면 타임리프가 실행되지 않을 것처럼 동작한다. No-Operation 예시의 false case를 보면 공백이 있는데 지워도 똑같다. 그런데 표준 문서를 보니 컨벤션인 것 같다. 공백도 포함해서 쓰자.
<span th:text="(10 % 2 == 0)?'짝수':'홀수'"></span>
<span th:text="${data}?: '데이터가 없습니다.'"></span>
<span th:text="${nullData}?:'데이터가 없습니다.'"></span>
<span th:text="${data}?: _">데이터가 없습니다.</span>
<span th:text="${nullData}?: _">데이터가 없습니다.</span>
속성 값 설정
예시와 같이 속성 값을 대체 할 수 있다. name 속성에 할당 된 "mock"이 "userA"로 대체 된다.
<input type="text" name="mock" th:name="userA" />
속성 추가도 되는데 attrappend, attrprepend는 띄어쓰기가 자동으로 삽입되지 않으니 주의해야 한다.
// 기존 속성 값 뒤에 추가
- 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/>
checked 속성은 "true"를 대입하든, "false"를 대입하든 존재하면 checked 처리 된다. 그래서 타임리프에서는 check는 값이 false인 경우 check 속성 자체를 제거한다. (타임리프 렌더링 거치지 말고, 브라우저에서 파일을 바로 열어 봅시다)
- 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/>
반복
반복 속성 (th:each) 속성이 있는 태그 내부의 내용을 반복한다. List, 배열, Iterable, Enumeration 모두 지원한다. Map도 지원하는데 변수에 담기는 값은 Map.Entry이다. 자바의 for(num : nums)와 비슷하니 설명 대신 예시로 보자.
두번째 파라미터는 반복 상태 값이 들어가고 생략도 가능한데, 생략하면 지정한 변수명(user) + Stat 이 변수명이 된다.
<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>
조건부 평가
if, unless(if의 반대)를 사용할 수 있다. 조건이 맞지 않으면 태그 자체를 렌더링하지 않는다.
switch 에서 * 는 만족하는 조건이 없을 때 들어오는 default이다.
<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="*">기타</span>
</td>
주석
타임리프의 주석은 렌더링 타임에 제거된다. 렌더링 타임에 제거 되므로 HTML 주석처럼 -->로 닫으려해도 닫히지 않는다. 예시의 렌더링하면 전부 주석으로 인지되어 삭제된다. (웹브라우저로) 네츄럴 템플릿으로 보면 보이겠다. 타임리프 프로토 타입 주석은 HTML 파일을 순수 HTML에서는 주석처리 되지만, 렌더링 후에만 보이는 기능인데 잘 안쓴다고 한다.
<!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->
<!--/*/
<span th:text="${data}">html data</span>
/*/-->
블록
"th:block"은 유일한 자체 태그이다. (나머지는 태그의 속성) 렌더링 후 <th:block>는 삭제된다.
예제에서는 <div> 두 개를 반복문으로 같이 돌릴 때 사용했다. 엔간하면 쓰지 않는 것이 좋다.
<th:block th:each="user : ${users}">
<div>
<span th:text="${user.username}">username</span>
<span th:text="${user.age}">age</span>
</div>
<div>
<span th:text="${user.username} + '/' + ${user.age}">username/age</span>
</div>
</th:block>
자바스크립트 인라인
<script th:inline="javascript">
...
</script>
1. 문자 타입의 데이터 처리
문자 타입인 경우 " "를 자동으로 붙여주고, 이스케이프 처리도 해준다.
// userA -> "userA"
var username = [[${user.username}]];
var age = [[${user.age}]];
2. 자바스크립트 내추럴 템플릿
렌더링 전후 다른 값을 대입할 수 있다. 렌더링 전에는 주석으로 동작하고, 렌더링 후에는 주석이 제거되고 인라인 된 식을 출력한다.
var username2 = /*[[${user.username}]]*/ "test username";
// 인라인 사용 전 : var username2 = /*userA*/ "test username";
// 인라인 사용 후 : var username2 = "userA";
3. 객체는 JSON으로 변환해준다.
var user = /*[[${session.user}]]*/ null;
->
var user = {"age":null,"firstName":"John","lastName":"Apricot",
"name":"John Apricot","nationality":"Antarctica"};
4. each 지원
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
// var user1 = {"username":"UserA","age":10};
// var user2 = {"username":"UserB","age":20};
// var user3 = {"username":"UserC","age":30};
템플릿 조각 (fragment)
웹 페이지의 공통되는 영역 (footers, headers, menus)들이 있다. 타임 리프는 th:fragment를 정의하여 조각들을 가져올 수 있는 기능을 지원한다.
// footer.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
<footer th:fragment="frag (onevar,twovar)">
<p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</footer>
</body>
</html>
하나의 파일에 여러 th:fragment가 있을 수 있다. 메서드처럼 이름으로 호출 할 수 있다.
가이드 문서보면 fragment 표현식 여러 개가 있지만 "~{templatename::fragmentname}" 하나만 써도 되겠다. ~{...} 을 사용하는것이 원칙이지만, 호출하는 코드가 단순하면 생략할 수 있다. 단순의 기준을 모르겠다. 그냥 생략하지 말고 써야겠다.
// 호출
<body>
...
<div th:insert="~{template/fragment/footer :: copy}"></div>
<div th:replace="~{template/fragment/footer :: copy}"></div>
</body>
<div th:replace="~{template/fragment/footer ::frag (${value1},${value2})}">...</div>
<div th:replace="~{template/fragment/footer ::frag (onevar=${value1},twovar=${value2})}">...</div>
- th:insert : 현재 태그 내부에 추가
- th:replace : 현재 태그를 대체
템플릿 레이아웃 1
앞서 배운 템플릿 조각을 조금 더 적극적으로 사용하는 방식이다. 레이아웃을 정의하고, 그 레이아웃에 필요한 코드 조각(태그)을 전달해서 완성한다.
여기서는 <head>의 layouy만 정의하여 사용했다.
// base.html
<head th:fragment="common_header(title,links)">
<title th:replace="${title}">The awesome application</title>
<!-- Common styles and scripts -->
<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>
<!--/* Per-page placeholder for additional links */-->
<th:block th:replace="${links}" />
</head>
//layoutMain.html
...
<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
<title>Awesome - Main</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>
...
<title> 과 <link> 태그들이 호출하는 common_header의 파라미터로 넘어간다.
사용해보니 파라미터로 들어가는 태그가 없으면 에러가 났다. 없을 수 있는 태그는 Elvis No-Operation 해줘야겠다.
<th:block th:replace="${links}?: _" />
// base.html
<head th:fragment="common_header(title,links)">
<title th:replace="${title}">The awesome application</title>
<!-- Common styles and scripts -->
<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>
<!--/* Per-page placeholder for additional links */-->
<th:block th:replace="${links}?: _" />
</head>
템플릿 레이아웃2
이번에는 <head> 만 적용하는게 아니라 <html> 전체를 적용한다.
// layoutFile.html
<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<title th:replace="${title}">Layout Title</title>
</head>
<body>
<h1>Layout H1</h1>
<div th:replace="${content}">
<p>Layout content</p>
</div>
<footer>
Layout footer
</footer>
</body>
</html>
<!DOCTYPE html>
<html th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Page Title</title>
</head>
<body>
<section>
<p>Page content</p>
<div>Included on page</div>
</section>
</body>
</html>
추가
form
<form action="item.html" th:action method="post">
...
</form>
th:action은 form 태그의 action 속성을 대체한다. HTML form에서는 action에 값이 없으면 현재 URL에 데이터를 전송한다.
상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 Mapping을 구분했다.
- 상품 등록 폼 : GET "/basic/items/add"
- 상품 등록 처리 : POST "/basic/items/add"