[Thymeleaf] 타임리프 사용 방법 완전 정복!

728x90

 

타임리프

특징

- 서버 사이드 HTML 렌더링 (SSR)

  : 백엔드 서버에서 HTML 을 동적으로 렌더링

- 네츄럴 템플릿

  : 순수 HTML 파일을 그대로 유지

  : 서버를 통해 뷰 탬플릿을 거치면 동적으로 HTML 을 변환

- 스프링 통합 지원

 

사용 선언

`<html xmlns:th="http://www.thymeleaf.org">`

 

속성 변경 `attrappend` `attrprepend` `classappend` `checked`

 

- `th:xxx`

  : 타임리프 뷰 템플릿을 거치게 되면 `th:xxx` 값으로 동적으로 변경된다.

  : HTML 대부분의 속성을 `th:xxx` 로 변경할 수 있다.

  : `th:href="@{/css/bootstrap.min.css}"`

- `th:attrappend` `th:attrprepend`

  : 속성 값 뒤/앞 에 값을 추가한다.

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

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

- `th:classappend`

  : class 속성에 값을 추가 (띄어쓰기 고려 안 해도 됨)

  : `<input type="text" class="text" th:classappend="large"/>`

- `th:checked`

  : HTML 에서 checked 속성이 있으면 값에 관계없이 checked 처리가 되는 특성을 방지한다.

  : th:checked=false 인 경우 checked 속성 자체를 제거한다.

  : `<input type="checkbox" name="active" th:checked="false"/>` 

 

URL 링크 표현식 `@{...}`

 

- `@{...}`

  : URL 경로를 나타낸다.

  : 절대 경로, 상대 경로 모두 사용 가능하다.

  : 서블릿 컨텍스트를 자동으로 포함한다.

  : URL 경로 상에 변수가 있으면 () 부분은 경로 변수로 처리, 나머지는 쿼리 파라미터로 처리

  : `th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}"`

    > `/hello/data1?param2=data2`

 

리터럴 대체 `|...|`

`model.addAttribute("data", "Spring!");`

 

- `|...|`

  : 문자와 표현식을 더하기 연산 없이 편리하게 사용할 수 있다.

  : 하나의 토큰을 작은 따옴표로 감싸지 않아도 된다.

  : `<span th:text="|hello ${data}|"></span>`

 

> 참고 <

공백 없이 쭉 이어진다면 하나의 토큰으로 인지하여 작은 따옴표를 생략할 수 있다.

 

반복 출력 `each` `Stat`

`model.addAttribute("users", list);`

 

- `th:each`

  : 오른쪽 컬렉션 값을 하나씩 꺼내어 왼쪽 변수에 담아 태그를 반복 실행한다.

  : 컬렉션의 수만큼 `<tr> .. </tr>` 이 하위 태그를 포함해서 생성된다.

  : `<tr th:each="user : ${users}">`

- `Stat`

  : 두 번째 파라미터를 설정하여 반복의 상태 확인 (생략 시 변수명 + Stat)

  : `index` `count` `size` `even/odd` `first/last` `current`

  : `<tr th:each="user, userStat : ${users}">`

 

SpringEL 표현식 `${...}` `with`

`model.addAttribute("user", userA);`

`model.addAttribute("users", list);`

`model.addAttribute("userMap", map);`

 

- `${...}`

  : SpringEL 을 사용하여 Model 에 포함된 값이나, 타임리프 변수로 선언한 값을 조회

  : `<span th:text="${user.username}"></span>`

    == `"${user['username']}"` `"${user.getUsername()}"`

  : `<span th:text="${users[0].username}"></span>`

    == `"${users[0]['username']}"` `"${users[0].getUsername()}"`

  : `<span th:text="${userMap['userA'].username}"></span>`

    == `"${userMap['userA']['username']}"` `"${userMap['userA'].getUsername()}"`

- `${{...}}`

  : 컨버전 서비스를 적용하여 변환된 결과로 조회

- `th:with`

  : 지역 변수 선언하여 사용, 선언한 태그 안에서만 사용

  : <div th:with="first=${users[0]}">

 

편의 객체

- `param`

  : HTTP 요청 파라미터 접근

  : `${param.paramData}`

- `session`

  : HTTP 세션 접근

  : `${session.sessionData}`

- `@`

  : 스프링 빈 접근

  : `${@helloBean.hello('spring!')}`

 

텍스트 출력 `text` `utext` `[[...]]` `[(...)]` 

`model.addAttribute("data", "Hello <b>spring!</b>");`

 

- `th:text`

  : 이스케이프를 제공

  : == `[[...]]` (컨텐츠 안에서 직접 출력)

  : `<span th:text="${data}"></span>` > `Hello <b>spring!</b>`

- `th:utext`

  : 언이스케이프를 제공

  : == `[(...)]` (컨텐츠 안에서 직접 출력)

  : `<span th:utext="${data}"></span>` > `Hello spring!`

 

> 참고 <

이스케이프는 특수문자를 HTML 엔티티로 변경하여 텍스트로 출력하도록 변환한다.

(`<` 를 `&lt;` 로, `>` 를 `&gt;` 로)

 

조건문 `if` `unless` `?` `switch`

`model.addAttribute("data", "spring");`

`model.addAttribute("user", user);`

 

- `th:if` `th:unless`

  : false 면 해당 태그는 아예 렌더링 되지 않는다.

  : `<span th:if="${user.age >= 18}">성인입니다.</span>`
  : `<span th:unless="${user.age >= 18}">미성년자입니다.</span>`

- `?`

  : `<span th:text="(10 % 2 == 0)? '짝수' : '홀수'"></span>`

  : `<span th:text="${data}?: '데이터가 없습니다.'"></span>`

  : `<span th:text="${data}?: _">데이터가 없습니다.</span>`

- `th:switch`

  : * 은 디폴트이다.

  : `<td th:switch="${user.age}"> <span th:case="10">10살</span> <span th:case="*">기타</span>`

 

> 참고 <

`_` 는 타임리프가 실행되지 않아 HTML 데이터가 그대로 노출된다.

 

스프링 검증 오류 기능

bindingResult 에 담긴 오류 객체를 간편하게 사용

SpringEL 을 사용한다.

 

- `#fields`

  : bindingResult 가 제공하는 검증 오류에 접근할 수 있다.

- `th:errors`

  : 해당 필드에 오류가 있는 경우 해당 필드의 오류 메시지를 출력한다.

- `th:errorclass`

  : th:field 에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.

<input type="text" th:errorclass="field-error" 
       class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:errors="*{itemName}">수량 필드 오류</div>

<div th:if="${#fields.hasGlobalErrors()}">
  <p class="field-error" th:each="err : ${#fields.glabalErrors()}" 
     th:text="${err}">글로벌 오류</p>
</div>

 

object field 기능 `*{...}`

 

- `th:object`

  : 커맨드 객체를 지정

- `th:field` `*{...}`

  : SpringEL 을 사용하여 object 로 지정한 객체의 필드를 자동으로 매핑

  : HTML 태그의 id, name, value 속성을 자동으로 처리

  : 검증 오류 발생 시, bindingResult 의 rejectedValue 를 자동 반영하여 value 속성으로 설정됨

  : value 속성이 컨버전 서비스를 적용하여 변환된 값을 갖는다.

...
<form action="item.html" th:action th:object="${item}" method="post">
 <div>
   <label for="itemName">상품명</label>
   <input type="text" id="itemName" th:field="*{itemName}" class="formcontrol">
 </div>
 <div>
   <label for="price">가격</label>
   <input type="text" id="price" th:field="*{price}" class="form-control">
 </div>
...

 

- 단일 checkbox

  > 기존 HTML 은 checkbox 를 선택하지 않으면 open 이라는 필드가 false 가 아닌 null 로 전송되지 않는다.

  > 따라서 히든 필드(_open)를 추가하여 open=on&_open=on 인 경우 체크, _open=on 인 경우 미체크로 인식

  > `<input type="hidden" name="_open" value="on"/>`

  > 타임리프가 히든 필드 추가를 자동으로 해결해 준다.

<div>판매 여부</div>
<div>
 <div class="form-check">
 <input type="checkbox" id="open" th:field="*{open}" class="form-checkinput">
 <label for="open" class="form-check-label">판매 오픈</label>
 </div>
</div>

 

- 다중 checkbox

  > 여러 체크박스를 만들 때, HTML 태그 속성에서 name 은 같아도 되지만 id 는 모두 달라야 한다. 

  > 타임리프가 반복문 루프 안에서 임의로 1, 2, 3 숫자를 붙여준다.

  > 그리고 `ids.prev(...)`, `ids.next(...)` 를 제공하여 동적으로 생성되는 id 값을 사용할 수 있도록 한다.

  > 또한 `th:field` 값과 `th:value` 값을 비교하여 체크를 자동으로 처리해 준다.

<div>
 <div>등록 지역</div>
 <div th:each="region : ${regions}" class="form-check form-check-inline">
  <input type="checkbox" th:field="*{regions}" th:value="${region.key}" 
         class="form-check-input">
  <label th:for="${#ids.prev('regions')}"
         th:text="${region.value}" class="form-check-label">서울</label>
 </div>
</div>

 

 

- 라디오 버튼

  > 라디오 버튼은 이미 선택되어 있다면 수정 시에도 항상 하나를 선택해야 하므로 히든 필드가 필요 없다.

  > 선택한 버튼에 checked="checked" 를 자동으로 추가해 준다.

<div>
 <div>상품 종류</div>
 <div th:each="type : ${itemTypes}" class="form-check form-check-inline">
 <input type="radio" th:field="${item.itemType}" th:value="${type.name()}" 
        class="form-check-input" disabled>
 <label th:for="${#ids.prev('itemType')}" th:text="${type.description}"
        class="form-check-label">BOOK</label>
 </div>
</div>

 

- 셀렉트 버튼

  > 선택한 버튼에 selected="selected" 를 자동으로 추가해 준다.

<div>
 <div>배송 방식</div>
 <select th:field="${item.deliveryCode}" class="form-select" disabled>
 <option value="">==배송 방식 선택==</option>
 <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}" 
         th:text="${deliveryCode.displayName}">FAST</option>
 </select>
</div>

 

> 참고 <

- field 를 지정하면 id 속성도 자동으로 처리해 주지만 IDE 에서 표시가 나타나기 때문에 적어준다.

(없어도 정상 동작함)

- enum 인 경우, `.name()` 을 하면 이름을 사용할 수 있다.

 

주석

 

- `<!-- ... -->`

  : 표준 HTML 주석

  : 타임리프 렌더링을 거쳐도 삭제되지 않고 유지된다.

- `<!--/* ... */-->`

  : 타임리프 파서 주석

  : 타임리프 파서가 주석을 제거하여 최종 코드에서는 완전히 삭제된다.

- `<!--/*/ ... /*/-->`

  : 타임리프 프로토타입 주석

  : HTML 파일에서는 주석으로 보이지만, 타임리프가 렌더링 하면 주석이 해제되어 활성화된다.

  : 흔히 샘플 HTML 을 만들 때 사용한다.

 

블록 `block`

`model.addAttribute("user", user);`

 

- `th:block`

  : HTML 태그가 아닌 타임리프 유일한 자체 태그

  : 2 항목을 반복하고 싶을 때 (each 사용하지 못할 때 사용)

  : `<th:block>` 은 렌더링 시 제거

  : `<th:block th:each="user : ${users}"> <div> ... </div> <div> ... </div> </th:block>`

 

자바스크립트 인라인 `inline`

`model.addAttribute("user", user);`

 

`th:inline`

  : 텍스트 렌더링

    > 문자열 처리, 이스케이프 처리 등 편리하게 사용할 수 있게 해 준다.

    > `var username = [[${user.username}]];` > `var username = "userA";`

  : 내추럴 템플릿

    > 렌더링 하면 주석 부분 값을 동적으로 적용할 수 있다.

    > `var username2 = /*[[${user.username}]]*/ "test username";` > `var username2 = "userA";`

  : 객체

    > 객체를 JSON 으로 자동 변환해 준다.

    > `var user = [[${user}]];` > `var user = {"username":"userA", "age":10};`

  : `<script th:inline="javascript"> ... </script>`

 

템플릿 조각 `fragment`

 

- `th:fragment` 

  : 해당 경로에 있는 해당 fragment 부분을 템플릿 조각으로 가져와서 사용

  : `insert` 현재 태그 내부에 추가

  : `replace` 현재 태그를 대체

...
<h2>부분 포함 replace</h2>
<div th:replace="~{template/fragment/footer :: copy}"></div>

<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
...

// /resources/templates/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>
...

 

템플릿 레이아웃

 

- `th:fragment`

  : 해당 경로에 있는 해당 frament 레이아웃의 파라미터 태그를 전부 넘겨서 사용

  : `insert` 현재 태그 내부에 추가

  : `replace` 현재 태그를 대체

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

// /resources/templates/template/layoutExtend/layoutFile.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>

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

출처 | 스프링 MVC 1(김영한) - 인프런

출처 | 스프링 MVC 2(김영한) - 인프런

728x90