[Spring Web MVC] Thymeleaf (1)
타임리프 뷰 템플릿 엔진은 백엔드 서버에서 HTML을 동적으로 렌더링 할 때 사용된다.
스프링과 통합이 가장 잘 되는 뷰 템플릿이고, 제공하는 기능도 가장 많아서 뷰 템플릿 중에서는 타임리프를 많이 사용한다.
텍스트
<li>th:text 사용 <span th:text="${data}">sdqwfe</span></li>
<li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li>
${data} 를 통해 sdqwfe를 모델로 전달받은 data로 바꿔서 렌더링한다.
HTML 컨텐츠 영역 안에서 직접 데이터를 출력하려면 [[]] 를 사용한다.
<li>th:utext = <span th:utext="${data}"></span></li>
<li><span th:inline="none">[(...)] = </span>[(${data})]</li>
이스케이프 기능을 사용하지 않고 렌더링하려면 utext 혹은 [()] 를 사용한다.
변수
기본적으로 변수 표현식 ${...} 을 사용해서 변수를 사용한다.
<h1>SpringEL 표현식</h1>
<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>
<h1>지역 변수 - (th:with)</h1>
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
모델을 통해 객체, 리스트, 맵 자료구조를 넘겨줘도 타임리프에서 값을 해석해서 사용할 수 있다.
자바의 프로퍼티 접근법을 사용해 user.username 은 알아서 user.getUsername() 으로 바꿔서 받아온다.
th:with 를 사용해 지역변수를 선언하고 사용할 수 있다.
지역 변수는 선언한 태그 내부에서만 유효하다.
기본 객체
<h1>식 기본 객체 (Expression Basic Objects)</h1>
<ul>
<li>request = <span th:text="${#request}"></span></li>
<li>response = <span th:text="${#response}"></span></li>
<li>session = <span th:text="${#session}"></span></li>
<li>servletContext = <span th:text="${#servletContext}"></span></li>
<li>locale = <span th:text="${#locale}"></span></li>
</ul>
<h1>편의 객체</h1>
<ul>
<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>
모델에 담아서 넘겨주지 않아도 많이 사용하는 요소들은 타임리프에서 기본 객체로 제공한다.
${#request} / ${#response} / ${#session} / ${#servletContext} / ${#locale}
기본 객체에 추가로 데이터에 편하게 접근하기 위해 여러 가지 편의 객체도 제공한다.
HTTP 요청 파라미터 접근 : param
HTTP 세션 접근 : session
스프링 빈으로 등록된 요소 접근 : @
이 외에도 문자 / 숫자 / 날짜 / URI 등을 편하게 다룰 수 있게 도와주는 다양한 유틸리티 객체들이 있다. 필요할 때 찾아 쓰자.
URL 링크
@{...} 을 사용해 URL을 생성한다.
<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>
모델로 param1, param2를 넘겨주고 시작한다.
/hello?param1=data1¶m2=data2 : () 안에 있는 부분은 쿼리 파라미터로 처리된다.
/hello/data1/data2 : URL 경로 내부에 변수가 있으면 () 안에 있는 부분은 경로 변수로 처리된다.
두 가지를 함께 사용해도 된다.
리터럴
문자 / 숫자 / 참.거짓 / null 리터럴이 있다.
여기서 문자 리터럴은 항상 작은 따옴표 (') 로 감싸서 사용해야 하지만, 공백 없이 쭉 이어진다면 작은 따옴표를 생략할 수 있다.
<!-- <li>"hello world!" = <span th:text="hello world!"></span></li>-->
<li>'hello' + ' world!' = <span th:text="'hello' + ' world!'"></span></li>
<li>'hello world!' = <span th:text="'hello world!'"></span></li>
<li>'hello ' + ${data} = <span th:text="'hello ' + ${data}"></span></li>
<li>리터럴 대체 |hello ${data}| = <span th:text="|hello ${data}|"></span></li>
맨 위 처럼 작은 따옴표를 사용하지 않으면 오류가 발생한다.
마지막에는 리터럴 대체 문법인 || 를 사용했는데, 이 방법으로 문자열을 좀 더 편하게 작성할 수 있다. (|| 내부를 문자열으로 간주하고 변수 부분만 교체함)
연산자
자바와 크게 다르지 않지만 HTML 엔티티를 사용하는 부분에 주의하자.
<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>
<li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)?'짝수':'홀수'"></span></li>
<li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가없습니다.'"></span></li>
<li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?:'데이터가 없습니다.'"></span></li>
<li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</span></li>
<li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가없습니다.</span></li>
> 대신 gt를 사용할 수 있다.
Elvis 연산자를 사용하면 삼항 연산자를 좀 더 간단하게 사용할 수 있다. ("${data}?:")
No-Operation 연산자는 Elvis 연산자와 비슷하지만 참일 경우 타임리프가 해당 라인을 실행하지 않는다. ( _ )
속성 값 설정
th:* 을 사용해 기존에 사용하던 속성을 대체하는 방식으로 사용한다. (기존 값이 없으면 새로 만든다.)
<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/>
th:attrappend : 속성 값의 뒤에 해당 값을 추가한다.
th:prepend : 속성 값의 앞에 해당 값을 추가한다.
th:classappend : class 속성에 자연스럽게 해당 값을 추가한다.
애초에 자주 사용되는 속성이 아니라.. classappend 정도만 기억해두자.
HTML에서 checked 속성은 checked 속성의 값에 관계없이 체크해버린다.
타임리프는 true false 진리값으로 직관적으로 체크할 수 있도록 도와준다.
반복
반복문은 th:each 를 통해 사용한다.
<tr th:each="user : ${users}">
<td th:text="${user.username}">username</td>
<td th:text="${user.age}">0</td>
</tr>
모델을 통해 users 컬렉션을 전달받는다. (리스트, 배열, Iterable, Enumeration, Map)
컬렉션 내부를 순회하면서 HTML을 렌더링한다.
<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>
반복 시 두 번째 파라미터를 설정해 반복의 상태를 확인할 수 있다. (생략 가능. 생략 시 변수명 + Stat)
index : 0 부터 시작하는 값
count : 1 부터 시작하는 값
size : 전체 사이즈
even, odd : 홀짝 여부
first, last : 시작 끝 여부
current : 현재
조건문
직관적이다. if / unless 를 사용해서 처리한다.
<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>
<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
해당 조건식에 부합하지 않으면 태그 자체를 렌더링하지 않는다.
switch 문법도 제공하는데, *은 만족하는 조건이 없어 기본값으로 설정할 때 사용한다.
주석
<h1>1. 표준 HTML 주석</h1>
<!--
<span th:text="${data}">html data</span>
-->
<h1>2. 타임리프 파서 주석</h1>
<!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->
<h1>3. 타임리프 프로토타입 주석</h1>
<!--/*/
<span th:text="${data}">html data</span>
/*/-->
1. HTML 주석 : 타임리프가 렌더링할 때 무시하고 진행한다.
2. 타임리프 파서 주석 : /* 부터 */ 까지가 해당 범위로, 렌더링 할 때 주석 부분을 제거한다.
3. 타임리프 프로토타입 주석 : 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>
for Each 문법으로 표현하기 어려울 때 사용한다.
<th:block> 태그는 렌더링 시 사라진다.
이런 지엽적인 문법은 최대한 사용을 지양하자.
자바스크립트 인라인
<!-- 자바스크립트 인라인 사용 전 -->
<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>
자바스크립트 인라인을 사용하기 전에는 값을 저장할 때 타입에 따라 하나하나 다르게 처리해야됐고, 타임리프의 네추럴 템플릿 기능도 사용할 수 없었다.
th:inling="javascript" 를 추가해 타입에 따라 알아서 따옴표를 넣어주는 등 후처리를 해 주고, 객체를 넣었을 때 자동으로 JSON 타입으로 변환해주는 등 여러 기능을 제공한다.
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
추가로 each로 반복문을 돌 수도 있다.
렌더링 시 user1 / user2 / user3 ... 이렇게 여러 개의 user N 변수가 생성된다.
템플릿 조각 / 레이아웃
웹 페이지를 설계할 때 공통으로 사용되는 영역이 많이 나온다.
그런 영역을 각 페이지별로 하나씩 작성해서 넣어버리면 페이지를 수정할 때 싹 다 수정해야 돼서 매우 비효율적이다.
타임리프는 공통인 부분을 따로 작성할 수 있도록 하는 기능을 제공한다. (템플릿 조각 / 템플릿 레이아웃)
<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>
footer.html
여기서 th:fragment 가 포함된 태그는 다른 페이지에서 가져다 쓰는 공통 코드 조각으로 생각하면 된다.
<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>
fragmentMain.html
template/fragment/footer :: copy : template/fragment/footer.html 위치의 th:fragment = "copy" 부분을 가져와서 사용함.
th:insert : 현재 태그 내부에 추가한다.
th:replace : 현재 태그를 대체한다.
파라미터 사용 : 데이터를 전달해 조각을 동적으로 렌더링 하는 것도 가능하다.
원래는 ~{...} 로 템플릿 조각을 사용함을 명시해야 하지만, 템플릿 조각을 사용하는 부분이 단순하면 생략해도 된다.
이렇게 템플릿 조각을 가져와서 웹 페이지를 설계했다.
템플릿 레이아웃은 웹 페이지를 구성하는 큰 틀에 코드 조각을 넘겨서 사용하는 방법이다.
공통되는 부분을 따로 분리해서 사용하는 개념은 같지만, 각 페이지마다 필요한 정보를 더 추가하고 싶을 때 사용한다.
<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>
base.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>
layoutMain.html
layoutMain.html 을 타임리프를 통해 렌더링한다.
template/layout/base :: common_header(~{::title}, ~{::link}) 이 부분에서 :: title은 현재 페이지의 title 태그들을 전달하고 ::link는 현재 페이지의 link 태그들을 전달한다.
template/layout/base.html 은 전달받은 데이터를 바탕으로 화면을 렌더링한다.
즉, 레이아웃에 필요한 코드 조각을 전달해서 완성하는 방식으로 화면을 구성한다.
템플릿 조각과 레이아웃을 적절히 사용해서 웹 페이지를 설계하자.
JSP, Mustache.. 모든 템플릿 엔진이 그렇듯 기능이 거의 비슷하다.
'Spring > Spring Web MVC' 카테고리의 다른 글
[Spring Web MVC] 메세지와 국제화 (0) | 2022.08.20 |
---|---|
[Spring Web MVC] Thymeleaf (2) (0) | 2022.08.20 |
[Spring Web MVC] Spring MVC와 Thymeleaf 예시 (0) | 2022.08.18 |
[Spring Web MVC] 응답 정보 다루기 (0) | 2022.08.17 |
[Spring Web MVC] 요청 정보 다루기 (0) | 2022.08.17 |
댓글
이 글 공유하기
다른 글
-
[Spring Web MVC] 메세지와 국제화
[Spring Web MVC] 메세지와 국제화
2022.08.20 -
[Spring Web MVC] Thymeleaf (2)
[Spring Web MVC] Thymeleaf (2)
2022.08.20 -
[Spring Web MVC] Spring MVC와 Thymeleaf 예시
[Spring Web MVC] Spring MVC와 Thymeleaf 예시
2022.08.18 -
[Spring Web MVC] 응답 정보 다루기
[Spring Web MVC] 응답 정보 다루기
2022.08.17