Spring Framework/Spring Boot

[Spring Boot] 8. Thymeleaf

hyomee2 2024. 9. 11. 12:48

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>