[Spring Boot] 8. Thymeleaf
Thymeleaf는 SpringBoot에서 공식적으로 지원하는 View 템플릿이다.
Thymeleaf의 특징
- JSP와 달리 Thymeleaf 문서는 html 확장자를 가지고 있어 JSP처럼 Servlet이 문서를 표현하는 방식이 아니기 때문에
서버 없이도 동작이 가능하다.
- SSR 템플릿으로 백엔드에서 HTML을 동적으로 생성한다.
- SpringBoot에서 JSP는 별도의 설정이 필요하지만 Thymeleaf는 바로 적용이 가능하다.
Thymeleaf의 장단점
1. 장점
- Natural Templates를 제공한다. (HTML의 기본 구조를 그대로 사용할 수 있으며 HTML 파일을 직접 열어도 동작한다.)
* Natural Templates: 기존 HTML의 마크업 언어를 사용한 디자인 구조로 되어 있는 템플릿으로
서버를 구동하지 않으면 순수 HTML을, 서버를 구동하면 동적으로 HTML이 생성된다.
즉, 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있다.
- 개발 시 디자인과 개발이 분리되어 작업 효율이 좋다.
- WAS을 통하지 않고도 파일을 웹 브라우저를 통해 열 수 있어 퍼블리셔와 협업이 용이하다.
2. 단점
- JSP 태그 라이브러리와 custom 태그들을 사용할 수 없기에 기존 JSP 코드를 재사용할 수 없다.
- 기존 태그를 유지하고 속성으로 템플릿 구문을 넣는데 있어 어느정도 한계가 있고 JS나 jQuery의 도움이 필요할 수도 있다.
Thymeleaf 문법
1. 주석
종류 | 문법 | 설명 |
parser-level 주석 | <!--/* 주석내용 */--> | parser-level 주석은 정적인 페이지에서는 주석으로 있다가 thymeleaf가 처리될 때 제거되어 클라이언트에게 노출되지 않는다는 장점이 있다. |
prototype-only 주석 | <!--// 주석내용 //--> | prototype-only 주석은 정적 페이지에서 주석으로 있다가 thymeleaf 처리 후에는 화면에 보여지게 된다. |
<ul>
<li>parser-level 주석</li>
<!--/* 주석내용 */-->
<li>prototype-only 주석</li>
<!--/*/ 주석내용 /*/-->
</ul>
prototype-only 주석은 화면에 표시되는 것을 확인할 수 있다.
2. 표현식
종류 | 문법 | 설명 |
변수 표현식 | ${...} | paramter, session, model 등에 저장되어 있는 변수의 값들을 문자열로 변환하여 불러온다. |
메시지 표현식 | #{...} | message source로부터 키에 해당하는 메시지를 가져온다. |
링크 표현식 | @{...} | th:href, th:src, th:action 등과 같이 URL이 지정되는 속성에 사용한다. |
선택 변수 표현식 | *{...} | 부모 태그의 th:object에 지정된 객체를 기준으로 해당 객체의 속성에 접근한다. |
인라인 표현식 | [[...]], [(...)] | 텍스트 모드나 자바스크립트 모드로 내부에 선언한 변수 표현식의 값을 가져와서 html에 직접 표시한다. |
(1) 변수 표현식
parameter -> param
session attribute -> session
request attribute -> 별도로 작성하지 않는다.
<button onclick="location.href='/lecture/expression?title=표현식&no=5&no=6'">
표현식
</button>
<p th:text="${ param.title }"></p>
<p th:text="${ param.no[0] }"></p>
<p th:text="${ param.no[1] }"></p>
<!-- 파라미터가 존재하지 않으면 무시하지 않고 에러 발생함 -->
<!--<p th:text="${ param.no[2] }"></p>-->
(2) 메세지 표현식
# resources/messages.properties
message.value=hello world
<p th:text="#{ message.value }"></p>
(3) 링크 표현식
<a th:href="@{/}">메인으로</a>
(4) 선택 변수 표현식
// ModelAndView의 Model에 추가 (name, age, gender, address)
mv.addObject("member", new MemberDTO("홍길동", 20, '남', "서울시 서초구"));
<p th:text="${ member.name }"></p>
<p th:object="${ member }" th:text="*{ age }"></p>
<p th:object="${ member }" th:text="*{ gender }"></p>
<div th:object="${ member }" >
<p th:text="*{ address }"></p>
</div>
(5) 인라인 표현식
// ModelAndView의 Model에 추가
mv.addObject("hello", "hello!<h3>Thymeleaf</h3>");
<p th:inline="none">
변수 표현식의 값을 html에 직접 표시하기 위해서 th:text와 같은 [[...]]를 사용하고
th:utext와 같은 [(...)]를 사용할 수 있다.
대괄호로 묶어 이와 같이 변수 표현식의 값을 가져오는 것을 인라인 모드라고 하며
인라인 모드는 text모드와 자바스크립트 모드가 있다.
우리는 변수 표현식의 값을 자바스크립트에서 쓰는 것이 아닌 html에서 사용하려는 것이므로
th:inline="text"를 태그에 속성값으로 주고 써야 하지만
th:inline="text"를 쓰지 않아도 적용된다.
반면 인라인 모드를 적용하지 않으려면 th:inline="none"을 속성값으로 주면 변수 표현식의 값이
인라인모드로 사용하지 않아([[]] 또는 [()]를 인식하지 않음) 단순 문자열로 처리할 수 있다.
자바스크립트에서 사용하려면 th:inline="javascript"를 태그에 속성값으로 주고 써야 하지만
역시나 th:inline="javascript"를 쓰지 않아도 적용된다.
<p>
<ul>
<li th:inline="text">[[${ hello }]]</li>
<li>[(${ hello })]</li>
<li th:inline="none">[[${ hello }]]</li>
<li th:inline="none">[(${ hello })]</li>
</ul>
<script th:inline="javascript">
window.onload = function(){
/* 정적 페이지에서는 자바스크립트 문법 오류가 난다. (리터럴 형태가 아니기 때문)
* 하지만 동적 페이지에서는 정상 동작한다. ""로 감싸서 던진다.
* */
// let hello = [[${hello}]];
/* 정적 페이지에서는 자바스크립트 문법 오류가 발생하지 않는다. (문자열 리터럴 형태이기 때문)
* 하지만 동적 페이지에서는 에러 발생한다. ""로 감싸기 때문에 ""[[${ hello }]]""가 된다.
* */
// let hello = "[[${ hello }]]";
/* 정적 페이지와 동적 페이지 모두 문제가 생기지는 않는다. */
let hello = '[[${ hello }]]';
alert(hello);
}
</script>
3. 제어문
종류 문법 설명
종류 | 문법 | 설명 |
if문 | th:if="${ONGL을 통한 조건식}" | 변수 표현식의 OGNL을 활용한 조건식으로 조건문을 작성하면 결과가 true일 때 해당 태그 범위가 처리된다. |
else문 | th:unless="${ONGL을 통한 조건식}" | 변수 표현식의 OGNL을 활용한 결과가 false일 때 해당 태그 범위가 처리된다. |
다중조건처리문 | th:if="${ONGL을 통한 조건식 and/or ONGL을 통한 조건식...}" |
변수 표현식의 OGNL을 활용한 조건식들과 and 또는 or를 통해 다중 조건문을 작성하고 결과가 true일 때 해당 태그 범위가 처리된다. |
switch문 | th:swith="${...}과 th:case="리터럴" | th:switch와 th:case를 통해 해당 조건의 값이 어떤 case에 해당되는지에 따라 태그를 선택할 수 있다. |
each문 | th:each="변수 : ${collection값}" | 컬렉션에 해당하는 값들을 하나씩 변수에 담아 collection의 크기만큼 반복하여 태그를 처리한다. |
(1) if문 / else문 / 다중조건처리문
// ModelAndView의 Model에 추가
mv.addObject("num", 1);
mv.addObject("str", "바나나");
<p th:if="${ num > 0 }">넘어온 값은 0보다 크다.</p> <!-- 조건에 해당되면 -->
<p th:if="${ num < 0 }">넘어온 값은 0보다 작다.</p>
<p th:unless="${ num < 0 }">넘어온 값은 0보다 크다.</p> <!-- 조건에 해당하지 않으면 -->
<th:block th:if="${ str == '사과' }"> <!-- th:block을 사용할 수도 있다. -->
<p>사과 좋아요!</p>
</th:block>
<th:block th:if="${ str == '바나나' }">
<p>바나나 좋아요!</p>
</th:block>
<!-- and나 or를 사용해서 다중 조건 처리도 가능하다. -->
<p th:if="${ num > 0 or num <= 10 }">1부터 10까지의 양수</p>
<p th:if="${ str != null and str == '바나나' }">바나나 좋아요!</p>
<!-- #strings라는 타임리프에서 제공하는 Utility Objects에서 제공하는 메소드를 통해서도 null에 대한 처리를 할 수 있다. -->
<p th:if="${ !#strings.isEmpty(str) and str == '바나나' }">바나나 좋아요!</p>
(2) switch문
// ModelAndView의 Model에 추가
mv.addObject("str", "바나나");
<th:block th:switch="${ str }">
<span th:case="사과">사과가 선택되었습니다</span>
<span th:case="바나나">바나나가 선택되었습니다</span>
</th:block>
(3) each문
// ModelAndView의 Model에 추가 (name, age, gender, address)
List<MemberDTO> memberList = new ArrayList<>();
memberList.add(new MemberDTO("홍길동", 20, '남', "서울시 서초구"));
memberList.add(new MemberDTO("유관순", 22, '여', "서울시 노원구"));
memberList.add(new MemberDTO("장보고", 40, '남', "서울시 종로구"));
memberList.add(new MemberDTO("신사임당", 30, '여', "서울시 성북구"));
mv.addObject("memberList", memberList);
<table>
<tr>
<th>이름</th>
<th>나이</th>
<th>성별</th>
<th>주소</th>
</tr>
<tr th:each="member : ${ memberList }">
<td th:text="${ member.name }"></td>
<td th:text="${ member.age }"></td>
<td th:text="${ member.gender }"></td>
<td th:text="${ member.address }"></td>
</tr>
</table>
<!-- th:each에 stat을 추가해서 반복상태를 확인할 수 있다. -->
<table>
<tr>
<th>이름</th>
<th>나이</th>
<th>성별</th>
<th>주소</th>
<th>INDEX</th>
<th>COUNT</th>
<th>SIZE</th>
<th>CURRENT</th>
<th>EVEN</th>
<th>ODD</th>
<th>FIRST</th>
<th>LAST</th>
</tr>
<tr th:each="member, stat : ${ memberList }">
<td th:text="${ member.name }"></td>
<td th:text="${ member.age }"></td>
<td th:text="${ member.gender }"></td>
<td th:text="${ member.address }"></td>
<td th:text="${ stat.index }"></td>
<td th:text="${ stat.count }"></td>
<td th:text="${ stat.size }"></td>
<td th:text="${ stat.current }"></td>
<td th:text="${ stat.even }"></td>
<td th:text="${ stat.odd }"></td>
<td th:text="${ stat.first }"></td>
<td th:text="${ stat.last }"></td>
</tr>
</table>
<!-- th:each에 stat을 추가하지 않으면 '변수명+Stat'으로 반복상태를 확인할 수 있다. -->
<table>
<tr>
<th>이름</th>
<th>나이</th>
<th>성별</th>
<th>주소</th>
<th>INDEX</th>
<th>COUNT</th>
<th>SIZE</th>
<th>CURRENT</th>
<th>EVEN</th>
<th>ODD</th>
<th>FIRST</th>
<th>LAST</th>
</tr>
<tr th:each="member: ${ memberList }">
<td th:text="${ member.name }"></td>
<td th:text="${ member.age }"></td>
<td th:text="${ member.gender }"></td>
<td th:text="${ member.address }"></td>
<td th:text="${ memberStat.index }"></td>
<td th:text="${ memberStat.count }"></td>
<td th:text="${ memberStat.size }"></td>
<td th:text="${ memberStat.current }"></td>
<td th:text="${ memberStat.even }"></td>
<td th:text="${ memberStat.odd }"></td>
<td th:text="${ memberStat.first }"></td>
<td th:text="${ memberStat.last }"></td>
</tr>
</table>
4. SpringEL
변수 표현식(${...})에서 SpringEL을 사용하여 단순한 변수가 아닌 Object, List, Map 같은 객체의 값들을 불러올 수 있다.
종류 | 문법 | 설명 |
Object | ${객체명.속성명} | 해당 객체의 속성값을 불러온다. |
${객체명['속성명']} | ||
${객체명.속성의 getter()} | ||
List | ${List객체명[index번째 객체].속성명} | List에서 index번째 객체의 속성을 불러온다. |
${List객체명[index번째 객체]['속성명']} | ||
${List객체명[index번째 객체].속성의 getter()} | ||
${List객체명.get(index번째 객체).속성의 getter()} | ||
${List객체명.get(index번째 객체).속성명} | ||
Map | ${Map객체명['객체의 키값'].속성명} | Map에서 키값에 해당하는 객체의 속성을 불러온다. |
${Map객체명['객체의 키값']['속성명']} | ||
${Map객체명['객체의 키값'].속성의 getter()} |
// ModelAndView의 Model에 추가 (name, age, gender, address)
MemberDTO member = new MemberDTO("홍길동", 20, '남', "서울시 서초구");
mv.addObject("member", member);
List<MemberDTO> memberList = new ArrayList<>();
memberList.add(new MemberDTO("홍길동", 20, '남', "서울시 서초구"));
memberList.add(new MemberDTO("유관순", 22, '여', "서울시 노원구"));
memberList.add(new MemberDTO("장보고", 40, '남', "서울시 종로구"));
memberList.add(new MemberDTO("신사임당", 30, '여', "서울시 성북구"));
mv.addObject("memberList", memberList);
Map<String, MemberDTO> memberMap = new HashMap<>();
memberMap.put("m01", new MemberDTO("홍길동", 20, '남', "서울시 서초구"));
memberMap.put("m02", new MemberDTO("유관순", 22, '여', "서울시 노원구"));
memberMap.put("m03", new MemberDTO("장보고", 40, '남', "서울시 종로구"));
memberMap.put("m04", new MemberDTO("신사임당", 30, '여', "서울시 성북구"));
mv.addObject("memberMap", memberMap);
<p>Object</p>
<ul>
<li th:text="${ member.name }"></li>
<li th:text="${ member['age'] }"></li>
<!-- 위 두가지 방식은 getter가 필요 없지만 getGender()는 반드시 해당 클래스에 getter가 있어야 한다. -->
<li th:text="${ member.getGender() }"></li>
</ul>
<p>List</p>
<ul>
<li th:text="${ memberList[1].name }"></li>
<li th:text="${ memberList[1]['age'] }"></li>
<!-- 위 두가지 방식은 getter가 필요 없지만 getGender()는 반드시 해당 클래스에 getter가 있어야 한다. -->
<li th:text="${ memberList[1].getGender() }"></li>
<li th:text="${ memberList.get(1).getGender() }"></li>
<li th:text="${ memberList.get(1).address }"></li>
</ul>
<p>Map</p>
<ul>
<li th:text="${ memberMap['m03'].name }"></li>
<li th:text="${ memberMap['m03']['age'] }"></li>
<!-- 위 두가지 방식은 getter가 필요 없지만 getGender()는 반드시 해당 클래스에 getter가 있어야 한다. -->
<li th:text="${ memberMap['m03'].getGender() }"></li>
</ul>
5. 기타
종류 | 문법 | 설명 |
타임리프 네임스키마 |
xmlns:th="http://www.thymeleaf.org" | 타임리프를 활용하기 위해서는 html의 html태그에 네임스키마로 선언해 주어야 한다. |
escape 적용 | th:text="${...}" | 변수 표현식의 값을 불러오지만 escape가 적용되어 태그를 단순 문자열로 처리하고 html에 표현한다. |
escape 미적용 | th:utext="${...}" | 변수 표현식의 값을 불러오지만 escape가 적용되지 않아 태그를 태그로써 인식하게 처리하고 html에 표현한다. |
value 속성 적용 | th:value="${...}" | 변수 표현식의 값을 불러와 태그의 value값을 지정한다. |
리터럴 치환 | th:text=” | 리터럴${…}리터럴 |
블럭태그 | th:block | 범위를 지정하고 싶을 때 사용한다. th:block을 통해 해당 범위에 변수나 객체를 적용하거나 조건에 해당되는지에 따라 해당 범위를 보여주거나 보여주지 않을 때 사용할 수 있다. |
지역변수 | th:with="변수명1 = ${...}, 변수명2 =${...}, ..." | 변수 표현식(${...})을 통해 불러온 값을 해당하는 변수명으로 해당 태그 범위의 지역변수가 되게 한다. |
security 인증 정보 여부 |
sec:authorize="isAuthenticated()” | 타임리프에서 시큐리티 적용 시 로그인, 로그아웃에 대한 이벤트를 줄 수 있다. |
(1) 타임리프 네임스키마
<html xmlns:th="http://www.thymeleaf.org">
(2) escape 적용/미적용, value 속성 적용
태그의 값을 태그 내부의 값으로 작성하기 위해서는 th:text 또는 th:utext를 사용할 수 있다.
th:text는 escape가 적용되어 단순 문자열로 처리하지만 th:utext는 escape가 적용되지 않아 태그를 태그로써 인식할 수 있다.
(DB에 태그가 포함된 문자열을 저장했을 시 유용하다.)
여기서 escape는 HTML 태그나 특수 문자를 문자 그대로 표시하기 위해 변환하는 과정이다.
// ModelAndView의 Model에 추가
mv.addObject("hello", "hello!<h3>Thymeleaf</h3>");
<ul>
<li th:text="${ hello }"></li>
<li th:utext="${ hello }"></li>
<li><input type="text" th:value="타임리프"></li>
</ul>
(3) 리터럴 치환
// ModelAndView의 Model에 추가 (name, age, gender, address)
mv.addObject("member", new MemberDTO("홍길동", 20, '남', "서울시 서초구"));
<p th:object="${ member }" th:text="|name = '*{ name }'|"></p>
<p th:object="${ member }" th:text="|age = '*{ age }'|"></p>
<p th:object="${ member }" th:text="|gender = '*{ gender }'|"></p>
<p th:object="${ member }" th:text="|address = '*{ address }'|"></p>
(4) 블럭 태그
// ModelAndView의 Model에 추가 (name, age, gender, address)
mv.addObject("member", new MemberDTO("홍길동", 20, '남', "서울시 서초구"));
<th:block th:object="${ member }" >
<p th:text="*{ age }"></p>
</th:block>
(5) 지역 변수
// ModelAndView의 Model에 추가 (startPage, endPage, pageNo)
SelectCriteria selectCriteria = new SelectCriteria(1, 10, 3);
mv.addObject(selectCriteria);
<th:block th:with="start = ${ selectCriteria.startPage }, last = ${ selectCriteria.endPage }">
<th:block th:each="p : ${ #numbers.sequence(start, last) }">
<th:block th:if="${ selectCriteria.pageNo eq p }">
<button th:text="${ p }" disabled></button>
</th:block>
<th:block th:if="${ selectCriteria.pageNo ne p }">
<button th:text="${ p }"></button>
</th:block>
</th:block>
</th:block>