2022. 8. 17. 18:26ㆍSpring
프로젝트 생성
타임리프 소개
공식 사이트: https://www.thymeleaf.org/
공식 메뉴얼 - 기본 기능: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
공식 메뉴얼 - 스프링 통합: https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html
타임리프 특징
1. 서버 사이드 HTML 렌더링 (SSR)
타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용됩니다.
백엔드 개발자의 경우 (화려한 프론트엔드 기술을 적용할 필요 없이) 빠르게 데이터를 확인 해야할 일이 생깁니다. 이때 간단한 어드민 페이지 정도 만들어서 확인해 볼 수 있는데요, 이를 위해서라도 SSR의 한 가지 정도는 공부하는 것을 추천합니다.
ex) jsp, thymeleaf
CSR
ex) 리액트
2.네츄럴 템플릿
타임리프는 순수 HTML을 최대한 유지하는 특징이 있습니다.
타임리프로 작성한 파일은 HTML을 유지하기 때문에 웹 브라우저에서 파일을 직접 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있습니다.
JSP를 포함한 다른 뷰 템플릿들은 해당 파일을 열면 웹 브라우저에서 정상적인 HTML 결과를 확인할 수 없고 오직 서버를 통해서 JSP가 렌더링 되고 HTML 응답 결과를 받아야 화면을 확인할 수 있습니다.
반면에 타임리프로 작성된 파일은 해당 파일을 그대로 웹 브라우저에서 열어도 정상적인 HTML 결과를 확인할 수 있습니다. 물론 이 경우 동적으로 결과가 렌더링 되지는 않습니다.
이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 내추럴 템플릿 (natural templates)이라 합니다.
3. 스프링 통합 지원
타임리프는 스프링과 자연스럽게 통합되고, 스프링의 다양한 기능을 편리하게 사용할 수 있게 지원합니다.
타임리프 기본 기능
타임리프 사용 선언
타임리프를 사용할 때 최상단에 선언합니다.
<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: _
참고: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standardexpression-syntax
위에서 살펴본 기본 표현식을 하나씩 알아보도록 하겠습니다.
기본 화면이자 목차로 쓸 index.html 화면입니다.
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
<li>텍스트
<ul>
<li><a href="/basic/text-basic">텍스트 출력</a></li>
</ul>
</li>
<li>표준 표현식 구문
<ul>
<li><a href="/basic/variable">변수 - SpringEL</a></li>
<li><a href="/basic/basic-objects?paramData=HIParam">기본 객체들</a></li>
<li><a href="/basic/date">유틸리티 객체와 날짜</a></li>
<li><a href="/basic/link">링크 URL</a></li>
<li><a href="/basic/literal">리터럴</a></li>
<li><a href="/basic/operation">연산</a></li>
</ul>
</li>
<li>속성 값 설정
<ul>bute">속성 값 설정</a></li>
</ul>
<li><a href="/basic/attri
</li>
<li>반복
<ul>
<li><a href="/basic/each">반복</a></li>
</ul>
</li>
<li>조건부 평가
<ul>
<li><a href="/basic/condition">조건부 평가</a></li>
</ul>
</li>
<li>주석 및 블록
<ul>
<li><a href="/basic/comments">주석</a></li>
<li><a href="/basic/block">블록</a></li>
</ul>
</li>
<li>자바스크립트 인라인
<ul>
<li><a href="/basic/javascript">자바스크립트 인라인</a></li>
</ul>
</li>
<li>템플릿 레이아웃
<ul>
<li><a href="/template/fragment">템플릿 조각</a></li>
<li><a href="/template/layout">유연한 레이아웃</a></li>
<li><a href="/template/layoutExtend">레이아웃 상속</a></li>
</ul>
</li>
</ul>
</body>
</html>
[ 텍스트 출력 ]
HTML의 콘텐츠(content)에 데이터를 출력할 때는 다음과 같이 th:text 를 사용하면 됩니다.
<span th:text="${data}">
💡 주의 : 이스케이프(Escape) 💡
HTML 문서는 < , > 같은 특수 문자를 기반으로 정의됩니다. 따라서 뷰 템플릿으로 HTML 화면을 생성할 때는 출력하는 데이터에 이러한 특수 문자가 있는 것을 주의해서 사용해야 합니다.
HTML 엔티티
웹 브라우저는 < 를 HTML 태그의 시작으로 인식하므로 < 를 태그의 시작이 아니라 문자로 표현할 수 있는 방법이 필요한데, 이것을 HTML 엔티티라 합니다.
그리고 이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 이스케이프(escape)라 합니다.
(타임리프가 제공하는 th:text , [[...]] 는 기본적으로 이스케이스(escape)를 제공합니다.)
하지만 우리가 원하는 것은 문자 그대로 출력이 아닙니다. 이럴 때는 Unescape 처리를 해주면 됩니다.
th:text → th:utext
[[...]] → [(...)]
실제 서비스를 개발하다 보면 escape를 사용하지 않아서 HTML이 정상 렌더링 되지 않는 수많은 문제가 발생하기에 escape를 기본으로 하고, 꼭 필요한 때만 unescape를 사용하는 것을 권장합니다.
basic > BasicController
package example.thymeleaf.basic;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/text-basic")
public String textBasic(Model model) {
model.addAttribute("data", "Hello World!");
return "basic/text-basic";
}
}
resources > templates > basic > text-basic.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>text vs utext</h1>
<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>
</body>
</html>
렌더링 화면
[ 변수 - SpringEL ]
타임리프에서 변수를 사용할 때는 변수 표현식 ${...}을 사용합니다.
그리고 이 변수 표현식에는 스프링이 제공하는 표현식을 사용할 수 있습니다. (SpringEL)
지역 변수 선언
th:with 를 사용하면 지역 변수를 선언해서 사용할 수 있습니다.
- 지역 변수는 선언한 태그 안에서만 사용 가능
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/variable")
public String variable(Model model) {
User userA = new User("userA", 10);
User userB = new User("userB", 20);
User userC = new User("userC", 30);
List<User> list = new ArrayList<>();
list.add(userA);
list.add(userB);
list.add(userC);
Map<String, User> map = new HashMap<>();
map.put("userA", userA);
map.put("userB", userB);
map.put("userC", userC);
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;
}
}
}
자동 변수 추출하기 : Ctrl + Alt + V
resources > templates > basic > variable.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<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 (th:text="${users[index].username})
<li>${users[0].username} = <span th:text="${users[0].username}"></span></li>
<li>${users[0]['username']} = <span th:text="${users[1]['username']}"></span></li>
<li>${users[0].getUsername()} = <span th:text="${users[2].getUsername()}"></span></li>
</ul>
<ul>Map (th:text="${userMap['key'].username})
<li>${userMap['userA'].username} = <span th:text="${userMap['userA'].username}"></span></li>
<li>${userMap['userA']['username']} = <span th:text="${userMap['userB']['username']}"></span></li>
<li>${userMap['userA'].getUsername()} = <span th:text="${userMap['userC'].getUsername()}"></span></li>
</ul>
<h1>지역 변수 (th:with)</h1>
<div th:with="second=${users[1]}">
<p>두번째 사람의 나이는 <span th:text="${second.age}"></span></p>
</div>
</body>
</html>
렌더링 화면
[ 타임리프가 제공하는 기본 객체들 ]
- ${#request}
- ${#response}
- ${#session}
- ${#servletContext}
- ${#locale}
이 중에서 자주 사용하는 것은 편리하게 접근하기 위해 편의 객체도 제공합니다.
- HTTP 요청 파라미터 접근: param
- 예) ${param.paramData}
- <li><a href="/basic/basic-objects?paramData=HIParam">기본 객체들</a></li>
- HTTP 세션 접근: session
- 예) ${session.sessionData}
- 스프링 빈 접근: @
- 예) ${@helloBean.hello('Spring!')}
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/basic-objects")
public String basicObjects(HttpSession session) {
session.setAttribute("sessionData", "HI Session");
return "basic/basic-objects";
}
@Component("helloBean")
static class helloBean {
public String hello(String data) {
return "HI " + data;
}
}
}
resources > templates > basic > text-objects.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<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('Thymeleaf!')}"></span></li>
</ul>
</body>
</html>
렌더링 화면
[ 유틸리티 객체 ]
타임리프는 문자, 숫자, 날짜, URI등을 편리하게 다루는 다양한 유틸리티 객체들을 제공합니다. 다행히 공식문서 해당 부분이 잘 설명이 되어있어 필요할 때마다 찾아서 적용하시면 될 것 같습니다.
타임리프 유틸리티 객체들 소개
- #message : 메시지, 국제화 처리
- #uris : URI 이스케이프 지원
- #dates : java.util.Date 서식 지원
- #calendars : java.util.Calendar 서식 지원
- #temporals : 자바8 날짜 서식 지원
- #numbers : 숫자 서식 지원
- #strings : 문자 관련 편의 기능
- #objects : 객체 관련 기능 제공
- #bools : boolean 관련 기능 제공
- #arrays : 배열 관련 기능 제공
- #lists , #sets , #maps : 컬렉션 관련 기능 제공
- #ids : 아이디 처리 관련 기능 제공
타임리프 유틸리티 객체
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expression-utilityobjects
유틸리티 객체 예시
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#appendix-b-expressionutility-objects
그중에서 [ 날짜 ]는 자주 사용하는 것이므로 코드로 알아보도록 하겠습니다.
자바8 날짜용 유틸리티 객체 #temporals
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/date")
public String date(Model model) {
model.addAttribute("localDateTime", LocalDateTime.now()); // 현재 시간
return "basic/date";
}
}
resources > templates > basic > date.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>LocalDateTime</h1>
<ul>
<li>default = <span th:text="${localDateTime}"></span></li>
<li>yyyy-MM-dd HH:mm:ss 포맷팅= <span th:text="${#temporals.format(localDateTime,'yyyy-MM-dd HH:mm:ss')}"></span></li>
</ul>
<h1>LocalDateTime - Utils</h1>
<ul>
<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>
<li>${#temporals.year(localDateTime)} = <span th:text="${#temporals.year(localDateTime)}"></span></li>
<li>${#temporals.dayOfWeek(localDateTime)} = <span th:text="${#temporals.dayOfWeek(localDateTime)}"></span></li>
<li>${#temporals.dayOfWeekName(localDateTime)} = <span th:text="${#temporals.dayOfWeekName(localDateTime)}"></span></li>
<li>${#temporals.dayOfWeekNameShort(localDateTime)} = <span th:text="${#temporals.dayOfWeekNameShort(localDateTime)}"></span></li>
<li>${#temporals.hour(localDateTime)} = <span th:text="${#temporals.hour(localDateTime)}"></span></li>
<li>${#temporals.minute(localDateTime)} = <span th:text="${#temporals.minute(localDateTime)}"></span></li>
<li>${#temporals.second(localDateTime)} = <span th:text="${#temporals.second(localDateTime)}"></span></li>
<li>${#temporals.nanosecond(localDateTime)} = <span th:text="${#temporals.nanosecond(localDateTime)}"></span></li>
</ul>
</body>
</html>
렌더링 화면
[ URL 링크 ]
타임리프에서 URL을 생성할 때는 @{...} 문법을 사용하면 됩니다.
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/link")
public String link(Model model) {
model.addAttribute("param1", "Hi");
model.addAttribute("param2", "Lea");
return "basic/link";
}
}
resources > templates > basic > link.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>URL 링크</h1>
<ul>
<li><a th:href="@{/link}">basic url</a></li>
<li><a th:href="@{/link(param1=${param1}, param2=${param2})}">query param</a></li>
<li><a th:href="@{/link/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>
<li><a th:href="@{/link/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>
</ul>
</body>
</html>
뒤에 있는 ${param1}이 {param1}에 바인딩됩니다.
<li><a th:href="@{/link/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>
렌더링 화면
페이지 소스 보기를 해서 확인해봅시다.
[ 리터럴 Literals ]
리터럴은 소스 코드상에 고정된 값을 말하는 용어입니다.
리터럴
- 문자: 'LeaHI'
- 숫자: 10
- 불린: true , false
- null: null
정말 쉬운 내용이지만 문자 리터럴을 처음 사용할 때 자주 하는 실수가 있기에 한 번 짚고 넘어가고자 합니다.
💡 타임리프에서 문자 리터럴은 항상 ' '(작은따옴표)로 감싸야합니다. 💡
<span th:text=" 'hello' ">
하지만 문자를 항상 ' '로 감싸는 것은 너무 번거로운 일입니다. 타임리프 개발자도 이를 인지했는지
공백 없이 쭉 이어진다면 하나의 의미 있는 토큰으로 인지해서 작은따옴표를 생략할 수 있도록 하였습니다.
A-Z , a-z , 0-9 , [] , . , - , _
<span th:text="hello"> → OK
<span th:text="hello world!"></span>
중간에 공백이 있기에 하나의 의미 있는 토큰으로도 인식되지 않습니다.(오류)
→ 수정
<span th:text=" 'hello world!' "></span> → OK
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/literal")
public String literal(Model model) {
model.addAttribute("data", "LeaCoding!");
return "basic/literal";
}
}
resources > templates > basic > literal.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>리터럴</h1>
<ul>
<li>'Welcome' + ' LeaCoding!' = <span th:text="'Welcome' + ' LeaCoding!'"></span></li>
<li>'Welcome LeaCoding!' = <span th:text="'Welcome LeaCoding!'"></span></li>
<li>'Welcome ' + ${data} = <span th:text="'Welcome ' + ${data}"></span></li>
<li>리터럴 대체 |Welcome ${data}| = <span th:text="|Welcome ${data}|"></span></li>
</ul>
</body>
</html>
렌더링 화면
[ 연산 ]
타임리프 연산은 자바와 크게 다르지 않지만
HTML안에서 사용하기 때문에 HTML 엔티티를 사용하는 부분만 주의하시면 될 것 같습니다.
비교 표현식
구분 | 표현식 | 비교 |
크다 | a gt b | a > b |
작다 | a lt b | a < b |
크거나 같다 | a ge b | a >= b |
작거나 같다 | a le b | a <= b |
같다 | a eq b | a == b |
같지 않다 | a ne b | a != b |
여기서 주의할 부등호 연산자
> → >
< → <
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/operation")
public String operation(Model model) {
model.addAttribute("nullData", null);
model.addAttribute("data", "Hi Lea!");
return "basic/operation";
}
}
resources > templates > basic > operation.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
<li>산술 연산
<ul>
<li>10 + 2 = <span th:text="10 + 2"></span></li>
<li>10 % 2 == 0 <span th:text="10 % 2 == 0"></span></li>
</ul>
</li>
<li>비교 연산
<ul>
<li>3 > 5 = <span th:text="1 > 10"></span></li>
<li>3 < 5 = <span th:text="1 < 10"></span></li>
<li>3 >= 5 = <span th:text="3 >= 5"></span></li>
<li>3 <= 5 = <span th:text="3 <= 5"></span></li>
<li>3 == 5 = <span th:text="3 == 5"></span></li>
<li>3 != 5 = <span th:text="3 != 5"></span></li>
</ul>
</li>
<li>조건식
<ul>
<li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)?'짝수':'홀수'"></span></li>
</ul>
</li>
<li>Elvis 연산자 (축약 조건식)
<ul>
<li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가 없습니다.'"></span></li>
<li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?:'데이터가 없습니다.'"></span></li>
</ul>
</li>
<li>No-Operation ( _ : HTML내용 그대로 출력 )
<ul>
<li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</span></li>
<li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가 없습니다.</span></li>
</ul>
</li>
</ul>
</body>
</html>
No-Operation
_ 인 경우 마치 타임리프가 실행되지 않는 것처럼 동작하기 때문에 HTML 내용 그대로 활용할 수 있습니다.
렌더링 화면
[ 속성 설정 및 추가, checked 처리 ]
타임리프 태그 속성(Attribute)
타임리프는 주로 HTML 태그에 th:* 속성을 지정하는 방식으로 동작합니다. th:* 로 속성을 적용하면 기존 속성을 대체하고 기존 속성이 없으면 새로 만듭니다. '기존 속성을 대체한다'는 부분은 코드를 통해 알아보도록 하겠습니다.
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/attribute")
public String attribute() {
return "basic/attribute";
}
}
resources > templates > basic > attribute.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<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>
<h2>thymeleaf</h2>
- checked o <input type="checkbox" name="active" th:checked="true" /><br/>
- checked x <input type="checkbox" name="active" th:checked="false" /><br/>
<h2>HTML</h2>
- checked=false 처리 했을 경우 <input type="checkbox" name="active" checked="false" /><br/>
</body>
</html>
1. 속성 설정
위에서 언급한 [ th:* 속성을 지정하면 타임리프는 기존 속성을 th:* 로 지정한 속성으로 대체한다. ]는 의미는
<input type="text" name="mock" th:name="userA" />을 타임리프 렌더링 후에는 기존 name="mock"을 "userA"로 대체한다는 의미입니다.
<input type="text" name="userA" />
2. 속성 추가
- th:attrappend : 속성 값의 뒤에 값을 추가
- th:attrprepend : 속성 값의 앞에 값을 추가한다.
- th:classappend : class 속성에 이어서 추가(띄어쓰기 상관하지 않음)
3. checked 처리
- HTML
checked 속성이 있으면(type="checkbox") checked 속성의 값(true, false)과 상관없이 체크가 됩니다. - thymeleaf
th:checked 는 값에 따라 체크 유무가 결정됩니다. 이런 부분이 true , false 값을 주로 사용하는 개발자 입장에서 편리하게 사용할 수 있는 장점입니다.
렌더링 화면
페이지 소스 보기
[ 반복 ]
타임리프에서 반복은 th:each 를 사용합니다. 추가적으로 반복의 상태 값도 지원합니다.(count, size, even, odd..)
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/each")
public String each(Model model) {
addUsers(model);
return "basic/each";
}
private void addUsers(Model model) {
List<User> list = new ArrayList<>();
list.add(new User("userA", 11));
list.add(new User("userB", 22));
list.add(new User("userC", 33));
model.addAttribute("users", list);
}
}
resources > templates > basic > each.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<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>
<h1>반복 상태 확인</h1>
<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>
</body>
</html>
1. ${users} 에 컬렉션을 담고 하나씩 꺼내서 user 변수에 넣고 프로퍼티 접근법으로 객체 속성에 접근합니다.
<tr th:each="user : ${users}">
<td th:text="${user.username}">username</td>
<td th:text="${user.age}">0</td>
</tr>
2. th:each 는 List 뿐만 아니라 배열, Map, java.util.Iterable , java.util.Enumeration 을 구현한 모든 객체를 반복에 사용할 수 있습니다.
3. 반복의 두 번째 파라미터를 설정해서 반복의 상태를 확인할 수 있습니다. 두 번째 파라미터는 생략 가능한데, 생략하면 지정한 변수명( user ) + Stat 가 됩니다. 여기서는 user + Stat = userStat 이므로 생략 가능합니다.
<tr th:each="user, userStat : ${users}">
반복 상태 확인
- index : 0부터 시작하는 값
- count : 1부터 시작하는 값
- size : 전체 사이즈
- even , odd : 홀수, 짝수 여부( boolean )
- first , last :처음, 마지막 여부( boolean )
- current : 현재 객체
홀수, 짝수 여부 또는 처음, 마지막 여부를 잘 활용하면 표의 특정 부분에 변화를 줄 수도 있습니다.
렌더링 화면
[ 조건부 평가 - if, unless ]
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/condition")
public String condition(Model model) {
addUsers(model);
return "basic/condition";
}
private void addUsers(Model model) {
List<User> list = new ArrayList<>();
list.add(new User("userA", 11));
list.add(new User("userB", 22));
list.add(new User("userC", 33));
model.addAttribute("users", list);
}
}
resources > templates > basic > condition.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>조건부 평가 - if, unless</h1>
<table border="1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
</tr>
<tr th:each="user, userStat : ${users}">
<td th:text="${userStat.count}"></td>
<td th:text="${user.username}"></td>
<td>
<span th:text="${user.age}"></span>
<span th:text="'초등학생'" th:if="${user.age lt 14}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
</td>
</tr>
</table>
<h1>switch</h1>
<table border="1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
</tr>
<tr th:each="user, userStat : ${users}">
<td th:text="${userStat.count}"></td>
<td th:text="${user.username}"></td>
<td th:switch="${user.age}">
<span th:case="11">11살</span>
<span th:case="22">22살</span>
<span th:case="*">청년</span>
</td>
</tr>
</table>
</body>
</html>
1. if, unless
타임리프는 해당 조건이 맞지 않으면 태그 자체를 렌더링 하지 않고 사라집니다.
2. switch
* 은 만족하는 조건이 없을 때 사용하는 디폴트입니다.
[ 주석 ]
주석에는 표준 HTML 주석, 타임리프 파서 주석, 타임리프 프로토타입 주석이 있습니다. 이중에서도 자주 쓰이는
타임리프 파서 주석에 대해 알아보도록 하겠습니다.
<!--/* 주석 */-->
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/comments")
public String comments(Model model) {
model.addAttribute("data", "Welcome Summer!");
return "basic/comments";
}
}
resources > templates > basic > comments.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<span th:text="${data}">html data</span>
<h1>타임리프 파서 주석</h1>
<!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->
</body>
</html>
렌더링 화면
페이지 소스 보기
타임리프 파서 주석을 활용할 경우 렌더링, 페이지 소스보기에서도 나타나지 않음을 알 수 있습니다.
[ 블록 ]
블록은 HTML 태그가 아닌 타임리프의 유일한 자체 태그로 <th:block>를 사용합니다.
- div를 여러 개를 돌리고 싶을 때 사용합니다, 만약 div 한 개만 돌리고 싶을 경우에는 위에서 배운 th:each를 사용하면 됩니다.
- <th:block>은 렌더링 시 제거됩니다.
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/block")
public String block(Model model) {
addUsers(model);
return "basic/block";
}
}
resources > templates > basic > block.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<th:block th:each="user : ${users}">
<div>
사용자 이름: <span th:text="${user.username}"></span>
사용자 나이: <span th:text="${user.age}"></span>
</div>
<div>
요약 <span th:text="${user.username} + ' / ' + ${user.age}"></span>
</div>
</th:block>
</body>
</html>
렌더링 화면
[ 자바스크립트 인라인 ]
타임리프는 자바스크립트에서 타임리프를 편리하게 사용할 수 있는 자바스크립트 인라인 기능을 제공합니다.
<script th:inline="javascript">
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/javascript")
public String javascript(Model model) {
model.addAttribute("user", new User("userAB", 12));
return "basic/javascript";
}
}
resources > templates > basic > javascript.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- 자바스크립트 인라인 사용 전 -->
<script>
// 텍스트 렌더링
var username = [[${user.username}]];
var age = [[${user.age}]];
// 자바스크립트 내추럴 템플릿 - 렌더링 내용이 주석처리 됨
var username2 = /*[[${user.username}]]*/ "html username";
// 객체 - 객체의 toString()이 호출
var user = [[${user}]];
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script th:inline="javascript">
// 텍스트 렌더링 - 문자 타입인 경우 " "를 포함 시켜줌
var username = [[${user.username}]];
var age = [[${user.age}]];
// 자바스크립트 내추럴 템플릿 - 주석 부분이 제거되고, 예상한 "userAB" 출력
var username2 = /*[[${user.username}]]*/ "html username";
// 객체 - JSON으로 자동 변환
var user = [[${user}]];
</script>
</body>
</html>
페이지 소스 보기로 확인
자바스크립트 안에서 each
또한 자바스크립트 안에서 each를 사용해야 하는 경우가 있습니다. 이 부분은 코드로 바로 알아보도록 하겠습니다.
basic > BasicController 추가
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/javascript")
public String javascript(Model model) {
model.addAttribute("user", new User("userAB", 12));
addUsers(model);
return "basic/javascript";
}
private void addUsers(Model model) {
List<User> list = new ArrayList<>();
list.add(new User("userA", 11));
list.add(new User("userB", 22));
list.add(new User("userC", 33));
model.addAttribute("users", list);
}
}
resources > templates > basic > javascript.html 추가
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
[# th:each="user, userStat : ${users}"]
var user[[${userStat.count}]] = [[${user}]];
[/]
</script>
</body>
</html>
페이지 소스 보기 결과
제가 타임리프 공부를 시작했던 이유는 스프링 부트와 통합이 잘 되어 있다는 점도 있겠지만
앞으로 소개드릴 템플릿 조각, 템플릿 레이아웃 기능 때문이었습니다. 여러 페이지를 작업할 때 공통으로 쓰이는 부분을 어떻게 하면 효율적으로 분리해낼 까 고민한 결과였습니다.
이 부분은 처음 공부할 땐 헷갈릴 수 있으니 독자 여러분들도 코드를 직접 치고 실행해보면서 비교해보시기 바랍니다!!
[ 템플릿 조각 ]
템플릿을 공부하기 위한 Controller를 하나 만들어보겠습니다.
basic > TemplateController
package example.thymeleaf.basic;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/template")
public class TemplateController {
@GetMapping("/fragment")
public String template() {
return "template/fragment/fragmentMain";
}
}
resources/templates/template/fragment/footer.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<!--th:fragment 가 있는 태그는 다른곳에 포함되는 코드 조각으로 이해-->
<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>
resources/templates/template/fragment/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>
<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('넘길 데이터1', '넘길 데이터2')}"></div>
</body>
</html>
렌더링 화면
페이지 소스 보기
1. "~{template/fragment/footer :: copy}" 의미는 template/fragment/footer.html 템플릿에 있는 th:fragment="copy"라는 부분을 템플릿 조각으로 가져와서 사용한다는 의미입니다.
2. 부분 포함 insert VS 부분 포함 replace
- th:insert를 사용하면 현재 태그( div )는 그대로 두고 내부에 추가한다.
- th:replace를 사용하면 현재 태그( div )를 대체한다.
[ 템플릿 레이아웃 1 ]
위에서 배운 템플릿 조각은 일부 코드 조각을 가지고 와서 사용했다면, 이번에는 개념을 더 확장해서 코드 조각을 레이아웃에 넘겨서 사용하는 방법에 대해서 알아보도록 하겠습니다.
예를 들자면, 공통으로 사용하는 css , javascript 같은 정보는 한 곳에 모아둬서 여러 페이지에 공통으로 사용하고,
각 페이지마다 새롭게 필요한 정보는 추가해서 사용하고 싶을 때 사용할 수 있습니다.
basic > TemplateController 추가
@Controller
@RequestMapping("/template")
public class TemplateController {
@GetMapping("/layout")
public String layout() {
return "template/layout/layoutMain";
}
}
resources/templates/template/layout/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>
resources/templates/template/layout/layoutMain.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
<!--바뀔 title-->
<title>메인 타이틀</title>
<!--바뀔 link-->
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>
<body>
메인 컨텐츠
</body>
</html>
렌더링 화면
페이지 소스 보기
1. common_header(~{::title},~{::link})
- ::title 은 현재 페이지의 title 태그들을 전달한다.
- ::link는 현재 페이지의 link 태그들을 전달한다.
[ 템플릿 레이아웃 2 ]
앞서 공부한 부분을 더 확장해서 이번에는 <html> 전체에 적용해보도록 하겠습니다.
이 글만 보고는 이해가 바로 가기 어려우실 테니 바로 코드를 소개하고 추가적으로 설명드리겠습니다.
이해를 위해 주석을 꼼꼼히 달았습니다. ㅎㅎ
basic > TemplateController 추가
@Controller
@RequestMapping("/template")
public class TemplateController {
@GetMapping("/layoutExtend")
public String layoutExtends() {
return "template/layoutExtend/layoutExtendMain";
}
}
resources/templates/template/layoutExtend/layoutFile.html
<!DOCTYPE html>
<!--여러 페이지에 가져다 쓸 전체 레이아웃 html-->
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<title th:replace="${title}">바뀔 레이아웃 타이틀</title>
</head>
<body>
<h1>레이아웃 html</h1>
<div th:replace="${content}">
<p>바뀔 레이아웃 컨텐츠</p>
</div>
<!--이 부분은 모든 페이지 적용-->
<footer>
레이아웃 푸터
</footer>
</body>
</html>
resources/templates/template/layoutExtend/layoutExtendMain.html
<!DOCTYPE html>
<!--<html></html> 안에 있는 부분이 template/layoutExtend/layoutFile로 대체된다-->
<html th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title},~{::section})}" xmlns:th="http://www.thymeleaf.org">
<head>
<!--넘길 title-->
<title>메인 페이지 타이틀</title>
</head>
<body>
<!--넘길 section-->
<section>
<p>메인 페이지 컨텐츠</p>
</section>
</body>
</html>
렌더링 화면
페이지 소스 보기
1. layoutFile.html
이 페이지는 공통으로 쓸 기본 레이아웃을 가지고 있습니다. th:fragment 속성이 정의되어 있음으로써 필요한 내용을 전달받아 부분 부분 변경할 수 있습니다.
2. layoutExtendMain.html
layoutExtendMain.html 는 현재 페이지로 th:replace를 사용해서 <html></html> 안에 있는 부분이 template/layoutExtend/layoutFile로 대체되는 것을 알 수 있습니다.
한 포스팅에 쭉 정리하려다 보니 너무 오래 걸렸습니다. 하지만 이 부분만 숙지하더라도 자주 사용하는 기능은 어느 정도 커버가 될 것이고 공식 문서가 워낙 잘 되어있어서 어렵지 않게 사용할 수 있을 것이라고 생각합니다
타임리프를 사용하면 html을 최대한 해치지 않고 가독성 높게 사용할 수 있는 장점뿐만 아니라 jsp로 되어있는 옛날 코드나 부트스트랩을 사용할 때도 유용함을 느꼈습니다.
개인적으로 jsp는 코드 가독성이 나쁘다고 느꼈고 현업에서 프론트단을 빠르게 만들기 위해 부트스트랩을 이용했었는데 그때 타임리프를 더 잘 알았다면 야근을 하지 않았을 텐데,,, 라고 깨닫는 시간이었습니다... ㅎㅎ
저는 이를 바탕으로 토이 프로젝트를 만들 예정이고(인스타그램 또는 마켓 컬리 클론 코딩) 타임리프와 관련된 블로그나 코드를 작성하게 된다면 추가적으로 링크를 달아두도록 하겠습니다. 감사합니다!
참고:
'Spring' 카테고리의 다른 글
스프링IoC 컨테이너 (Feat. 빈(Bean), 빈 스코프(Bean Scope)) (0) | 2022.10.24 |
---|---|
Spring, Spring boot란? (컨테이너, DI, IoC) (2) | 2022.09.02 |
Spring Boot, Spring Security를 이용한 JWT 인증·인가 구현 (1/2) (5) | 2022.08.01 |
[Spring Security] JWT(Json Web Token)란? (0) | 2022.07.27 |
[Spring Security] 인증 방식 비교(서버 기반 인증, 토큰 기반 인증) (0) | 2022.07.27 |