타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링하는 용도로 사용된다.
타임리프는 순수 HTML을 최대한 유지하는 특징이 있다.
타임리프로 작성한 파일은 HTML을 유지하기 때문에 웹 브라우저에서 파일을 직접 열어도 내요을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.
JSP를 포함한 다른 뷰 템플릿들은 해당 파일을 열면, 예를 들어서 JSP 파일 자체를 그대로 웹 브라우저에서 열어보면 JSP 소스코드와 HTML이 뒤죽박죽 섞여서 웹 브라우저에서 정상적인 HTML 결과를 확인할 수 있다. 오직 서버를 통해서 JSP가 렌더링되고 HTML 응답 결과를 받아야 화면을 확인할 수 있다.
반면에 타임리프로 작성된 파일은 해당 파일을 그대로 웹 브라우저에서 열어도 정상적인 HTML 결과를 확인할 수 있다. 물론 이 경우 동적으로 결과가 렌더링 되지는 않는다. 하지만 HTML 마크업 결과가 어떻게 되는지 파일만 열어도 바로 확인할 수 있다.
이렇게 순수 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 태그의 속성에 기능을 정의해서 동작한다. HTML의 콘텐츠(content)에 데이터를 출력할 때는 다음과 같이 th:text를 사용하면 된다.
<span th:text="${data}">
HTML 태그의 속성이 아니라 HTML 콘텐츠 영역안에서 직접 데이터를 출력하고 싶으면 다음과 같이[[...]] 을 사용하면 된다.
컨텐츠 안에서 직접 출력하기 = [[${data}]]
BasicController
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/text-basic")
public String textBasic(Model model) {
model.addAttribute("data", "Hello Spring!");
return "basic/text-basic";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>컨텐츠에 데이터 출력하기</h1>
<ul>
<li>th:text 사용 <span th:text="${data}"></span></li>
<li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li>
</ul>
</body>
</html>
HTML 문서는 <,> 같은 특수 문자를 기반으로 정의된다. 따라서 뷰 템플릿으로 HTML 화면을 생성할 때는 출력하는 데이터에 이러한 특수 문자가 있는 것을 주의해서 사용해야 한다.
앞에서 만든 예제의 데이터를 다음과 같이 변경해서 실행해보자.
변경 전
"Hello Spring!"
변경 후
"Hello <b>Spring!<b>"
b 태그를 사용해서 Spring! 이라는 단어가 진하게 나오도록 해보자.
웹 브라우저에서 실행결과를 보자.
Hello <b>Spring!</b>Hello <b>Spring!</b>개발자가 의도한 것은 <b> 가 있으면 해당 부분을 강조하는 것이 목적이었다. 그런데 <b> 태그가 그대로 나온다. 소스보기를 하면 < 부분이 <로 변경된 것을 확인할 수 있다.
HTML 엔티티
웹 브라우저는 < 를 HTML 태그의 시작으로 인식한다. 따라서 < 태그를 시작이 아니라 문자로 표현할 수 있는 방법이 필요한데, 이것을 HTML 엔티티라 한다. 그리고 이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 이스케이프(escape)라 한다. 그리고 타임리프가 제공하는 th:text, [[...]] 는 기본적이스 이스케이프를 제공한다.
이스케이프 기능을 사용하지 않으려면 어떻게 해야할까?
타임리프는 다음 두 기능을 제공한다.
th:text -> th:utext[[...]] -> [(...)]BasicController에 추가
@GetMapping("/text-unescaped")
public String textUnescaped(Model model) {
model.addAttribute("data", "Hello <b>Spring!</b>");
return "basic/text-unescaped";
}
<!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>
th:inline:"none": 타임리프는 [[...]]를 해석하기 때문에, 화면에 [[...]] 글자를 보여줄 수 없다. 이 태그 안에서는 타임리프가 해석하지 말라는 옵션이다.주의 !
실제 서비스를 개발하다 보면 escape를 사용하지 않아서 HTML이 정상 렌더링 되지 않는 수 많은 문제가 발생한다. escape를 기본으로 하고, 꼭 필요할 때만 unescape를 사용하자.
타임리프에서 변수를 사용할 때는 변수 표현식을 사용한다.
변수 표현식: ${...}
그리고 이 변수 표현식에는 스프링 EL이라는 스프링이 제공하는 표현식을 사용할 수 있다.
BasicController 추가
@GetMapping("/variable")
public String variable(Model model) {
User userA = new User("userA", 10);
User userB = new User("userB", 20);
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;
}
}
<!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
<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>
</body>
</html>
SpringEL 다양한 표현식 사용
Object
user.username: user의 username을 프로퍼티 접근 -> user.getUsername()user['username']: 위와 같음user.getUsername(): user의 getUsername()을 직접 호출이하 리스트나 MAP은 인덱싱 혹은 키값으로 객체를 찾고 똑같이 username을 찾는 방식이다.
th:with를 사용하면 지역 변수를 선언해서 사용할 수 있다. 지역 변수는 선언한 태그 안에서만 사용할 수 있다.
<h1>지역 변수 - (th:with)</h1>
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
타임리프는 기본 객체들을 지원한다.
${#request} - 스프링 부트 3.0부터 제공하지 않는다.${#response} - 스프링 부트 3.0부터 제공하지 않는다.${#session} - 스프링 부트 3.0부터 제공하지 않는다.${#servletContext} - 스프링 부트 3.0부터 제공하지 않는다.${#locale}주의! - 스프링 부트 3.0
스프링 부트 3.0부터는 ${#request}, ${#response}, ${#session}, ${#servletContext}를 지원하지 않는다.
스프링 부트 3.0이라면 직접 model에 해당 객체를 추가해서 사용해야 한다.
BasicController 추가
@GetMapping("/basic-objects")
public String basicObjects(Model model,
HttpServletRequest request,
HttpServletResponse response,
HttpSession session) {
session.setAttribute("sessionData", "Hello Session");
model.addAttribute("request", request);
model.addAttribute("response", response);
model.addAttribute("servletContext", request.getServletContext());
return "basic/basic-objects";
}
@Component("helloBean")
static class HelloBean {
public String hello(String data) {
return "Hello " + data;
}
}
<!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('Spring!')}"></span></
li>
</ul>
</body>
</html>