타임리프
특징
- 서버 사이드 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 엔티티로 변경하여 텍스트로 출력하도록 변환한다.
(`<` 를 `<` 로, `>` 를 `>` 로)
조건문 `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(김영한) - 인프런
'💠프로그래밍 언어 > Java' 카테고리의 다른 글
[Spring] 필드/글로벌 검증하기 (BindingResult, MessageResolver, @Validated) (1) | 2025.04.02 |
---|---|
[Spring] 메시지 / 국제화 하기 (0) | 2025.03.31 |
[Spring] SpringMVC 의 요청 매핑과 요청/응답 메시지 기능 ! (0) | 2025.03.20 |
[심화] 로깅은 무엇일까 ?? (0) | 2025.03.19 |
[Spring] SpringMVC 의 세부적인 구조 (핸들러, 핸들러 어댑터, 메시지 컨버터) (1) | 2025.03.19 |