1. 타임리프 소개
타임리프의 특징은 다음과 같다.
- 서버 사이드 HTML 렌더링 (SSR)
- 타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용된다.
- 네츄럴 템플릿
- 타임리프는 순수 HTML을 최대한 유지한다. 타임리프로 작성한 파일은 HTML을 유지하기 때문에 웹 브라우저에서 파일을 직접 열어도 내용 확인이 가능하다. 또한 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.
- JSP를 포함한 다른 뷰 템플릿들은 소스코드와 HTML이 뒤죽박죽 섞여있기 때문에 웹 브라우저로 접근시 정상적인 HTML 결과를 확인하기 힘들다. 하지만 타임리프는 웹 브라우저에서 열어도 정상적인 결과 확인이 가능하다. 이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 것이 타임리프의 특징이고, 이러한 특징을 가진 뷰 템플릿을 네츄럴 템플릿이라고 부른다.
- 스프링 통합 지원
- 타임리프는 스프링과 자연스럽게 통합되고, 스프링의 다양한 기능을 편리하게 사용할 수 있다.
타임리프를 사용하려면 먼저 "<html xmls:th="http://www.thymeleaf.org">" 타임리프를 선언해야 한다.
2. 텍스트 - text, utext
먼저 타임리프의 갖아 기본적인 텍스트를 출력하는 기능을 살펴보자. 타임리프는 기본적으로 HTML 태그 속성에 기능을 적용하여 동작한다. HTML의 콘텐츠에 데이터를 출력할 때는 아래와 같이 "<th:text>"를 사용하면 된다.
- <span th:text="${data}">
만약 HTML 태그 속성이 아니라 컨텐츠 안에서 출력하고 싶다면 "[[...]]"를 사용하면 된다.
- [[${data}]]
먼저 요청 url을 처리할 컨트롤러 하나를 만들어보자.
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("text-basic")
public String textBasic(Model model) {
model.addAttribute("data", "Hello ThymeLeaf!");
return "basic/text-basic";
}
}
위에서 배운 내용을 바탕으로 ThymeLeaf를 작성해보면 아래처럼 사용해볼 수 있다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1> 컨텐츠에 Data 출력 </h1>
<ul>
<li>th:text 사용 (속성) <span th:text="${data}"></span></li>
<li>th:text 사용 (컨텐츠 안) <span>[[${data}]]</span></li>
</ul>
</body>
</html>
이때 아래와 같이 결과가 나온다.
이때 주의해야할 점이 있다. HTML 문서는 "<", ">"같은 특수 문자를 기반으로 정의된다. 따라서 뷰 템플릿으로 HTML 화면을 생성할 때 출력하는 데이터에 이러한 특수문자가 있는지 주의해야 한다. 이전 컨트롤러에서 data에 전달하는 문자열을 아래와 같이 바꿔보자.
- "Hello <b>Spring!</b>"
그러면 아래와 같이 출력이 나온다.
우리는 ThymeLeaf라는 문자를 진하게 하면서 문자를 강조하고 싶었는데 이렇게 된 것이다. 그럼 왜 이렇게 된걸까? 소스를 봐보자. 그러면 아래와 같이 소스가 작성된 것을 확인할 수 있다.
<li>th:text 사용 (속성) <span>Hello <b>ThymeLeaf!</b></span></li>
<li>th:text 사용 (컨텐츠 안) <span>Hello <b>ThymeLeaf!</b></span></li>
- 이렇게 "$lt;"와 같은 것을 "HTML 엔티티"라고 부른다. 개발자가 의도한 것은 <b> 태그가 있으면 해당 부분을 강조하는 것이 목적이었다. 근데 이 b가 화면에 그대로 나왔고, 소스를 보니 "<"가 "<"로 변경되었다. 타임리프는 이 태그를 출력할 때 그냥 출력하는 것이 아니라 HTML 엔티티로 바꿔서 출력하는 거다. 웹 브라우저는 이런 크다, 작다 부등호를 HTML 태그의 시작으로 인식한다. 그래서 이런 거를 태그의 시작이 아니라 문자 표현할 수 있는 방법이 필요하다.
- 이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 "이스케이프(escape)"라고 부른다. "th:text"와 "[[...]]"는 기본적으로 escape를 제공한다.
이러한 escape 기능을 사용하지 않으려면 "th:utext", "[(...)]" 두 가지를 사용하면 된다. 이러한 기능을 unescape라고 부른다.
- 기본적으로는 escape를 사용하고, 꼭 필요한 경우만 unescape를 사용하자.
3. 변수 - SpringEL
타임리프에서 변수를 사용할 때는 변수 표현식인 "${...}"를 사용하면 된다. 이 변수 표현식에는 SpringEL이라는 스프링이 제공하는 표현식을 사용할 수 있다. 먼저 실습을 위한 컨트롤러는 다음과 같다.
@GetMapping("/variable")
public String variable(Model model) {
User userA = new User("userA", 10);
User userB = new User("userB", 20);
List<Object> 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;
}
}
- User 객체, User객체를 담은 List와 Map을 Model에 넘겨준다.
이제 해당 친구들을 어떻게 ThymeLeaf를 통해 조회할 수 있는지 살펴보자.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>SpringEL 표현식</h1>
<ul> <b> Object </b>
<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> <b> List </b>
<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> <b> Object </b>
<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>
- user.username, user['username'] 모두 username을 프로퍼티 접근하는 방법이다. 모두 user.getUsername()이 된다.
- user[0].username, user[0]['username']: list.get(0).getUsername()
- userMap['userA'].username, userMap['userA']['username']: map.get("userA").getUsername()
이때, 타임리프 안에서도 <th:with>를 통해 지역변수를 선언할 수 있다.
<div th:with"first=${users[0]}">
<p><span th:text=${first.username}></span></p>
</div>
위처럼 사용하면 users[0]가 first에 담아진다. 따라서 first.username을 통해 username에 접근할 수 있는 것이다.
4. 기본 객체들
타임리프는 스프링 3.0 이전에 아래와 같은 기본 객체들을 제공한다.
- ${#request}
- ${#response}
- ${#session}
- ${#servletContext}
- ${#locale}
단, #request는 HttpServletRequest 객체를 그대로 제공했다. 따라서 데이터 조회시 request.getParameter("data")로 접근해야 한다. 이를 위해 편리한 몇 가지 편의 객체도 지원한다.
- HTTP 요청 파라미터: param → ${param.paramData}
- HTTP 세션 접근: session → ${session.sessionData}
- 스프링 빈 접근: @ → ${@helloBean.hello{'spring!')}
스프링 3.0의 경우 해당 객체들을 사용하기 위해 아래와 같이 모델에 직접 추가해야 한다. 실습을 위해 사용할 컨트롤러와 스프링 빈은 아래와 같다.
@GetMapping("basic-objects")
public String basicObjects(Model model, HttpServletRequest request, HttpServletResponse response, HttpSession session) {
session.setAttribute("sessionData", "I'm session Data");
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 "I'm SpringBean data: "+data;
}
}
해당 데이터들은 타임리프에서 아래와 같이 접근할 수 있다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1> 모델에 담아서 가져오는 법 </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>
- locale의 경우 ${#locale}로 접근이 가능하다.
"http://localhost:8080/basic/basic-objects?paramData=HelloParam" 요청의 결과는 다음과 같다.
5. 유틸리티 객체와 날짜
타임리프는 문자, 숫자, 날짜, URI 등을 편리하게 다루는 다양한 유틸리티 객체들을 제공한다. 아래와 같은 유틸리티 객체를 지원한다.
- #message: 메시지, 국제화 처리
- #uris: URI 이스케이프 지원
- #dates: 'java.util.Date' 서식 지원
- #calendars: 'java.util.Calendar' 서식 지원
- #temporals: 자바8 날짜 서식 지원
- #numbers: 숫자 서식 지원
- #strings: 문자 관련 편의 기능
- #objects: 객체 관련 기능
- #bools: boolean 관련 기능
- #arrays: 배열 관련 기능
- #lists, #sets, #maps: 컬렉션 관련 기능
- #ids: 아이디 처리 관련 기능
메뉴 및 예시는 다음을 참고하자.
단, 타임리프에서 자바8 날짜인 LocalDate, LocalDateTime, Instant를 사용하려면 아래의 추가 라이브러리가 필요하다.
- thymeleaf-extras-java8time
하지만 스프링부트 타임리프를 쓰면 기본으로 들어가 있으며 "#temporals"를 사용해 쓰면 된다. 실습을 위해 컨트롤러를 아래와 같이 작성하자.
@GetMapping("/date")
public String date(Model model) {
model.addAttribute("localDateTime", LocalDateTime.now());
return "basic/date";
}
그럼 localDateTime에는 현재 시각이 들어가게 된다. 이때 #temporals를 통해 아래와 같이 활용할 수 있다.
<!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>
출력 결과는 다음과 같다.
6. URL 링크
타임리프에서 URL을 생성할 때는 "@{...}"을 사용하면 된다. 먼저 아래와 같은 컨트롤러를 생성하자.
@GetMapping("link")
public String link(Model model) {
model.addAttribute("param1", "data1");
model.addAttribute("param2", "data2");
return "basic/link";
}
이때 위에서 전달된 데이터들을 통해 Query Parameter, Path Variable을 활용할 수 있다.
<!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="@{/hello}">basic url</a></li>
<li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">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 Param</a></li>
</ul>
</body>
</html>
- 2번째 결과: http://localhost:8080/hello?param1=data1¶m2=data2
- 3번째 결과: http://localhost:8080/hello/data1/data2
- 4번째 결과: http://localhost:8080/hello/data1?param2=data2
- 단, "/hello"의 경우 절대 경로를 의미하며, "hello"로 작성하면 상대 경로를 의미한다.
7. 리터럴
리터럴이란 소스 코드상 고정된 값을 말한다. 예를 들어, 아래 코드를 보자.
String a = "Hello";
int a = 10 * 20;
위 경우 "Hello"는 문자 리터럴, 10, 20은 숫자 리터럴이다. 타임리프에는 문자('Hello'), 숫자(10, 20), 불린(true, false), null 총 네 가지 리터럴이 있다. 이때 문자 리터럴은 작은 따옴표로 감싸야 한다. 그래서 사실 이전 "th:text"는 아래와 같이 작성해야 한다.
- <span th:text="'hello'">
하지만 이처럼 작은 따옴표로 감싸는 것이 매우 귀찮기 때문에 타임리프는 공백 없이 쭉 이어지는 경우 하나의 토큰으로 인지하여 아래와 같이 따옴표를 생략할 수 있다.
- <span th:text="hello">
아래와 같이 공백이 있는 경우 에러가 발생한다.
- <span th:text="hello world!"> → <span th:text="'hello world!'">
일단 다음과 같이 컨트롤러 하나를 작성하자.
@GetMapping("/literal")
public String literal(Model model) {
model.addAttribute("data", "Spring!");
return "basic/literal";
}
그리고 타임리프는 아래와 같이 작성할 수 있다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>리터럴</h1>
<ul>
<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>리터럴 대체 = <span th:text="|hello ${data}|"></span></li>
</ul>
</body>
</html>
- 리터럴 대체(|...|): "|...|"를 사용하면 그냥 해당 내용을 통으로 문자로 본다. 그리고 ${}는 따로 변수로 처리해준다.
8. 연산
타임리프 연산은 자바와 크게 다르지 않지만 HTML 안에서 사용하므로 HTML 엔티티를 사용하는 부분만 조심하면 된다. 일단 컨트롤러를 아래처럼 작성하자.
@GetMapping("/operation")
public String operation(Model model) {
model.addAttribute("nullData", null);
model.addAttribute("data", "Spring!");
return "basic/operation";
}
그리고 아래 타임리프를 확인하자.
<!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>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>
</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
<ul>
<li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</span></li>
<li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가 없습니다.</span></li>
</ul>
</li>
</ul>
</body>
</html>
- +, %: 자바와 동일하게 산술 연산을 수행한다.
- 비교 연산은 >, gt를 쓰든 상관이 없다. 또한 >=, ge를 쓰든 상관이 없다.
- Elvis 연산자: 데이터가 없을 때 사용한다. 조건식을 좀 더 편리하게 쓸 수 있다. 만약 ${data}?: '...'와 같이 입력하면 데이터가 없으면 '...'내용이 출력되고, 있으면 data를 그대로 출력한다.
- No-Operation: ${data}?: _의 경우 Elvis 연산자 이므로 data가 있으니 data 내용이 출력된다. 하지만 nullData의 경우 No-Operation인 "_"가 작동한다. 즉, 아무것도 하지 않는다. 이 말은 해당 타임리프 태그가 무효화된다는 것이다. 따라서 그냥 기존 content 내용이 그대로 출력된다.
결과는 다음과 같다.
9. 속성 값 설정
타임리프는 HTML 태그에 "th:*" 속성을 지정한다. 이때 해당 속성을 지정하면 기존에 있던 속성을 대체하며, 기존 속성이 없었다면 새로 만들어준다. 먼저 아래와 같이 컨트롤러를 추가하자.
@GetMapping("/attribute")
public String attribute() {
return "basic/attribute";
}
Thymeleaf의 코드는 다음과 같다.
<!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>
- 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/>
</body>
</html>
- th:attrappend = 원하는 속성의 뒤에 추가로 내용을 작성해준다. 위의 경우 "text large"로 class 속성이 변경된다. 단, 위처럼 작성하지 않고 'large'로 띄어쓰기를 주지 않으면 "textlarge"로 이상하게 작성된다.
- th:attrprepend = 원하는 속성의 앞에 추가로 내용을 작성해준다. 위의 경우 "large text"로 class 속성이 변경된다. 단, 위처럼 작성하지 않고 'large'로 띄어쓰기를 주지 않으면 "largetext"로 이상하게 작성된다.
- th:classappend = attrappend와 attrprepend는 띄어쓰기를 조심해야 한다. 그래서 그냥 class의 경우 정의된 classappend를 많이 사용한다. 이 친구는 "large"라는 속성을 내가 추가한다고 하면 내가 알아서 적절하게 "text large"와 같이 속성을 추가해준다.
- <input type="checkbox" name="active" checked="false" />: HTML은 이렇게 태그를 작성해도(checked에 "false"가 들어가도), checked 속성이 있어서 checked 처리가 돼버린다. 즉, HTML은 checked의 속성값과 상관없이 checked 속성만 있어도 체크를 해준다. 타임리프의 경우 th:checked를 false로 설정하면 checked 속성 자체를 제거해준다.
해당 HTML의 결과는 다음과 같다.
10. 반복
타임리프에서 반복은 "th:each"를 쓰면 된다. 추가적으로 반복에서 사용할 수 있는 여러 상태값을 지원한다. 먼저 컨트롤러 코드는 다음과 같다.
@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", 10));
list.add(new User("userB", 20));
list.add(new User("userC", 30));
model.addAttribute("users", list);
}
이때 ThymeLeaf의 코드는 다음과 같다.
<!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>
- 위처럼 반복문은 th:each를 사용한다.
- <tr th:each="user : ${users}">: 오른쪽 컬렉션 ${users}의 값을 하나씩 꺼내서 왼쪽 변수("user")에 담아서 반복을 실행한다. th:each에는 java.util.Iterable, java.util.Enumeration을 구현한 모든 객체를 반복에 사용할 수 있다.
- <tr th:each="user, userStat : ${users}">: 반복의 두 번째 파라미터를 설정하면 반복의 상태를 확인할 수 있다. 이때 두 번째 파라미터는 생략될 수 있는데 이런 경우 (변수명[user])+"Stat"가 된다. 이때 Stat로 다음 것들을 확인할 수 있다.
- index: 0부터 시작하는 인덱스
- count: 1부터 시작하는 인덱스 (즉, 현재까지 나온 개수)
- size: (리스트의) 전체 사이즈
- even, odd: 짝수인지 홀수인지 파악
- first, last: 처음인지 마지막인지 파악
- current: 현재 객체
즉, 결과는 다음과 같다.
11. 조건부 평가
타임리프는 "if", "unless"를 지원한다. 또한 "th:switch"와 "th:case"를 통해 스위치 문도 제공한다. 이것도 일단 컨트롤러부터 살펴보자.
@GetMapping("/condition")
public String condition(Model model) {
addUsers(model);
return "basic/condition";
}
이전처럼 addUsers 메서드를 호출하고, view 이름을 반환한다. 아래는 ThymeLeaf 코드다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
</tr>
<tr th:each="user : ${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 20}"></span>
<span th:text="성인" th:unless="${user.age lt 20}"></span>
</td>
</tr>
</table>
<table border="1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
</tr>
<tr th:each="user : ${users}">
<td th:text="${userStat.count}"></td>
<td th:text="${user.username}"></td>
<td th:switch="${user.age}">
<span th:case="${10}">10대</span>
<span th:case="${20}">20대</span>
<span th:case="*">기타</span>
</td>
</tr>
</table>
</body>
</html>
- if, unless의 경우 조건이 맞지 않으면 태그 자체를 렌더링하지 않는다. 즉, 위에서 20, 30살인 유저의 경우 "미성년자"를 아예 출력하지 않는다.
해당 코드의 결과는 아래와 같다.
12. 주석
HTML 주석은 아래와 같이 사용한다.
<!-- -->
타임리프의 경우 파서 주석, 프로토타입 주석 두 종류가 있다. 파서 주석은 타입리프 파서 차원에서 주석처리를 하기 때문에 출력이 아예 안나온다. 타임리프 프로토타입 주석은 타임리프로 렌더링 됐을 때만 보이는 주석이다.
<!-- 파서 주석 -->
<!--/* */-->
<!--/*--> <!--*/-->
<!-- 프로토타입 주석 -->
<!--/*/ /*/-->
- 파서 주석: 타임리프 파서 자체가 주석으로 인정한다. 그렇기 때문에 이 부분은 렌더링시 싹 지운다. 아예 렌더링시 주석처럼 아무것도 안된다.
- 프로토타입 주석: 해당 파일을 직접 접근하면 HTML은 해당 부분을 주석으로 처리하여 렌더링되지 않는다. 하지만 서버에 직접 요청하게 되면 해당 부분을 렌더링해준다. 즉, 타임리프를 렌더링 한 경우만 보이는 기능이다.
아래는 예시 코드다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>1. 표준 HTML 주석</h3>
<!--
<span th:text="${data}">data</span>
-->
<h3>2. 타임리프 파서 주석</h3>
<!--/*[[${data}]]*/-->
<!--/*-->
<span th:text="${data}">data</span>
<!--*/-->
<h3>3. 타임리프 파서 주석</h3>
<!--/*/
<span th:text="${data}">data</span>
/*/-->
</body>
</html>
결과는 다음과 같다.
13. 블록
<th: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>
결과를 보자.
div 태그 안에서는 하나의 반복문만 수행할 수 있다. 하지만 위의 경우 두 개의 div 태그가 하나의 묶음인데, 이런 경우 <th:block> 태그를 사용해서 좀 더 편하게 할 수 있다. 단, <th:block>은 렌더링시에는 제거된다.
14. 자바스크립트 인라인
이번 시간에는 타임리프에서 자바스크립트를 사용할 수 있는 자바스크립트 인라인 기능을 살펴보자. 타임리프는 자바스크립트에서 타임리프를 편리하게 사용할 수 있는 자바스크립트 인라인 기능을 제공한다. 자바스크립트 인라인은 아래와 같이 적용하면 된다.
- <script th:inline="javascript">
그러면 이 영역안에서는 뭔가 자바스크립트랑 타임리프랑 같이 쓸 때 편한 기능을 지원해준다. 원래대로 할려면 불편한데 편리하게 제공해준다. 이 영역 안에서는 자바스크립트를 위한 모드로 동작한다. 먼저 컨트롤러 코드는 아래와 같다.
@GetMapping("/javascript")
public String javascript(Model model) {
model.addAttribute("user", new User("userA", 20));
addUsers(model);
return "basic/javascript";
}
그리고 일단 아래 코드를 보자.
<!--자바스크립트 인라인 사용 전-->
<script>
var username = [[${user.username}]];
var age = [[${user.age}]];
// 자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "username2";
// 객체
var user = [[${user}]];
</script>
- [[...]]를 사용하면 타임리프에서 안에 있는 내용을 그대로 렌더링 해준다. javasript에서 <span th:text="~">같은 내용을 사용할 수 없다. 그래서 그냥 해당 문법을 써서 javascript 변수값으로 넣어주면 된다.
- 자바 스크립트도 타임리프를 쓰면 네추럴 템플릿 기능을 지원한다. /**/는 자바스크립트 주석이다. 이때 var username2 = /*[[${user.username}]]*/ "username2"라고 하면 "username2"가 username2 변수에 저장될 것이다.
위와 같이 코드를 작성하고 F12를 눌러보면 javascript 에러가 발생한다.
userA가 정의되어 있지 않다고 한다. 즉, var username = [[${user.username}]];가 var username = "[[${user.username}]]";로 바뀌어야 한다. 그럼 해당 코드를 고치고 다시 돌려보자. 그럼 아래 에러가 발생한다.
그럼 잠시 변환된 코드를 살펴보자.
지금 이렇게 되면, 타임리프로 랜더링해도 내추럴 템플릿을 쓸 수가 없다. /*userA*/가 주석처리 되고, 이게 "username2"가 변수에 들어가버린다. 근데 inline이라는 것을 사용하면 주석이 변수에 들어가게 된다. 또한 위에서 객체를 넣으면 user객체.toString이 호출된다. 그래서 그 결과가 위와 같은 "BasicController.User(username=userA. age=20)"인 것이다. 그런데 타임리프는 아래와 같이 inline을 지원하여 앞으로 해당 구간은 자바스크립트의 영역이고, 좀 더 편하게 코딩할 수 있게 도와준다. 해당 코드는 아래와 같다.
<!--자바스크립트 인라인 사용 후-->
<script th:inline="javascript">
var username = [[${user.username}]];
var age = [[${user.age}]];
// 자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "username2";
// 객체
var user = [[${user}]];
</script>
이때 코드는 아래와 같이 작성된다.
이렇게 각 변수의 타입에 맞게 알아서 다 처리해준다. 문자열은 따옴표를 붙여주고, 숫자는 그냥 숫자로 처리한다. 그리고 주석의 경우 변수 할당을 할 때 주석 부분을 제외한 부분을 지우고 주석 부분을 값으로 사용하게 해준다. 그래서 보면 "UserA"가 들어간 것을 확인할 수 있다. 이 덕분에 자바스크립트도 네추럴 템플릿이 가능하다. 실제 html 파일을 실행시키면 "test username"이 들어갈 것이다. 근데 서버 사이드로 렌더링 되면 주석값이 렌더링된다. 또한 객체를 Json으로 넣어준다.
그리고 자바스크립트 인라인에서 가끔 each를 써야하는 경우가 있다. 이런 경우 아래와 같이 사용한다.
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]]
[/]
</script>
결과는 다음과 같이, user1, user2, user3으로 변수 이름이 설정되고, 각 변수에 각 user 객체가 json의 형태로 들어간 것을 확인할 수 있다.
15. 템플릿 조각
웹 페이지를 개발할 때는 공통 영역이 많다. 상단 영역이나, 하단 영역, 왼쪽에 네비게이션이 있거나 등등 이거는 HTML 자체를 영역 별로 재활용해야하는 경우가 많다. 타임리프는 이러한 문제를 해결하기 위해 파일을 조각조각내서 이것을 불러다가 쓸 수 있게 지원한다. 예를 들어, 페이지가 10개인데, 하단 내용이 똑같다면, 이것을 조각으로 만들고 다른 페이지에서 이를 쓰는 것이다. 이를 위해 타임리프가 지원하는 것이 템플릿 조각과 템플릿 레이아웃이다. 먼저 템플릿 조각을 살펴보자. 일단 아래는 사용할 컨트롤러 코드다.
@GetMapping("/fragment")
public String template() {
return "fragment/gragmentMain";
}
그리고 아래 ThymeLeaf 코드는 중복되는 영역에 대한 코드이다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<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>
- 우리가 HTML을 만들건데 여러 페이지에서 다 똑같은 하단 영역을 사용할 것이다. 그래서 얘가 코드 조각 역할을 할것이다. 얘는 직접적으로 불린다기 보다는 다른 타임리프 파일들이 불러다쓰는 파일이다.
- <th: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>
<h2>부분 포함 단순 표현식</h2>
<div th:replace="template/fragment/footer :: copy"></div>
<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
</body>
</html>
- <th: insert>: ~{...}을 통해 일단 원하는 조각이 있는 html 파일의 위치를 작성해야 한다. 그리고 "::" 다음에 <th:fragment>로 지정한 이름을 입력하면 된다.
- <th: replace>: insert와 사용 방법은 동일하다. 근데 지금처럼 단순하게 경로와 이름정도만 있는 경우에는 "~{...}"없이 3번째처럼 단순하게 큰따옴표를 통해 작성할 수 있다. 근데 좀 복잡해지면 이걸 못쓴다.
- 4번째 경우처럼 파라미터도 넣을 수 있다.
결과 코드를 보자.
- insert는 위와 같이 th:insert를 사용했던 div안에 가져온 copy의 내용을 집어넣는다.
- replace는 기존 태그였던 div를 copy의 내용으로 대체한다.
16. 템플릿 레이아웃1
이번 시간에는 템플릿 레이아웃에 대해 알아보자. 이전에는 일부 코드조각을 가져와서 썼다. 이번에는 좀 더 이 개념을 확장해서 내 코드 조각을 레이아웃에 넘겨보자. 예를 들어, <head>에 공통으로 사용하는 css, javasript같은 정보들이 있는데, 이런 정보를 한 곳에 넣고 쓰고 싶다. 근데 가끔 각 페이지마다 공통 헤더에 대한 레이아웃이 있는데 여기에다가 매 페이지마다 조금씩 다른 것을 헤더에 넣고 싶은 것이다. 쉽게 말하면 큰 모양이 이미 있고, 내 코드를 그 모양에 맞춰서 넣는 것이다. 일단 코드를 보자. 먼저 컨트롤러 코드다.
@GetMapping("/layout")
public String layout() {
return "template/layout/layoutMain";
}
그리고 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>
- 전체 사이트가 공통적으로 사용할 모양이다. 여러 리소드들을 가져다 쓰고 있다.
그럼 이제 layoutMain을 만들어보자.
<!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>
- 먼저 th:replace로 layout/base에 있는 해당 파일을 통해 layout을 완전히 바꿔치기 한다. 단, 이걸 그냥 바꿔치기 하지 않는다. 사실 공통 레이아웃이 있어도 각 사이트마다 조금씩 다르게 하고 싶은 것이 있다. 예를 들어, 요기서는 타이틀을 다르게하고 싶은 것이다. 그리고, 추가로 뭔가 링크 관련된 css같은 정보들을 각 페이지마다 다르게 넣고 싶은 것이다. 그러니까 원래 레이아웃 틀이 있고 거기에 내가 원하는 것을 집어 넣어서 그 레이아웃을 완성하는 것이다.
- 이때 사용한 문법이 "(~{::title},~{::link})"이다. 이 문법을 쓰면 이 타이틀 태그인 <title>메인 타이틀</title>를 여기에 넣어버린다. 타이틀 태그 자체를 집어넣는다. 이전과 개념은 비슷한데 단순히 데이터만 넣는 정도가 아니고 태그 자체를 집어 넣는다. 그래서 렌더링 되면, 이 타이틀이 replace된다.
결과는 다음과 같다.
- 중간의 공통 부분은 base.html과 동일하고, title 태그와 link 태그에 대한 것만 수정되었다. 즉, base라는 것은 하나의 거대한 layout이고, 여기에 일부분은 내가 원하는 것으로 채울 수 있다.
17. 템플릿 레이아웃2
이번에는 템플릿 레이아웃을 헤드뿐만 아니라 HTML 전체를 레이아웃으로 만들고 거기에 우리의 코드조각을 살짝 넣는 방식으로 더 확장해보자. 먼저 컨트롤러 코드는 다음과 같다.
@GetMapping("/layoutExtend")
public String layoutExtend() {
return "template/layoutExtend/layoutExtendMain";
}
그리고 기본 레이아웃 코드는 다음과 같다.
<!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>
- 우리 사이트가 100 페이지 정도 된다고 하자. 근데 이 페이지가 다 모양이 똑같다. 이때 div 부분과 title 부분만 바뀌어야 한다.
이런 경우 아래와 같이 템플릿 레이아웃을 수행해볼 수 있다.
<!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>
- <html 태그를 보면> html 자체를 방금 우리가 만든 layoutFile로 th:replace한다.
결과는 다음과 같다.
- title 태그가 잘 교체된 것을 볼 수 있다. (title 파라미터로 넘겼음)
- section 영역이 통으로 잘 넘어간 것을 확인할 수 있다. (content 파라미터로 넘겼음)
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'BackEnd > Spring' 카테고리의 다른 글
[Spring MVC-2] 메시지, 국제화 (0) | 2023.11.10 |
---|---|
[Spring MVC-2] 타임리프 - 스프링 통합과 폼 (0) | 2023.09.27 |
[Spring MVC-1] 스프링 MVC - 웹 페이지 만들기 (0) | 2023.09.24 |
[Spring MVC-1] 스프링 MVC - 기본 기능 (0) | 2023.09.23 |
[Spring MVC-1] 스프링 MVC - 구조 이해 (0) | 2023.09.22 |