백엔드 서버에서 HTML을 동적으로 렌더링하는 용도로 사용
순수 HTML을 최대한 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 말함
Thymeleaf vs JSP
- Thymeleaf
순수 HTML 파일을 웹브라우저에서 열면 내용을 확인할 수 있고,
서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있음
- JSP
JSP 파일은 웹브라우저에서 열면 JSP 소스코드와 HTML이 뒤죽박죽 섞여 확인이 어려움
-> 서버를 통해 JSP를 열어야 함
타임리프는 스프링을 위해 만들어진 것처럼 자연스럽게 통합됨
스프링의 다양한 기능을 편리하게 사용할 수 있도록 지원함
xmlns:th="http://www.thymeleaf.org"
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<!-- -->
</html>
HTML의 속성을 th:xxx로 변경, 기존 HTML 속성값이 없다면 새로 생성
th:xxx가 붙은 부분은 서버사이드에서 렌더링 됨
➡️ 기존 HTML 속성을 대체
HTML 파일을 직접 연다면 th:xxx가 있더라도 웹 브라우저는 이를 무시함
➡️ HTML 파일 보기를 유지하면서 템플릿 기능이 가능
Natural Templates
➡️ 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용 가능한 타임리프의 특징
ex)
<!-- href=xxx 를 th:href=xxx로 변경 -->
<link href="../ABC"
th:href="@{/abc}">
<button onclick="location.href='ABC.html'"
th:onclick="|location.href='@{/abc/def/hi}'|"
type="button"></button>
🔹 th:value="${속성 값}"
모델에 있는 member 정보를 획득하고 프로퍼티 접근법으로 출력 (member.getName())
value 속성을 th:value 속성으로 변경
ex)
th:value="${member.name}"
<button onclick="location.href='ABC.html'"
th:onclick="|location.href='@{/abc/def/hi/patch(memberId=${member.id)}'|"
type="button"></button>
HTML form에서 action에 값이 없으면 현재 URL에 데이터 전송
ex)
등록 폼을 호출하는 URL과 실제 등록을 처리하는 URL을 동일하게 하고 HTTP 메서드로 두 기능을 구분
th:checked=
"false"HTML에서 checked 속성은 checked 속성의 값과 상관없이 checked라는 속성만 있으면 체크박스에 체크됨
타임리프의 th:checked는 값이 false라면 checked 속성 자체를 제거함
th:checked 값이 true라면 체크, false라면 체크되지 않을 것
🔹 단순한 URL
th:href="@{URL}"
타임리프는 URL 링크를 사용하는 경우 @{URL링크}를 사용함
🔹 쿼리 파라미터, 경로 변수
th:href="@{URL/{_path variable}(path variable=${경로변수 값}[,query='쿼리 파라미터 값'])_}"
쿼리 파라미터
th:href="@{/URL(param1=${param1}, param2=${param2})}
() 안에 부분은 쿼리 파라미터로 처리
경로 변수
th:href="@{/URL/{param1}/{param2}(param1=${param1}, param2=${param2})}
URL 경로상에 변수가 있으면 () 부분은 경로 변수로 처리
ex
th:href="@{/abc/def/hi/{memberId}(memberId=${member.id})}"
-> 생성 링크 : http://localhost:8080/abc/def/hi/member.name값
th:href="@{/ABC/{memberName}(memberName=${member.name},query='hi')}"
-> 생성 링크 : http://localhost:8080/ABC/member.name값?query=hi
리터럴 대체 문법을 활용하여 간단히 사용할 수도 있음
ex)
th:href="@{/abc/def/hi/{memberId}(memberId=${member.id})}"
<=>
th:href="@{|/abc/def/hi/${member.name}|}"
Literlas : 소스 코드상에 고정된 값을 말하는 용어
|문자 표현식 문자 ...|
타임리프에서 문자와 표현식은 분리되어 있으나
'+'를 통해 더해서 사용하는 것이 아닌 리터럴 대체 문법을 사용할 수 있음
ex)
th:onclik="location.href=' + '\' + @{/abc/def/hi} + '\'"
<=>
th:onclick="|location.href='@{/abc/def/hi}'|"
th:each="변수 : ${컬렉션 데이터}"
오른쪽 컬렉션(${컬렉션 데이터})의 값을 하나씩 '변수'에 담아 태그를 반복
List, 배열, java.util.Iterable, java.util.Enumeration을 구현한 모든 객체를 반복에 사용 가능
Map도 사용 가능, 이 때 변수에 담기는 값은 Map.Entry
모델에 포함된 컬렉션 데이터가 변수에 하나씩 포함되고, 반복문 안에서 변수를 사용할 수 있음
ex)
<tr th:each="member : ${members}">
<td></td>
<!-- -->
</tr>
컬렉션의 수 만큼 <tr>..</tr>이 하위 태그를 포함해서 생성됨
🔹 반복 상태 유지 기능
두 번째 파라미터를 설정해서 반복의 상태를 확인할 수 있음
ex)
<tr th:each="member, memberStat : ${members}">
<td>
<span th:text="${memberStat.count}"></span>
</td>
<!-- -->
</tr>
두 번째 파라미터인 memberStat은 생략 가능
두 번째 파라미터를 생략하면 지정 변수명(member) + Stat으로 설정됨
SpringEL
변수 표현식에서는 스프링이 제공하는 표현식인 스프링 EL을 사용함
Object
member.name ➡️ user.getMembername()
member['membername'] ➡️ user.getMembername() //동적으로 꺼내기 쉬움
member.getMembername ➡️ member의 getMembername() 직접 호출
List
member[0].membername ➡️ List에서 첫 번째 회원을 찾아 프로퍼티 접근
member[0]['membername'] ➡️ list.get(0).getMembername()
member[0].getMembername() ➡️ List에서 첫 번째 회원을 찾고 메서드 직접 호출
Map
memberMap['A'].membername : ➡️ Map에서 A를 찾아 프로퍼티 접근
memberMap['A']['membername'] ➡️ map.get("A").getMembername()
memberMap['A'].getMembername() ➡️ Map에서 A를 찾고 메서드 직접 호출
지역 변수
th:with를 사용하여 지역 변수 선언해서 사용 가능
ex)
<div th:with="name=${member.name}">
<li th:text="${name.membername}"></li>
</div>
모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있음
ex)
<td th:text="${member.name}">이름</td>
#request는 HttpServletRequest 객체가 그대로 제공됨
-> 데이터를 조회하려면 request.getParameter("data")와 같이 접근하기 불편함
- 필요할때 찾아서 사용
타임리프 유틸리티 객체
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
내용의 값을 th:text
의 값으로 변경
ex)
<td th:text="${member.name}">내용</td>
'내용'을 ${member.name}의 값으로 변경
위와 달리 직접 출력하는 방법도 존재
[[${member.name]]
<li>멤버 이름은 = [[${member.name]]</li>
멤버 이름은 = member.name값 으로 출력됨
th:text, [[...]]는 escape 제공
(escape : '<' 같은 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것)
< ➡️ & l t ;
> ➡️ & g t ;escape 기능 사용하지 않으려면
th:text ➡️ th:utext
[[...]] ➡️ [(...)]
변경하여 사용단 unescape는 꼭 필요한 상황에만 사용
escape 처리가 되어있지 않으면 HTML이 정상 렌더링 되지 않는 문제가 발생할 수 있음
의도적으로 따로 지원하는 경우에만 사용할 것
<td th:switch=" ">
<span th:case=" "> </span>
<!-- -->
<span th:case="*"> </span> // 만족하는 조건이 없을 때 * 사용
th:if(unless)=" " 조건이 true여야 렌더링, false면 해당 태그 소멸
표준 HTML 주석
자바스크립트 표준 HTML 주석은 타임리프가 렌더링하지 않음
<!-- -->
타임리프 파서 주석
타임리프의 진짜 주석, 렌더링에서 주석 부분을 제거
<!--/* */-->
타임리프 프로토타입 주석
HTML 파일을 웹 브라우저에서 열어보면 HTML 주석이기 때문에 웹브라우저가 렌더링하지 않음
타임리프 렌더링 과정을 거치면 해당 주석 부분이 정상 렌더링됨
➡️ 타임리프를 렌더링 한 경우에만 보이는 부분
<!--/*/ /*/-->
<th:block>
타임리프는 태그가 아닌 속성으로 동작하지만 해결하기 어려운 부분을 해결하기 위한
타임리프의 유일한 자체 태그
ex) <div> 태그를 2개씩 짝지어 반복으로 사용하는 경우 -> each만으로 해결 힘듦
<th:block th:each="member : ${members}">
<div>
...
</div>
<div>
...
</div>
</th:block>
자바스크립트에서 타임리프를 편리하게 사용할 수 있도록 제공하는 기능
<script th:inline="javascript">
🔷 자바스크립트 인라인을 사용하지 않는 경우
문자 -> 개발자가 직접 " "를 달아줘야 함
ex)var membername = "[[${member.membername]]";
natural templates를 사용하기 어려움
ex) var first_name = /[[$member.first_name]]/ "test info";
원하는 결과 : first_name = member.first_name값;
동작 결과 : first_name = "test info";
주석이 무시됨
객체 -> 제대로 동작 안함
ex) var member = [[${member}]];
원하는 결과 : var member = {"membername":"이름", "age":나이};
동작 결과 : var member = 클래스명.Member(membername=이름, age=나이);
member객체.toString()이 호출된 결과를 가져옴
🔷 자바스크립트 인라인을 사용하면
문자 타입인 경우 ""를 포함해줌
자바스크립트에서 문제가 될 수 있는 문자가 포함되어 있다면 escape 처리
natural tempates 기능 제공
객체를 JSON으로 자동 변환
each 지원함
ex)
[# th:each="member : ${members}"]
var member[[${member.index}]] = [[${member}]];
[/]
웹 페이지를 개발할 때 여러 페이지에서 함께 사용하는 공통 영역들이 존재할 것
이러한 부분들을 복사하여 각각의 페이지에서 사용한다면 유지보수에 매우 비효율적임
➡️ 타임리프는 이러한 문제를 해결하기 위해 템플릿 조각과 레이아웃 기능을 지원함
th:fragment="이름"
"~{파일 경로 :: 이름
}"
해당 경로의 템플릿에 있는 th:fragment="이름" 이라는 부분을 템플릿 조각으로 가져와서 사용한다는 의미
ex)
파일1 A/B/footer.html
<footer th:fragment="fr"> 푸터 </footer>
파일2 A/B/main.html
<div th:repalce="~{A/B/footer :: fr}"></div>
템플릿 조각을 사용하는 코드가 단순하면 ~{ } 생략 가능
-><div th:replace="A/B/footer :: fr"></div>
ex)
파일1 A/B/footer.html
<footer th:fragment="fr (param1, param2)">
<p th:text="${param1}"></p>
<p th:text="${param2}"></p>
</footer>
파일2 A/B/main.html
<div th:insert="~{A/B/footer :: fr ('data1', 'data2')}"></div>
일부 코드 조각을 가져와서 사용하는 것이 아닌 코드 조각을 레이아웃에 넘겨서 사용하는 방법
공통 정보들을 한 곳에 모아두고, 각 페이지마다 공통 정보에 필요한 정보들을 추가해서 사용 가능
th:fragment="이름(태그1, 태그2)"
"경로 :: 이름(~{::태그1}, ~{::태그2})..."
해당 경로의 페이지의 '태그1' 태그들과 '태그2' 태그들을 전달
파일1 A/B/main.html
<head th:fragment="common(title,links)">
<!-- 바뀌는 부분 -->
<title th:replace="${title}">Title</title>
<!-- 바뀌지 않는 부분 -->
<link rel="stylesheet" th:href="@{/URL0}">
<!-- 바뀌는 부분 -->
<th:block th:replace="${links}"/>
</head>
파일2 A/B/layout.html
<head th:replace="A/B/layout :: common(~{::title},~{::link})">
<title>타 이 틀</title>
<link rel="stylesheet" th:href="@{/URL1}">
<link rel="stylesheet" th:href="@{/URL2}">
</head>
파일1은 다음과 같이 렌더링 됨
<head>
<title>타 이 틀</title>
<link rel="stylesheet" href="/URL0">
<link rel="stylesheet" href="/URL1">
<link rel="stylesheet" href="/URL2">
</head>
html태그에 th:fragment 속성 정의해서 사용 가능
파일1 A/B/main.html
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<!-- 바뀌는 부분 -->
<title th:replace="${title}">Title</title>
</head>
<body>
<div th:replace="${content}">
<p>Content</p>
</div>
<p> Hello </p>
</body>
</html>
파일2 A/B/layout.html
<html th:replace="~{A/B/main :: layout(~{::title}, ~{::section})}" xmlns:th="http://www.thymeleaf.org">
<head>
<title>타 이 틀</title>
</head>
<body>
<section>
<p>컨 텐 츠</p>
</section>
</body>
</html>
파일1은 다음과 같이 렌더링됨
<html>
<head>
<title>타 이 틀</title>
</head>
<body>
<section>
<p>콘 텐 츠</p>
</section>
<p> Hello </p>
</body>
</html>
장점 : 레이아웃만 바꾸어서 여러 페이지를 한꺼번에 수정 가능
단점 : 체계적으로 관리해야 함
🔹 redirect/...
스프링은 redirect/... 형식으로 편리하게 리다이렉트를 사용할 수 있도록 지원함
ex)
@PostMapping("/URL")
public String 메서드명(//...) {
//...
return "redirect:/..."
}
인프런 스프링 MVC 1편, 2편 - 백엔드 웹 개발 핵심 기술 (김영한) 참조