[spring] thymeleaf 와 문법

공수정·2022년 6월 1일
0

thymeleaf

목록 보기
2/2
post-custom-banner

Thymeleaf 란?

  • 서버 사이드 HTML 렌더링 (SSR)
    : 타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용
  • 네츄럴 템플릿
    : jsp등과 다르게 타임리프는 html형식을 최대한 유지하기 때문에 html파일을 직접 열어도 내용을 확인할 수 있고, 뷰 템플릿을 거치면 동적인 화면을 확인할 수 있다. 이렇게 순수 HTML을 유지하면서 뷰 템플릿을 유지하면서 뷰 템플릿을 사용할 수 있는 특징을 네츄럴 템플릿 이라고 함
  • 스프링 통합 지원
    : 타임리프는 스프링과 자연스럽게 통합되고, 스프링의 다양한 기능을 편리하게 사용 할 수 있게 지원해준다.

공식 사이트 : https://www.thymeleaf.org/
공식 메뉴얼 - 기본 기능: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
공식 메뉴얼 - 스프링 통합 : https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

문법

사용 선언

<html xmlns:th="http://www.thymeleaf.org">을 html파일 상단에 선언하면 사용할 수 있다.

표현식

  • 간단한 표현식
    • 변수 : ${,,,} 변수를 사용할 때 사용
    • 선택 변수 : *{,,,} th:Object와 함께 사용
    • 메시지 표현식 : #{,,,}
    • 링크 URL : @{,,,} URL 링크를 표현 할 때
    • 조각 표현식 : ~{,,,} replace, insert 등을 사용 할 때
  • 리터럴
    • 텍스트 : 'one text', 'Another one!' ,,,,
    • 숫자 : 0, 34, 12.3, 14.0 ,,,,
    • 불린 : true, false ,,,
    • 널 : null
    • 리터럴 토큰 : one, sometext, main
  • 문자 연산
    • 문자 합치기 : +
    • 리터럴 대체 : |My name is ${name}!|

      타임리프에서 문자 리터럴에서 주의해야할 점이 있는데, 바로 모든 문자열은 '(싱글) 으로 감싸져야한다는 것이다.
      하지만 예외적으로 a-z, A-Z,0-9 으로만 이루어진 문자열은 감싸지 않아도 되지만, 띄어쓰기가 들어가거나 특수 문자 등이 들어가거나 변수와 문자열 합치기를 해야하는 경우에는 반드시 '(싱글)으로 감싸져 있어야한다.
      혹은 위에서 말한 |으로 감싸져도 된다.

  • 산술 연산
    • Binary operators : +, -, *, /, %
    • Minus sign (unary operator) : -
  • 불린 연산
    • Binary operators : and, or
    • Boolean negation (unary operator) : !, not
  • 비교와 동등
    • 비교 :>,<,>=,<=(gt,lt,ge,le)
    • 동등 연산 : ==, != (eq, ne)

      HTML Entity와 주의를 해야한다.

  • 조건 연산
    • If-then: (if) ? (then)
    • If-then-else: (if) ? (then) : (else)
    • Default: (value) ?: (defaultvalue)
  • 특별한 토큰
    • No-Operation : _

사용 방법

타임리프는 기본적으로 HTML 테그의 속성에 기능을 정의해서 동작한다.

<span th:text="${data}">원래 텍스트</span>

이런식으로 안에 th:를 먼저쓰고 뒤에 textobject 등으로 어떤 값인지 구분을 하고, 그 값에 표현식을 넣는다.

아니면 html 태그 안에 값을 직접 넣고 싶다면,

<span>[[${data}]]</span>

이러한 방식으로 [[]]안에 표현식을 써서 사용할 수도 있다.


텍스트

data가 <b>text</b>라고 상황을 두고 두 개를 비교해보자.

  • text : 이스케이프 기능이 자동으로 적용되는 text

[타임리프]

<span th:text="${data}">원래 텍스트</span>

[결과] -> 소스보기

<span>&lt;b&gt;text&lt;/b&gt;</span>
  • utext : 이스케이프 기능이 적용되지 않는 text

[타임리프]

<span th:utext="${data}">원래 텍스트</span>

[결과] -> 소스보기

<span><b>text</b></span>

Escape
html문서는 <, > 같은 특수 문자를 기반으로 정의 되기 때문에 html 텍스트를 보여줄 때 주의 해야하는데, 이때 <, >와 같이 html 파일에서 사용하는 특수문자를 문자로 표현하는 문자를 html entity라고 한다.
이 html 파일에서 사용하는 특수문자를 html entity로 변경하는 것을 escape라고 한다.
기본적인 예시로는
< -> &lt; , > -> &gt;가 있고 이 외에도 많은 html entity가 있다.

위에서 말한 [[]]안에 표현식을 써서 사용하는 방법은 text와 같고, [()]를 사용하면 utext와 같다.


변수

타임리프에서는 변수를 사용 할 때에는 ${,,,}의 변수 표현식을 사용하는데, 여기 안에는 springEL 이라는 스프링이 제공하는 표현식을 사용할 수 있다.

SpringEL 다양한 표현식 사용
1. Object
user.username : user의 username을 프로퍼티 접근
user['username'] : 위와 같음 user.getUsername()
user.getUsername() : user의 getUsername() 을 직접 호출
2. List
users[0].username : List에서 첫 번째 회원을 찾고 username 프로퍼티 접근 -> list.get(0).getUsername()
users[0]['username'] : 위와 같음
users[0].getUsername() : List에서 첫 번째 회원을 찾고 메서드 직접 호출
3. Map
userMap['userA'].username : Map에서 userA를 찾고, username 프로퍼티 접근 map.get("userA").getUsername()
userMap['userA']['username'] : 위와 같음 userMap['userA'].getUsername() : Map에서 userA를 찾고 메서드 직접 호출

  • 지역 변수 : th:with 를 사용해 지역 변수를 선언해서 사용할 수 있, 해당 지역 변수는 선언한 태그 안에서만 사용할 수 있다.

[타임리프]

<div th:with="userFirst=${users[0]}">
	<p>1번째 유저 이름 : <span th:text="${userFirst.username}"></span></p>
</div>

객체

기본 객체

  • ${#request}
  • ${#response}
  • ${#session}
  • ${#servletContext}
  • ${#locale}

유틸리티 객체

  • #message : 메시지, 국제화 처리
  • #uris : URI 이스케이프 지원
  • #dates : java.util.Date 서식 지원
  • #calendars : java.util.Calendar 서식 지원
  • #temporals : 자바8 날짜 서식 지원
  • #numbers : 숫자 서식 지원
  • #strings : 문자 관련 편의 기능
  • #objects : 객체 관련 기능 제공
  • #bools : boolean 관련 기능 제공
  • #arrays : 배열 관련 기능 제공
  • #lists , #sets , #maps : 컬렉션 관련 기능 제공
  • #ids : 아이디 처리 관련 기능 제공, 뒤에서 설명

URL 링크

타임리프에서 링크를 생성할 때 @{...}를 사용하면 된다.

  • 단순한 URL
    @{/hello} -> /hello
  • 쿼리 파라미터 : () 에 있는 부분은 쿼리 파라미터로 처리
    @{/hello(param1=${param1}, param2=${param2})} -> /hello?param1=data1&param2=data2
  • 경로 변수 : URL 경로상에 변수가 있으면 () 부분은 경로 변수로 처리
    @{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})} -> /hello/data1/data2
  • 경로 변수 + 쿼리 파라미터 : 경로 변수와 쿼리 파라미터를 함께 사용할 수 있음
    @{/hello/{param1}(param1=${param1}, param2=${param2})} -> /hello/data1?param2=data2

속성 값 설정

  • 속성 값 설정 :th:* 속성을 지정하면 기존 속성을 th:* 로 지정한 속성으로 대체한다. (없으면 만듬)

[타임리프]

<input type="text" name="mock" th:name="userA" />

[결과]

<input type="text" name="userA" />
  • 속성 추가 : 원래 있던 속성 값을 대체 하는 것이 아니라 그 값에 추가
    • th:attrappend : 속성 값의 뒤에 값을 추가한다.(띄어쓰기 필요)
    • th:attrprepend : 속성 값의 앞에 값을 추가한다.(띄어쓰기 필요)
    • th:classappend : class 속성에 자연스럽게 추가한다.(띄어쓰기 필요 없음, 자동으로 띄어쓰기까지 해줌)

[타임리프]

<input type="text" class="text" th:attrappend="class=' large'" />

[결과]

<input type="text" class="text large" />

checked
사실 checkedtrue, false 값을 구분하는 것이 아니라 속성 값이 존재하는지 하지 않는지를 구분하기때문에 처리가 까다로운데, 타임리프는 th:checked="false"라고 되어있으면 아예 checked 값을 넣지 않는다.


제어문

반복

  • 반복 1

[타임리프]

<tr th:each="user : ${users}">
  <td th:text="${user.username}">유저이름</td>
</tr>

[결과]

<tr>
  <td>username1</td>
<tr>
</tr>
  <td>username2</td>
</tr>

반복시 오른쪽 컬렉션${users}의 값을 하나씩 꺼내서 왼쪽 변수user에 담아서 태그를 반복
반복 할 수 있는 객체 : Map, List ,배열, java.util.Iterable , java.util.Enumeration을 구현한 모든 객체를 반복에 사용할 수 있음

Map 도 사용할 수 있는데 이 경우 변수에 담기는 값은 Map.Entry 입니다.

  • 반복 2
    반복의 두번째 파라미터를 설정해 반복의 여러 값을 확인 할 수 있음
    이 때 두번째 파라미터를 지정하지 않아도 첫번째 파라미터 이름 + Stat 으로 접근 가능
    반복 상태 유지 기능
    • index : 0부터 시작하는 값
    • count : 1부터 시작하는 값
    • size : 전체 사이즈
    • even , odd : 홀수, 짝수 여부( boolean )
    • first , last :처음, 마지막 여부( boolean )
    • current : 현재 객체

[타임리프]

<tr th:each="user, userStat : ${users}">
  <td th:text="${userStat.index}">인덱스</td>
  <td th:text="${user.username}">유저이름</td>
</tr>

[결과]

<tr>
  <td>0</td>
  <td>username1</td>
</tr>
<tr>
  <td>1</td>
  <td>username1</td>
</tr>

조건

타임리프는 조건식을 비교했는데 맞지 않으면 아예 렌더링을 하지 않고 사라짐

  • if, unless

[타임리프]

<span th:text="'결과1'" th:if="${21 lt 20}"></span>
<span th:text="'결과2'" th:unless="${21 gt 20}"></span>

[결과]

<span>결과2</span>
  • switch
    th:case="*"는 만족하는 값이 없을때 사용하는 디폴트 값

[타임리프]

<td th:switch="'10'">
	<span th:case="10">10살</span>
  	<span th:case="20">20살</span>
  	<span th:case="*">기타</span>
</td>

[결과]

<td>
	<span>10살</span>
</td>

주석

  • 표준 HTML 주석 : 타임리프가 렌더링 X , 주석 부분이 그대로 존재
  • 타임리프 파서 주석 : 렌더링에서 주석 부분을 제거
  • 타임리프 프로토타입 주석 : HTML 파일 : 주석 / 타임리프를 렌더링 한 경우 : 보임

블록

<th:block> 은 HTML 태그가 아닌 타임리프의 유일한 자체 태그
원래 타임리프는 html태그 안에 속성으로 기능을 정의해서 사용하는데, 그렇게 하기 애매한 경우에 사용
이 태그는 렌더링시에는 사라진다.

[타임리프]

<th:block th:each="user : ${users}"> 
	<div>
        <div th:text="${user.index}"></div>
    </div>
	<div>
    	<div th:text="${user.name}"></div>
        <div th:text="${user.age}"></div>
    </div>
</th:block>

[결과]

<div>
	<div>1</div>
</div>
<div>
	<div>유저1</div>
	<div>10</div>
</div>
<div>
	<div>2</div>
</div>
<div>
	<div>유저2</div>
	<div>20</div>
</div>

자바스크립트 인라인

타임리프는 자바스크립트에서 타임리프를 사용할 수 있는 자바스크립트 인라인 기능을 제공한다.
적용하기 위해서는 <script th:inline="javascript">를 적으면 된다.

  • 텍스트 : 이스케이프까지 처리해서 "로 감싸져서 들어감
  • 내추럴 템플릿 : 주석 부분은 렌더링시 사라짐
  • 객체 : 객체를 자동으로 JSON으로 변환
  • each : 반복문 문법이 동작함

[타임리프]

<script th:inline="javascript">
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
  

[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>

[결과]

<script>
var username = "userA";
var age = 10; //자바스크립트 내추럴 템플릿
var username2 = "userA"; //객체
var user = {"username":"userA","age":10};
  
var user1 = {"username":"userA","age":10};
var user2 = {"username":"userB","age":20};
var user3 = {"username":"userC","age":30};
</script>

템플릿 조각

템플릿 조각은 여러 페이지에서 공통적으로 사용하는 영역들을 해결하기 위해서 용한다. 예를 들어 footer와 header 등과 같은 부분은 여러 페이지에서 공통적으로 사용되면서 동시에 모든 페이지에서 똑같아야하는 부분등을 의미한다.

부분 포함

footer를 따로 파일로 분리하고, 각 페이지에서 분리했던 footer를 가져다 사용하는 방식이다.

[타임리프]
/template/fragment/footer.html

<footer th:fragment="copy"> 푸터 자리 입니다.</footer>

<footer th:fragment="copyParam (param1, param2)">
	<p>파라미터 자리 입니다.</p>
	<p th:text="${param1}"></p>
  	<p th:text="${param2}"></p>
</footer>

/template/fragment/main.html

<body>
	<h2>부분 포함 insert</h2>
	<div th:insert="~{template/fragment/footer :: copy}"></div>
	<h2>부분 포함 replace</h2>
	<div th:replace="~{template/fragment/footer :: copy}"></div>
	<h1>파라미터 사용</h1>
	<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터 2')}"></div>
</body>

[결과]

<body>
	<h2>부분 포함 insert</h2>
	<div>
		<footer> 푸터 자리 입니다.</footer>
  	</div>
	<h2>부분 포함 replace</h2>
	<footer> 푸터 자리 입니다.</footer>
	<h1>파라미터 사용</h1>
  	<footer>
		<p>파라미터 자리 입니다.</p>
		<p>데이터1</p>
	  	<p>데이터2</p>
	</footer>
</body>
  • th:insert : 이 속성이 적혀 있던 태그의 자식 태그로 들어옴
  • th:replace : 이 속성이 적혀 있던 태그가 사라지고, 그 위치로 들어옴

또 파라미터를 사용 할 수 있고, 파라미터로는 태그도 들어 갈 수 있다.
이것을 이용해서 다양한 동적인 페이지를 만들 수 있다.

레이아웃

위에서는 공통된 부분을 부분별로 잘라내어 따로 저장하고 메인 페이지에서 이를 가져다 사용하는 방법이였다면, 이 방법은 오히려 반대로 한 html에 레이아웃을 짜놓고, 내용이 변경 되는 부분에만 내용을 넣어서 페이지를 구성하는 방식이다.

레이아웃을 짜놓을 파일에
<html th:fragment="layout(title, content)" xmlns:th="http://www.thymeleaf.org">를 추가해 해당 페이지를 layout이라고 정의하고, titlecontent를 받아서 레이아웃 안에 넣어 페이지를 보여준다.

[타임리프]

layout.html

<!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>

main.html

<!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>

[결과]

<!DOCTYPE html>
<html>
<head>
	<title>메인 페이지 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<section>
	<p>메인 페이지 컨텐츠</p>
	<div>메인 페이지 포함 내용</div>
</section>
<footer> 레이아웃 푸터</footer>
</body>
</html>

출처
1. 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
계속해서 공부하는 개발자입니다 :)
post-custom-banner

0개의 댓글