08. 타임리프 기본기능
정리
타임리프 사용 선언
<html xmlns:th="http://www.thymeleaf.org">
(스프링 부트에 타임리프를 추가했으면 선언하지 안하도 타임리프 문법이 적용되는 것 같긴 합니다)
기본 표현식
• 간단한 표현:
◦ 변수 표현식: ${...}
◦ 선택 변수 표현식: *{...}
◦ 메시지 표현식: #{...}
◦ 링크 URL 표현식: @{...}
◦ 조각 표현식: ~{...}
• 리터럴
◦ 텍스트: 'one text', 'Another one!',…
◦ 숫자: 0, 34, 3.0, 12.3,…
◦ 불린: true, false
◦ 널: null
◦ 리터럴 토큰: one, sometext, main,…
• 문자 연산:
◦ 문자 합치기: +
◦ 리터럴 대체: |The name is ${name}|
• 산술 연산:
◦ Binary operators: +, -, *, /, %
◦ Minus sign (unary operator): -
• 불린 연산:
◦ Binary operators: and, or
◦ Boolean negation (unary operator): !, not
• 비교와 동등:
◦ 비교: >, <, >=, <= (gt, lt, ge, le)
◦ 동등 연산: ==, != (eq, ne)
• 조건 연산:
◦ If-then: (if) ? (then)
◦ If-then-else: (if) ? (then) : (else)
◦ Default: (value) ?: (defaultvalue)
• 특별한 토큰:
◦ No-Operation: _
텍스트 - text
, utext
타임리프는 기본적으로 HTML 태그의 속성에 기능을 정의해서 동작한다. HTML 컨텐츠에 SSR로 데이터를 출력해야 할 때는 th:text
를 사용하면 된다.
<span th:text="${data}"></span>
HTML 태그의 속성이 아니라 HTML 콘텐츠 영역 안에서 직접 데이터를 출력하고 싶으면 다음과 같이 [[...]]
를 사용하면 된다.
컨텐츠 안에서 직접 출력하기 = [[${data}]]
@GetMapping("/text-basic")
public String textBasic(Model model) {
model.addAttribute("data", "Hello Spring!");
return "basic/text-basic";
}
/resources/templates/basic/text-basic.html
<h1>컨텐츠에 데이터 출력하기</h1>
<ul>
<li>th:text 사용 <span th:text="${data}"></span></li>
<li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li>
</ul>
Escape
서버에서 HTML을 동적으로 랜더링할 때, 특정 태그가 적용되도록 하고 싶을 수 있다. (Ex. )
HTML 문서는 <
, >
같은 특수 문자를 기반으로 정의된다. 따라서 태그 적용을 위해 특수 문자가 사용되는지, 아니면 단순히 문자를 출력하기 위한 용도인지를 구분해야 한다. 뷰 템플릿으로 HTML 화면을 생성할 때는 출력하는 데이터에 이러한 특수 문자가 있는 것을 주의해서 사용해야 한다.
ex) 태그
"Hello <b>Spring!</b>"
이런식으로 렌더링하여 “Spring!” 문자열이 진하게 나오도록 해보자.
그런데 실제로 웹 브라우저에서 실행하면
- 웹 브라우저:
Hello <b>Spring!</b>
- 소스보기:
Hello <b>Spring!</b>
이렇게 코드가 렌더링되었다. <
부분이 <
로 변경된 것을 확인할 수 있다.
HTML 엔티티
웹 브라우저는 <
를 HTML 태그의 시작으로 인식한다. 따라서 <
를 태그의 시작이 아니라 문자로 표현할 수 있는 방법이 필요한데(문자로 표현해서 <
로 보이게 해준다), 이것을 HTML 엔티티라 한다. 그리고 이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 이스케이프(escape)라 한다. 그리고 타임리프가 제공하는 th:text
, [[...]]
는 기본적으로 이스케이프(escape)를 제공한다.
<
<
;>
>
;
위 예시에서는 의도하지는 않았지만 디폴트인 escape가 적용된 것이다.
Unescape
우리가 원했던 바다. 태그가 문자열로 보이는게 아니라 실제로 태그가 적용된 것을 원했다.
이스케이프 기능을 사용하지 않도록 하기 위해선 두 가지 방법을 사용할 수 있다.
th:text
th:utext
[[...]]
[(...)]
/resources/templates/basic/text-unescaped.html
<ul>
<li>th:text = <span th:text="${data}"></span></li>
<li>th:utext = <span th:utext="${data}"></span></li>
</ul>
<h1><span th:inline="none">[[...]] vs [(...)]</span></h1>
<ul>
<li><span th:inline="none">[[...]] = </span>[[${data}]]</li>
<li><span th:inline="none">[(...)] = </span>[(${data})]</li>
</ul>
- th:inline="none": 타임리프는
[[...]]
를 해석하기 때문에, 화면에[[...]]
글자를 보여줄 수 없다.
이 태그 안에서는 타임리프가 해석하지 말라는 옵션이다.
주의!
실제 서비스를 개발하다 보면 escape를 사용하지 않아서 HTML이 정상 렌더링 되지 않는 수많은 문제가 발생한다. (사용자가 이런 식으로 막 입력을 하는데, 이스케이프 처리를 하지 않으면 깨져보이는 문제가 발생한다) escape를 기본으로 하고, 꼭 필요한 때만 unescape를 사용하자. unescape를 남발하면 전달하고자 하는 content 내용 자체가 깨져보이는 실수를 범할 수 있다.
변수 - SpringEL
타임리프에서 변수를 사용할 때는 변수 표현식을 사용한다.
- 변수 표현식:
${...}
그리고 이 변수 표현식에는 스프링 EL이라는 스프링이 제공하는 표현식을 사용할 수 있다.
@GetMapping("/variable")
public String variable(Model model) {
// ... 생략
model.addAttribute("user", userA);
model.addAttribute("users", list);
model.addAttribute("userMap", map);
return "basic/variable";
}
Object
user.username
: user의 username을 프로퍼티 접근user.getUsername()
- ex)
<span th:text="${user.username}"></span>
- ex)
- user[‘username’]: 위와 같음
user.getUsername()
- ex)
<span th:text="${user['username']}">
- ex)
user.getUsername()
: user의getUsername()
을 직접 호출- ex)
<span th:text="${user.getUsername()}">
- ex)
List
users[0].username
: List에서 첫 번째 회원을 찾고 username 프로퍼티 접근list.get(0).getUsername()
users[0]['username']
: 위와 같음users[0].getUsername()
: List에서 첫 번째 회원을 찾고 메서드 직접 호출
Map
userMap['userA'].username
: Map에서 userA를 찾고, username 프로퍼티 접근map.get("userA").getUsername()
userMap['userA']['username']
: 위와 같음userMap['userA'].getUsername()
: Map에서 userA를 찾고 메서드 직접 호출
지역 변수 선언
th:with
를 사용하면 지역 변수를 선언해서 사용할 수 있다. 지역 변수는 선언한 태그 안에서만 사용할 수 있다.
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
타임리프 기본 객체
스프링 부트 3.0부터는 ${#request}
, ${#response}
, ${#session}
, ${#servletContext}
를 지원하지 않는다. ${#locale}
는 그대로 지원한다. 위 객체를 사용하려면 전부 Model에 담아서 전달해야 한다.
그런데 파라미터를 조회하는 일은 자주 있다. 매번 모델에서 request를 꺼내서 getParameter를 하면 매우 불편할 것이다. 이런 점을 해결하기 위해 편의 객체도 제공한다.
- HTTP 요청 파라미터 접근:
param
예)${param.paramData}
- HTTP 세션 접근:
session
예)${session.sessionData}
- 스프링 빈 접근:
@
예)${@helloBean.hello('Spring!')}
@GetMapping("/basic-objects")
public String basicObjects(Model model, HttpServletRequest request, HttpServletResponse response, HttpSession session) {
session.setAttribute("sessionData", "Hello Session");
model.addAttribute("request", request);
model.addAttribute("response", response);
model.addAttribute("servletContext", request.getServletContext());
return "basic/basic-objects";
}
@Component("helloBean")
static class HelloBean {
public String hello(String data) {
return "Hello " + data;
}
}
/resources/templates/basic/basic-objects.html
<h1>식 기본 객체 (Expression Basic Objects)</h1>
<ul>
<li>request = <span th:text="${request}"></span></li>
<li>response = <span th:text="${response}"></span></li>
<li>session = <span th:text="${session}"></span></li>
<li>servletContext = <span th:text="${servletContext}"></span></li>
<li>locale = <span th:text="${#locale}"></span></li>
</ul>
<h1>편의 객체</h1>
<ul>
<li>Request Parameter = <span th:text="${param.paramData}"></span></li>
<li>session = <span th:text="${session.sessionData}"></span></li>
<li>spring bean = <span th:text="${@helloBean.hello('Spring!')}"></span></li>
</ul>
타임리프 유틸리티 객체
#message
, #uris
, #dates
, #calendars
등등 많은 종류의 유틸리티 객체가 있다.
타임리프 유틸리티 객체: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expression-utility-objects
공식 docs를 보고 필요할 때 찾아서 사용하자.
자바 8 날짜용 유틸리티 객체
#temporals
@GetMapping("/date")
public String date(Model model) {
model.addAttribute("localDateTime", LocalDateTime.now());
return "basic/date";
}
<span th:text="${#temporals.format(localDateTime, 'yyyy-MM-dd HH:mm:ss')}"></span>
이 외에도 다양한 사용 방법이 존재한다.
URL 링크
타임리프에서 URL을 생성할 때는 @{...}
문법을 사용하면 된다.
단순한 URL
@{/hello}
/hello
쿼리 파라미터
@{/hello(param1=${param1}, param2=${param2})}
-
/hello?param1=data1¶m2=data2
()
에 있는 부분은 쿼리 파라미터로 처리된다.
-
경로 변수
@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}
-
/hello/data1/data2
- URL 경로상에 변수가 있으면
()
부분은 경로 변수로 처리된다.
-
경로 변수 + 쿼리 파라미터
@{/hello/{param1}(param1=${param1}, param2=${param2})}
-
/hello/data1?param2=data2
- 경로 변수와 쿼리 파라미터를 함께 사용할 수 있다.
- 간단하게 생각하면 경로 변수에 사용되지 않은 변수는 쿼리 파라미터로 처리한다고 생각하면 될 듯
-
리터럴
타임리프는 다음과 같은 리터럴이 있다.
- 문자:
'hello'
- 숫자:
10
- 불린:
true
,false
- null:
null
타임리프에서 문자 리터럴은 항상 '
(작은 따옴표)로 감싸야 한다.<span th:text="'hello'"></span>
공백 없이 쭉 이어진다면 하나의 의미있는 토큰으로 인지해서 다음과 같이 작은 따옴표를 생략할 수 있다.<span th:text="hello"></span>
실수 주의!<span th:text="hello world!"></span>
문자 리터럴은 원칙상 '
로 감싸야 한다. 중간에 공백이 있어서 하나의 의미있는 토큰으로도 인식되지 않는다.
수정: <span th:text="'hello world!'"></span>
예시
<li>'hello ' + ${data} = <span th:text="'hello ' + ${data}"></span></li>
<li>리터럴 대체 |hello ${data}| = <span th:text="|hello ${data}|"></span></li>
리터럴 대체 (Literal substitutions)
<span th:text="|hello ${data}|"></span>
마지막의 리터럴 대체 문법을 사용하면 마치 템플릿을 사용하는 것처럼 편리하다.
연산
타임리프 연산은 자바와 크게 다르지 않다. HTML 안에서 사용하기 때문에 HTML 엔티티를 사용하는 부분만 주의하면 된다고 한다.
비교연산
HTML 엔티티를 사용해야 하는 부분을 주의하자.
>
(gt
),<
(lt
),>=
(ge
),<=
(le
),!
(not
),==
(eq
),!=
(neq
,ne
)
조건식
<li>(10 % 2 == 0) ? '짝수' : '홀수' = <span th:text="(10 % 2 == 0) ? '짝수' : '홀수'"></span></li>
삼항 연산자처럼 사용하면 된다.
Elvis 연산자
@GetMapping("/operation")
public String operation(Model model) {
model.addAttribute("nullData", null);
model.addAttribute("data", "Spring!");
return "basic/operation";
}
<li>${data} ?: '데이터가 없습니다.' = <span th:text="${data} ?: '데이터가 없습니다.'"></span></li>
<li>${nullData} ?: '데이터가 없습니다.' = <span th:text="${nullData} ?: '데이터가 없습니다.'"></span></li>
데이터가 null인 경우 뒤에 있는 문자열을 출력해준다.
◦ ${data} ?: '데이터가 없습니다.' = Spring!
◦ ${nullData} ?: '데이터가 없습니다.' = 데이터가 없습니다.
No-Operation_
인 경우 마치 타임리프가 실행되지 않는 것처럼 동작한다. 이것을 잘 사용하면 HTML의 내용 그대로 활용할 수 있다. 마지막 예를 보면 "데이터가 없습니다." 부분이 그대로 출력된다.
<li>${data} ?: _ = <span th:text="${data} ?: _">데이터가 없습니다.</span></li>
<li>${nullData} ?: _ = <span th:text="${nullData} ?: _">데이터가 없습니다.</span></li>
타임리프 태그 속성 (Attribute)
타임리프는 주로 HTML 태그에 th:*
속성을 지정하는 방식으로 동작한다. th:*
로 속성을 적용하면 기존 속성을 대체한다. 기존 속성이 없으면 새로 만든다.
속성 설정<input type="text" name="mock" th:name="userA" />
타임리프 렌더링 후<input type="text" name="userA" />
속성 추가
<h1>속성 추가</h1>
- th:attrappend = <input type="text" class="text" th:attrappend="class='large'"/><br/>
- th:attrprepend = <input type="text" class="text" th:attrprepend="class='large'"/><br/>
- th:classappend = <input type="text" class="text" th:classappend="large"/><br/>
- th:attrappend = <input type="text" class="text" th:attrappend="class=' large'"/><br/>
th:attrappend
: 속성 값의 뒤에 값을 추가한다.- 기존의 클래스 속성의 값 "text" 뒤에 " large"가 추가된다
- th:attrprepend = <input type="text" class="text" th:attrprepend="class='large '"/><br/>
th:attrprepend
: 속성 값의 앞에 값을 추가한다- 기존의 클래스 속성의 값 "text" 앞에 "large "가 추가된다
- th:classappend = <input type="text" class="text" th:classappend="large"/><br/>
th:classappend
:class
속성에 자연스럽게 추가한다.- 띄어쓰기 신경스기 귀찮으니 알아서 text에 large를 더해준다. classappend는 띄어쓰기 안해줘도 됨.
checked 처리
HTML에서는 <input type="checkbox" name="active" checked="false" />
이 경우에도 checked
속성이 있기 때문에 체크가 된다.
Why? HTML에서 checked
속성은 checked
속성의 값과 상관없이 checked
라는 속성만 있어도 체크가 된다.
타임리프의 th:checked
는 값이 false
인 경우 checked
속성 자체를 제거한다.<input type="checkbox" name="active" th:checked="false" />
타임리프 렌더링 후<input type="checkbox" name="active" />
반복
타임리프에서 반복은 th:each
를 사용한다.
<tr th:each="user : ${users}">
<td th:text="${user.username}">username</td>
<td th:text="${user.age}">0</td>
</tr>
반복 상태 유지
<tr th:each="user, userStat : ${users}">
반복의 두 번째 파라미터를 설정해서 반복의 상태를 확인할 수 있습니다.
두 번째 파라미터는 생략 가능한데, 생략하면 지정한 변수명 (user
) + Stat
가 됩니다.
여기서는 user
+ Stat
= userStat
이므로 생략 가능합니다.
반복 상태 유지 기능
index
: 0부터 시작하는 값count
: 1부터 시작하는 값size
: 전체 사이즈even
,odd
: 홀수, 짝수 여부 (boolean)first
,last
: 처음, 마지막 여부 (boolean)current
: 현재 객체
<td>
index = <span th:text="${userStat.index}"></span>
count = <span th:text="${userStat.count}"></span>
size = <span th:text="${userStat.size}"></span>
even? = <span th:text="${userStat.even}"></span>
odd? = <span th:text="${userStat.odd}"></span>
first? = <span th:text="${userStat.first}"></span>
last? = <span th:text="${userStat.last}"></span>
current = <span th:text="${userStat.current}"></span>
</td>
조건부 평가
타임리프의 조건식: if
, unless
(if
의 반대)
if, unless
타임리프는 해당 조건이 맞지 않으면 태그 자체를 렌더링하지 않는다.
<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
만약 다음 조건이 false
인 경우 <span>...</span>
부분 자체가 렌더링 되지 않고 사라진다.
switch
자바의 switch-case 문과 같이 타임리프에서도 switch문을 사용할 수 있다.
<tr th:each="user, userStat : ${users}">
...
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="*">기타</span>
</td>
</tr>
*
은 만족하는 조건이 없을 때 사용하는 디폴트이다.
주석
- 표준 HTML 주석
- 자바스크립트의 표준 HTML 주석은 타임리프가 렌더링 하지 않고, 그대로 남겨둔다.
<h1>1. 표준 HTML 주석</h1>
<!--
<span th:text="${data}">html data</span>
-->
- 타임리프 파서 주석
- 타임리프의 진짜 주석이다. 렌더링에서 주석 부분을 제거한다.
<h1>2. 타임리프 파서 주석</h1>
<!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->
<!--위 코드가 통으로 날라간다-->
- 타임리프 프로토타입 주석
- 잘 쓰지 않는 특이한 주석이다. HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 렌더링한 경우에만 보이는 기능이다.
<h1>3. 타임리프 프로토타입 주석</h1>
<!--/*/
<span th:text="${data}">html data</span>
/*/-->
블록
<th:block>
은 HTML 태그가 아닌 타임리프의 유일한 자체 태그다.
지금까지는 아래와 같이 특정 태그의 속성으로 each를 사용했었다.
<div th:each="user : ${users}">
사용자 이름: <span th:text="${user.username}"></span><br>
사용자 나이: <span th:text="${user.age}"></span>
</div>
그런데 아래에 div 태그가 하나 더 있다면? 또 each로 루프를 돌려야 하는 번거로움이 있기 때문에, 타임리프에서 유일하게 block이라는 태그를 제공한다.
<th:block th:each="user : ${users}">
<div>
사용자 이름: <span th:text="${user.username}"></span>
</div>
<div>
요약: <span th:text="${user.username} + ' / ' + ${user.age}"></span>
</div>
</th:block>
태그의 속성 밖으로, 상위 레벨로 루프를 빼서 여러 하위 태그에서 객체를 사용할 수 있다.
실제로 <th:block>
은 렌더링 시 제거된다.
자바스크립트 인라인
자바스크립트 인라인 기능은 다음과 같이 적용하면 된다.<script th:inline="javascript">
그러면 자바스크립트 인라인 기능이 뭘까?
<!-- 자바스크립트 인라인 사용 전 -->
<script>
var username = [[${user.username}]];
// 이렇게 하면 안되고 "[[${user.username}]]"로 해야 한다.
// 문자면 따옴표 해줘야 하고, 숫자면 빼야 하고,,, 이런거 챙기기 너무 어렵다.
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
// user 객체의 toString()이 호출된 결과가 들어간다.
</script>
자바스크립트 인라인 기능을 사용하지 않으면var username = userA
이런 식으로 userA라는 변수명으로 적용이 되어 오류가 발생한다.
우리가 원하는 건 var username = "userA"
이다.
문자면 따옴표 해줘야 하고, 숫자면 빼야 하고,,, 이런거 챙기기 너무 어렵다.
자바스크립트 인라인 기능을 사용하면 문자 타입인 경우 "
를 포함해준다. 추가로 자바스크립트에서 문제가 될 수 있는 문자가 포함되어 있으면 이스케이프 처리도 해준다. 예) ”
“\"”
자바스크립트 내추럴 템플릿
타임리프는 HTML 파일을 직접 열어도 동작하는 내추럴 템플릿 기능을 제공한다.
var username2 = /*[[${user.username}]]*/ "test username”;
- 인라인 사용 전:
var username2 = /*userA*/ "test username";
- 인라인 사용 후:
var username2 = "userA”;
- 인라인 사용 전:
/* */ 주석 부분을 제거하고 “userA”가 적용됨을 확인할 수 있다.
객체
타임리프의 자바스크립트 인라인 기능을 사용하면 객체를 JSON으로 자동으로 변환해준다.
var user = [[${user}]];
- 인라인 사용 전:
var user = BasicController.User(username=userA, age=10);
- 인라인 사용 후:
var user = {"username":"userA","age":10};
- 인라인 사용 전:
- 인라인 사용 전은 객체의
toString()
이 호출된 값이다. - 인라인 사용 후는 객체를 JSON으로 변환해준다.
자바스크립트 인라인 each
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
렌더링 결과
<script>
var user1 = {"username":"userA","age":10};
var user2 = {"username":"userB","age":20};
var user3 = {"username":"userC","age":30};
</script>
템플릿 조각
웹 페이지를 개발할 때는 공통 영역을 매 번 복사해서 사용하면 일부분 변경 시 여러 페이지를 모두 수정해야 하는 비효율적인 문제가 존재한다. 타임리프는 이런 문제를 해결하기 위해 템플릿 조각과 레이아웃 기능을 지원한다.
예시를 통해 봐보자./resources/templates/template/fragment/footer.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="copy">
푸터 자리 입니다.
</footer>
<footer th:fragment="copyParam (param1, param2)">
<p>파라미터 자리 입니다.</p>
<p th:text="${param1}"></p>
<p th:text="${param2}"></p>
</footer>
</body>
</html>
th:fragment
가 있는 태그는 다른 곳에 포함되는 코드 조각으로 이해하면 된다.
/resources/templates/template/fragment/fragmentMain.html
<h1>부분 포함</h1>
<h2>부분 포함 insert</h2>
<div th:insert="~{template/fragment/footer :: copy}"></div>
<!-- footer의 copy를 불러온다-->
<!-- div 안에 Insert-->
<h2>부분 포함 replace</h2>
<div th:replace="~{template/fragment/footer :: copy}"></div>
<!-- div 태그 자체를 교체한다-->
<h2>부분 포함 단순 표현식</h2>
<div th:replace="template/fragment/footer :: copy"></div>
<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
template/fragment/footer :: copy
template/fragment/footer.html
템플릿에 있는 th:fragment="copy"
라는 부분을 템플릿 조각으로 가져와서 사용한다는 의미이다.
부분 포함 insert<div th:insert="~{template/fragment/footer :: copy}"></div>
th:insert
를 사용하면 현재 태그 (div
) 내부에 추가한다.
<h2>부분 포함 insert</h2>
<div>
<footer>
푸터 자리 입니다.
</footer>
</div>
부분 포함 replace<div th:replace="~{template/fragment/footer :: copy}"></div>
th:replace
를 사용하면 현재 태그 (div
)를 대체한다.
<h2>부분 포함 replace</h2>
<footer>
푸터 자리 입니다.
</footer>
부분 포함 단순 표현식<div th:replace="template/fragment/footer :: copy"></div>
~{...}
를 사용하는 것이 원칙이지만 템플릿 조각을 사용하는 코드가 단순하면 이 부분을 생략할 수 있다. 복잡할 때는 이렇게 못쓴다.
파라미터 사용<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
footer.html
의 copyParam
부분
<footer th:fragment="copyParam (param1, param2)">
<p>파라미터 자리 입니다.</p>
<p th:text="${param1}"></p>
<p th:text="${param2}"></p>
</footer>
파라미터로 데이터1, 데이터 2가 전달되어 아래와 같이 html이 렌더링된다.
<h1>파라미터 사용</h1>
<footer>
<p>파라미터 자리 입니다.</p>
<p>데이터1</p>
<p>데이터2</p>
</footer>
템플릿 레이아웃
위 예시는 코드 조각을 가져와서 해당 html에 사용하는 예제였다. 이제는 코드 조각을 레이아웃에 넘겨서 사용하는 방법에 대해 다룬다.
공통 정보들을 한 곳에 모아두고, 공통으로 사용하지만 각 페이지마다 필요한 정보를 더 추가해서 사용하고 싶다면 다음과 같이 사용하면 된다.
/resources/templates/template/layout/base.html
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common_header(title,links)">
<title th:replace="${title}">레이아웃 타이틀</title>
<!-- 공통 -->
<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
<link rel="shortcut icon" th:href="@{/images/favicon.ico}">
<script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>
<!-- 추가 -->
<th:block th:replace="${links}" />
</head>
/resources/templates/template/layout/layoutMain.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
<!-- common header의 title에 <title>메인타이틀</title> 자체가 넘어가버린다.-->
<!-- base에 title이 넘어가서 base의 th:replace에 title 부분이 메인타이틀로 교체되어버린다.-->
<!-- 레이아웃이 있고, 원하는 부분만 바꿔치기 해준다고 생각하면 된다.-->
<title>메인 타이틀</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
<!-- css도 마찬가지로 link 태그를 따라 base로 넘어간다-->
</head>
<body>
메인 컨텐츠
</body>
</html>
레이아웃이 있고, 원하는 부분만 바꿔치기 해준다고 생각하면 된다.
common_header(~{::title},~{::link})
부분이 핵심이다.
::title
은 현재 페이지의<title>
태그들을 전달한다.::link
는 현재 페이지의<link>
태그들을 전달한다.
결과를 보면
- 메인 타이틀이 전달한 부분으로 교체되었다.
- 공통 부분은 그대로 유지되고, 추가 부분에 전달한
<link>
들이 포함된 것을 확인할 수 있다.
레이아웃 개념을 두고, 그 레이아웃에 필요한 코드 조각을 전달해서 완성하는 것으로 이해하면 된다.
또한, 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}">
<!-- 생략 -->
<html>
에 th:fragment
속성이 정의되어 있다.
이 레이아웃 파일을 기본으로 하고 여기에 필요한 내용을 전달해서 부분부분 변경하는 것으로 이해하면 된다.
결국 layoutFile.html
에 필요한 내용을 전달하면서 <html>
자체를 layoutFile.html
로 변경한다.
'Spring > MVC' 카테고리의 다른 글
[Spring MVC] 07. 스프링 MVC - 웹 페이지 만들기(타임리프, PRG, RedirectAttribute) (0) | 2024.12.26 |
---|---|
[Spring/MVC] WebMvcConfigurer (0) | 2024.12.24 |
[Spring MVC] 06. 스프링 MVC - 기본 기능 (0) | 2024.11.24 |
[Spring MVC] 05. 스프링 MVC - 구조 이해 (0) | 2024.11.21 |
[Spring MVC] 04. MVC 프레임워크 만들기 (0) | 2024.11.20 |