이 글은 김영한 강사님의 강의를 참고하여 작성하였습니다.
들어가기 앞서 gradle과 lombok 확인 잊지말자
물론 이전에 뷰 템플릿에는 xml도 jsp도 있었다. 현재는 스프링과 가장 적용 가능하고 좋은 타임리프를 쓴다.
서버 사이드 HTML 렌더링 (SSR)
• 타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용된다.
네츄럴 템플릿
• 타임리프는 순수 HTML을 최대한 유지하는 특징이 있다.
타임리프로 작성한 파일은 HTML을 유지하기 때문에 웹 브라우저에서 파일을 직접 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.
-> 물론 이 경우 동적으로 결과가 렌더링 되지는 않는다.
• JSP를 포함한 다른 뷰 템플릿들은 해당 파일을 열면
-> JSP의 경우, 웹 브라우저에서 열어보면 JSP 소스코드와 HTML이 뒤죽박죽 섞여서 웹 브라우저에서 정상적인 HTML 결과를 확인할 수 없다.
-> 오직 서버를 통해서 JSP가 렌더링 되고 HTML 응답 결과를 받아야 화면을 확인할 수 있다.
• 이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿 (natural templates)이라 한다.
스프링 통합 지원
타임리프는 스프링과 자연스럽게 통합되고, 스프링의 다양한 기능을 편리하게 사용할 수 있게 지원한다. 이 부분은 스프링 통합과 폼 장에서 자세히 알아보겠다.
들어가기 전에 타임리프는 미리 html 위에 선언 해주어야 합니다.
<html xmlns:th="http://www.thymeleaf.org">
- 간단한 표현:
- 변수 표현식: ${...}
- 선택 변수 표현식: *{...}
- 메시지 표현식: #{...}
- 링크 URL 표현식: @{...}
- 조각 표현식: ~{...}- 리터럴
• 텍스트: 'one text', 'Another one!',…
• 숫자: 0, 34, 3.0, 12.3,…
• 불린: true, false
• 널: null
• 리터럴 토큰: one, sometext, main,…- 문자 연산:
◦ 문자 합치기: +
◦ 리터럴 대체: |The name is ${name}|- 산술 연산:
◦ Binary operators: +, -, *, /, %
◦ Minus sign (unary operator): -- 불린 연산:
◦ Binary operators: and, or
◦ Boolean negation (unary operator): !, not- 비교와 동등:
◦ 비교: >, <, >=, <= (gt, lt, ge, le)
◦ 동등 연산: ==, != (eq, ne)- 조건 연산:
◦ If-then: (if) ? (then)
◦ If-then-else: (if) ? (then) : (else)
◦ Default: (value) ?: (defaultvalue)- 특별한 토큰:
◦ No-Operation: _
HTML의 콘텐츠(content)에 데이터를 출력할 때 th:text
ex) <span th:text="${data}">
HTML 테그의 속성이 아니라 HTML 콘텐츠 영역 안에서 직접 데이터를 출력하고 싶으면 [[...]]
를 사용하면 된다.
=> 컨텐츠 안에서 직접 출력하기 = [[${data}]]
<li>th:text 사용 <span th:text="${data}"></span></li>
<li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li>
[[${data}]]
는 이렇게 직접 출력된다.들어가기 전에 HTMl 문서는
<
>
같은 특수문자를 기반으로 정의.
=> 따라서 뷰 템플릿으로 HTML 화면을 생성할 때는 출력하는 데이터에 이러한 특수 문자가 있는 것을 주의해서 사용해야 한다.
<
를 HTML 테그의 시작으로 인식한다. 따라서 <
를 테그의 시작이 아니라 문자로 표현할 수 있는 방법이 필요 th:text
,[[...]]
는 기본적으로 이스케이스(escape)를 제공우리는 <b>
태그를 이용해 Spring! 을 만들고 싶다.
<b>
태그를 HTML 엔티티로 변경시킴..<
를 <
로 바꾸네 😢💦💦 .
.
.
방법은 있다.
th:text
→ 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"
: 여기서는 타임리프르 쓰지 않겠다는 말이다.
타임리프에서 변수를 사용할 때는 변수 표현식을 사용한다.
변수 표현식 : ${...}
그리고 이 '변수 표현식'에는 '스프링 EL'이라는 스프링이 제공하는 표현식을 사용할 수 있다.
Object
- user.username``` : user의 username을 프로퍼티 접근 → ```user.getUsername()``` -
user['username']: 위와 같음 →
user.getUsername() - ````user.getUsername()
: user의 getUsername() 을 직접 호출
List
- users[0].username
: List에서 첫 번째 회원을 찾고 username 프로퍼티 접근 → list.get(0).getUsername()
- users[0]['username']
: 위와 같음
- users[0].getUsername()
: List에서 첫 번째 회원을 찾고 메서드 직접 호출
Map
- userMap['userA'].username
: Map에서 userA를 찾고, username 프로퍼티 접근 → map.get("userA").getUsername()
- userMap['userA']['username']
: 위와 같음
- userMap['userA'].getUsername()
: Map에서 userA를 찾고 메서드 직접 호출
<h1>지역 변수 - (th:with)</h1>
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
th:with
: 지역변수 선언- 지역변수는 선언한 범위 내에서만 사용 가능
타임리프는 기본 객체들을 제공한다.
${#request}
${#response}
${#session}
${#servletContext}
${#locale}
그런데 #request 는 HttpServletRequest 객체가 그대로 제공되기때문에 데이터를 조회하려면
request.getParameter("data")
처럼 불편하게 접근해야 한다.
이런 점을 해결하기 위해 편의 객체도 제공한다.
param
${param.paramData}
session
${session.sessionData}
@
${@helloBean.hello('Spring!')}
보통 유틸리티 객체들은 대략 이런 것들이 있다는 것만 파악하고, 필요 시 찾아서 사용한다. 그러니 더 자세히 참조하려면 이 곳에서 참조
#message
: 메시지, 국제화 처리#uris
: URI 이스케이프 지원#dates
: java.util.Date 서식 지원#calendars
: java.util.Calendar 서식 지원#temporals
: 자바8 날짜 서식 지원#numbers
: 숫자 서식 지원#strings
: 문자 관련 편의 기능#objects
: 객체 관련 기능 제공#bools
: boolean 관련 기능 제공#arrays
: 배열 관련 기능 제공#lists
,#sets
, #maps
: 컬렉션 관련 기능 제공#ids
: 아이디 처리 관련 기능 제공, 뒤에서 설명라이브러리
thymeleaf-extras-java8time
자바8 날짜용 유틸리티 객체
#temporals
사용예시
<span th:text="${#temporals.format(localDateTime, 'yyyy-MM-dd HH:mm:ss')}"></
span>
URL을 생성할 때는 @{...}
단순한 URL
- @{/hello} /hello
쿼리 파라미터
- @{/hello(param1=${param1}, param2=${param2})}
-> /hello?param1=data1¶m2=data2
- () 에 있는 부분은 쿼리 파라미터로 처리된다.
경로 변수
- @{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}
-> /hello/data1/data2
- URL 경로상에 변수가 있으면 () 부분은 경로 변수로 처리된다.
경로 변수 + 쿼리 파라미터
- @{/hello/{param1}(param1=${param1}, param2=${param2})}
-> /hello/data1?param2=data2
- 경로 변수와 쿼리 파라미터를 함께 사용할 수 있다.
상대경로, 절대경로, 프로토콜 기준을 표현할 수 도 있다.
/hello
: 절대 경로
hello
: 상대 경로
정의 : 리터럴은 소스 코드 상에 고정된 값을 뜻함
ex) 여기선 "Hello", 10,20 이렇게 리터럴임.
String a = "Hello"
int a = 10 * 20
<전> : <span th:text="'hello'">
=> 그러나 공백 없이 쭉 이어진다면 하나의 의미있는 토큰으로 인지해서 다음과 같이 작은 따옴표를 생략 (ㅇ)
<후>: <span th:text="hello">
<span th:text="|hello ${data}|">
=> 리터럴 대체 문법을 사용하면 마치 템플릿을 사용하는 것 처럼 편리하다.
>
(gt),<
(lt),>=
(ge),<=
(le),!
(not),==
(eq),!=
(neq, ne)
<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>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)?
'짝수':'홀수'"></span></li>
<li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가 없습니다.'"></span></li>
이 경우
${data}?
이 부분에 데이터가 들어오면, 본인을 출력하고
아닐 경우: '데이터가 없습니다.'
이 뒤의 '데이터가 없습니다'가 출력된다.
_
인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작한다. 이것을 잘 사용하면 HTML의 내용 그대로 활용할 수 있다. <li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</
span></li>
<li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가
없습니다.</span></li>
타임리프는 th:*
속성을 지정하면(*
=어떤 속성), 타임리프는 렌더링 후, 기존 속성에서 th:*
로 지정한 속성으로 대체한다.
만일 기존 속성이 없다면 새로 만든다.
<input type="text" name="mock" th:name="userA" />
(타임리프 렌더링 후...)
<input type="text" name="userA" />
📢 속성 추가
th:attrappend
: 속성 값의 뒤에 값을 추가한다.
th:attrprepend
: 속성 값의 앞에 값을 추가한다.
th:classappend
: class 속성에 자연스럽게 추가한다.
HTML에서 checked 속성은 checked 속성의 값과 상관없이 checked 라는 속성만 있어도 체크가 된다.
이런 부분이 true , false 값을 주로 사용하는 개발자 입장에서는 불편하다.
타임리프의 th:checked
는 값이 false 인 경우 checked 속성 자체를 제거한다.
전 : <input type="checkbox" name="active" th:checked="false" />
타임리프 렌더링 후: <input type="checkbox" name="active" />
<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>
- 오른쪽 컬렉션(
${users}
)의 값을 하나씩 꺼내서 왼쪽 변수(user
)에 담아서 태그를 반복- 반복의 두번째 파라미터를 설정해서 반복의 상태를 확인 할 수 있습니다
- if, unless
타임리프는 해당 조건이 맞지 않으면태그 자체를 렌더링하지 않는다.
만약 다음 조건이 false 인 경우 ... 부분 자체가 렌더링 되지 않고 사라진다.
<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="*">기타</span>
</td>
*
은 만족하는 조건이 없을 때 사용되는 '디폴트 값'
타임리프가 렌더링 하지 않고, 그대로 남겨둔다
- 2. 타임리프의 진짜 주석이다. 렌더링에서 주석 부분을 제거한다.
- 3. 타임리프 프로토타입 주석
타임리프 프로토타입은 약간 특이한데, HTML 주석에 약간의 구문을 더했다.
=> 쉽게 이야기해서 HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 렌더링 한 경우에만 보이는 기능이다.
HTML 태그 안에 속성으로 기능을 정의해서 사용하는데, 위 예처럼 이렇게 사용하기 애매한 경우에 사용하면 된다. <th:block>
은 렌더링시 제거된다.
<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">
<script th:inline="javascript">
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
</script>
var username = [[${user.username}]];
사용 (후) : var username = "userA";
으로 문자 타입으로 출력 해준다.
=> 예) " \"
타임리프는 HTML 파일을 직접 열어도 동작하는 내추럴 템플릿 기능을 제공
var username2 = /*[[${user.username}]]*/ "test username";
사용 (전) : var username2 = /*userA*/ "test username";
사용 (후) : var username2 = "userA";
그 전에는 '내추럴 템플릿' 기능 작동X, -> 순수하게 그대로 해석
타임리프의 자바스크립트 인라인 기능을 사용하면 객체를 JSON으로 자동으로 변환
var user = [[${user}]];
var user = BasicController.User(username=userA, age=10);
var user = {"username":"userA","age":10};
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
[/] 자바 세미콜론처럼 타임리프 끝난다는 뜻.
웹 페이지를 개발할 때는 공통 영역이 많이 있다. 예를 들어서 상단 영역이나 하단 영역, 좌측 카테고리 등등 여러 페이지에서 함께 사용하는 영역들이 있다.
이런 부분을 코드를 복사해서 사용한다면 변경시 여러 페이지를 다 수정해야 하므로 상당히 비효율 적이다. 타임리프는 이런 문제를 해결하기 위해 ₁템플릿 조각과 ₂레이아웃 기능을 지원한다
<!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>
th:fragment
가 있는 태그는 다른곳에 포함되는 코드 조각으로 이해
<!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>
template/fragment/footer :: copy :
->template/fragment/footer.html
템플릿에 있는
th:fragment="copy"
라는 부분을 ,템플릿 조각으로 가져와서 사용한다는 의미
<div th:insert="~{template/fragment/footer :: copy}"></div>
=> 현재 태그( div ) 내부에 추가
<div th:replace="~{template/fragment/footer :: copy}"></div>
=> 현재 태그( div )를 대체한다.
- 사용하는 것이 원칙이지만 템플릿 조각을 사용하는 코드가 단순하면 이 부분을 생략할 수 있다
다음과 같이 파라미터를 전달해서 동적으로 조각을 렌더링 할 수도 있다.
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
코드 조각을 레이아웃에 넘겨서 사용하는 방법에 대해서 알아보자.
공통으로 사용하는 css , javascript 같은 정보들이 있는데, 이러한 공통정보들을 한 곳에 모아두고, 공통으로 사용하지만, 각 페이지마다 필요한 정보를 더 추가해서 사용하고싶다면 다음과 같이 사용하면 된다.
<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>
<!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>
common_header(~{::title},~{::link})
이 부분이 핵심이다.
-::title
은 현재 페이지의 title 태그들을 전달한다.
-::link
는 현재 페이지의 link 태그들을 전달한다.
앞서 이야기한 개념을 <head>
정도에만 적용하는게 아니라 <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>
layoutFile.html
을 보면 기본 레이아웃을 가지고 있는데,<html>
에th:fragment
속성이 정의되어 있다.
-> 이 레이아웃 파일을 기본으로 하고, 여기에 필요한 내용을 전달해서 부분부분 변경하는 것 으로 이해하면 된다.
<!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
는 현재 페이지인데,
<html>
자체를th:replace
를 사용해서 변경하는 것을 확인할 수 있다.
=> 결국layoutFile.html
에 필요한 내용을 전달하면서<html>
자체를layoutFile.html
로 변경
<!DOCTYPE html>
<html>
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
<footer>
레이아웃 푸터
</footer>
</body>
</html>