타임리프 소개
기본 표현식
타임리프는 다음과 같은 기본 표현식들을 제공한다. 지금부터 하나씩 알아보자.
• 간단한 표현:
◦ 변수 표현식: ${...}
◦ 선택 변수 표현식: *{...}
◦ 메시지 표현식: #{...}
◦ 링크 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 엔티티(<, >)로 변경하는 것을 이스케이프(escape)라 한다
타임리프가 제공하는 th:text , [[...]] 는 기본적으로 이스케이스(escape)를 제공한다.
※따라서 th:text 또는 [[...]]를 사용하는 경우(escape)
→ 여는 태그 "<"를 기호 그대로 "<" 표시한다.
→ 닫는 태그 ">"를 기호 그대로 ">" 표시한다.
※th:utext 와 [ (. . .) ]를 사용하는 경우(Unescape)
→ 태그(ex-진하게)가 그대로 적용된다.
주의!
실제 서비스를 개발하다 보면 escape를 사용하지 않아서 HTML이 정상 렌더링 되지 않는 수 많은 문제가
발생한다. escape를 기본으로 하고, 꼭 필요한 때만 unescape를 사용하자.
SpringEL 다양한 표현식 사용
model.addAttribute("user", userA); //model.addAttribute("이름", 객체)
model.addAttribute("users", list);
model.addAttribute("userMap", map);
Object(객체)
user.username : user의 username을 프로퍼티 접근 → user.getUsername()
user['username'] : 위와 같음→ user.getUsername()
user.getUsername() : user의 getUsername() 을 직접 호출
List(객체를 담은 리스트)
users[0].username : List에서 첫 번째 회원을 찾고 username 프로퍼티 접근→list.get(0).getUsername()
users[0]['username'] : List에서 첫 번째 회원을 찾고 username 프로퍼티 접근→list.get(0).getUsername()
Map(객체이름, 객체)
userMap['userA'].username : Map에서 userA를 찾고, username 프로퍼티 접근→map.get("userA").getUsername()
userMap['userA']['username'] : 위와 같음
userMap['userA'].getUsername() : Map에서 userA를 찾고 메서드 직접 호출
지역 변수 선언
th:with 를 사용하면 지역 변수를 선언해서 사용할 수 있다. 지역 변수는 선언한 테그 안에서만 사용할 수
있다.
기본 객체들
타임리프는 기본 객체들을 제공한다.
• ${#request}
• ${#response}
• ${#session}
• ${#servletContext}
• ${#locale}
그런데 #request 는 HttpServletRequest 객체가 그대로 제공되기 때문에 데이터를 조회하려면
request.getParameter("data") 처럼 불편하게 접근해야 한다.
통상적인 경우 파라미터를 model에 담아서 view template으로 넘겨서 렌더링 하는데,
${param.paramData}를 사용하는 경우 별도의 절차 없이 바로 불러서 조회 할 수 있다.
이런 점을 해결하기 위해 편의 객체도 제공한다.
HTTP 요청 파라미터 접근: param
예) ${param.paramData}
HTTP 세션 접근: session
예) ${session.sessionData}
스프링 빈 접근: @
예) ${@helloBean.hello('Spring!')}
Url 링크
@GetMapping("/link")
public String link(Model model){
model.addAttribute("param1", "data1");
model.addAttribute("param2", "data2");
return "basic/link";
}
<li><a th:href="@{/hello}">basic url</a></li>
<li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">hello query param</a></li>
<li><a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>
<li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>
1. @{/hello} → /hello?
2. @{/hello(param1=${param1}, param2=${param2})} → /hello?param1=data1¶m2=data2
3. @{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}
/hello/{param1}/{param2}
/hello/data1/data2
4. @{/hello/{param1}(param1=${param1}, param2=${param2})}
/hello/{param1}?param2=data2
/hello/data1?param2=data2
리터럴
Literals
리터럴은 소스 코드상에 고정된 값을 말하는 용어이다.
예를 들어서 다음 코드에서 "Hello" 는 문자 리터럴, 10 , 20 는 숫자 리터럴이다.
String a = "Hello"
int a = 10 * 20
• 문자: 'hello'
• 숫자: 10
• 불린: true , false
• null: null
타임리프에서 문자 리터럴은 항상 ' (작은 따옴표)로 감싸야 한다.
<span th:text="'hello'">
그런데 문자를 항상 ' 로 감싸는 것은 너무 귀찮은 일이다. 공백 없이 쭉 이어진다면 하나의 의미있는
토큰으로 인지해서 다음과 같이 작은 따옴표를 생략할 수 있다.
룰: A-Z , a-z , 0-9 , [] , . , - , _
<span th:text="hello">
오류
<span th:text="hello world!"></span>
문자 리터럴은 원칙상 ' 로 감싸야 한다. 중간에 공백이 있어서 하나의 의미있는 토큰으로도 인식되지
않는다.
수정
<span th:text="'hello world!'"></span>
이렇게 ' 로 감싸면 정상 동작한다.
리터럴 대체(Literal substitutions)
<span th:text="|hello ${data}|">
마지막의 리터럴 대체 문법을 사용하면 마치 템플릿을 사용하는 것 처럼 편리하다
연산
타임리프 연산은 자바와 크게 다르지 않다. HTML안에서 사용하기 때문에 HTML 엔티티를 사용하는
부분만 주의하자.
• No-Operation: _ 인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작한다.
이것을 잘 사용하면 HTML의 내용 그대로 활용할 수 있다.
속성 값 설정
타임리프는 주로 HTML 태그에 th:* 속성을 지정하는 방식으로 동작한다. th:* 로 속성을 적용하면 기존
속성을 대체한다. 기존 속성이 없으면 새로 만든다.
<input type="text" name="mock" th:name="userA" />
→타임리프 렌더링 후 <input type="text" name="userA" />
checked 처리
HTML에서는 <input type="checkbox" name="active" checked="false" /> 이 경우에도
checked 속성이 있기 때문에 checked 처리가 되어버린다.
HTML에서 checked 속성은 checked 속성의 값과 상관없이 checked 라는 속성만 있어도 체크가 된다.
이런 부분이 true , false 값을 주로 사용하는 개발자 입장에서는 불편하다.
타임리프의 th:checked 는 값이 false 인 경우 checked 속성 자체를 제거한다.
<input type="checkbox" name="active" th:checked="false" />
→ 타임리프 렌더링 후: <input type="checkbox" name="active" />
반복
타임리프에서 반복은 th:each 를 사용한다. 추가로 반복에서 사용할 수 있는 여러 상태 값을 지원한다.
반복 기능
<tr th:each="user : ${users}">
반복시 오른쪽 컬렉션( ${users} )의 값을 하나씩 꺼내서 왼쪽 변수( user )에 담아서 태그를 반복
실행합니다.
th:each 는 List 뿐만 아니라 배열, java.util.Iterable , java.util.Enumeration 을 구현한 모든
객체를 반복에 사용할 수 있습니다. Map 도 사용할 수 있는데 이 경우 변수에 담기는 값은 Map.Entry
입니다.
반복 상태 유지
<tr th:each="user, userStat : ${users}">
반복의 두번째 파라미터를 설정해서 반복의 상태를 확인 할 수 있습니다.
두번째 파라미터는 생략 가능한데, 생략하면 지정한 변수명( user ) + Stat 가 됩니다.
여기서는 user + Stat = userStat 이므로 생략 가능합니다.
반복 상태 유지 기능
index : 0부터 시작하는 값
count : 1부터 시작하는 값
size : 전체 사이즈
even , odd : 홀수, 짝수 여부( boolean )
first , last :처음, 마지막 여부( boolean )
current : 현재 객체
템플릿 조각
웹 페이지를 개발할 때는 공통 영역이 많이 있다. 예를 들어서 상단 영역이나 하단 영역, 좌측 카테고리 등등
여러 페이지에서 함께 사용하는 영역들이 있다. 이런 부분을 코드를 복사해서 사용한다면 변경시 여러
페이지를 다 수정해야 하므로 상당히 비효율 적이다. 타임리프는 이런 문제를 해결하기 위해 템플릿 조각과
레이아웃 기능을 지원한다.
/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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>부분 포함</h1>
<h2>부분 포함 insert</h2>
<div th:insert="~{template/fragment/footer :: copy}"></div>
<h2>부분 포함 replace</h2>
<div th:replace="~{template/fragment/footer :: copy}"></div>
<h2>부분 포함 단순 표현식</h2>
<div th:replace="template/fragment/footer :: copy"></div>
<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
</body>
</html>
template/fragment/footer :: copy : template/fragment/footer.html 템플릿에 있는 th:fragment="copy" 라는 부분을 템플릿 조각으로 가져와서 사용한다는 의미이다
템플릿 레이아웃1
템플릿 레이아웃
이전에는 일부 코드 조각을 가지고와서 사용했다면, 이번에는 개념을 더 확장해서 코드 조각을 레이아웃에
넘겨서 사용하는 방법에 대해서 알아보자.
예를 들어서 <head> 에 공통으로 사용하는 css , javascript 같은 정보들이 있는데, 이러한 공통
정보들을 한 곳에 모아두고, 공통으로 사용하지만, 각 페이지마다 필요한 정보를 더 추가해서 사용하고
싶다면 다음과 같이 사용하면 된다.
@GetMapping("/layout")
public String layout(){
return "/template/layout/layoutMain";
}
/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>
<body>
</body>
</html>
/resources/templates/template/layout/layoutMain.html
(layoutMain은 자신을 사용하지 않고,
공통된 요소(base)를 사용하되 layoutMain의 개성을 부분적으로 이용★)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
<title>메인 타이틀</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>
<body>
메인 컨텐츠
</body>
</html>
현재 나 자신의 페이지(layoutMain.html)를 사용하지 않고,
th:replace="template/layout/base를 이용해서 base로 대체
그런데, base의 추가요소는 layoutMain자신의 title과 link를 사용한다.
common_header(~{::title},~{::link}) 이 부분이 핵심이다.
::title 은 현재 페이지(layoutMain.html)의 title 태그들을 base로 전달한다.
::link 는 현재 페이지(layoutMain.html)의 link 태그들을 base로 전달한다
결과
<!DOCTYPE html>
<html>
<head>
<title>메인 타이틀</title>
<!-- 공통 -->
<link rel="stylesheet" type="text/css" media="all" href="/css/awesomeapp.css">
<link rel="shortcut icon" href="/images/favicon.ico">
<script type="text/javascript" src="/sh/scripts/codebase.js"></script>
<!-- 추가 -->
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/themes/smoothness/jquery-ui.css">
</head>
<body>
메인 컨텐츠
</body>
</html>
템플릿 레이아웃2
템플릿 레이아웃 확장
앞서 이야기한 개념을 <head> 정도에만 적용하는게 아니라 <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>
/resources/templates/template/layoutExtend/layoutExtendMain.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>
layoutExtendMain.html은 자신을 사용하지 않고, layoutFile.html로 대체★
layout(~{::title},~{::section}) 이 부분이 핵심이다.
::title 은 현재 페이지(layoutExtendMain.html)의 title 태그들을 layoutFile.html로 전달한다.
::section 는 현재 페이지(layoutExtendMain.html)의 section태그들을 layoutFile.html로 전달한다
결과
<!DOCTYPE html>
<html>
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
<footer>
레이아웃 푸터
</footer>
</body>
</html>
layoutFile.html 을 보면 기본 레이아웃을 가지고 있는데, <html> 에 th:fragment 속성이 정의되어
있다. 이 레이아웃 파일을 기본으로 하고 여기에 필요한 내용을 전달해서 부분부분 변경하는 것으로
이해하면 된다.
layoutExtendMain.html 는 현재 페이지인데, <html> 자체를 th:replace 를 사용해서 변경하는 것을
확인할 수 있다. 결국 layoutFile.html 에 필요한 내용을 전달하면서 <html> 자체를
layoutFile.html 로 통째로 변경한다.
'스프링 > 스프링 MVC 패턴 2편' 카테고리의 다른 글
5. 검증2 - Bean Validation (0) | 2022.12.01 |
---|---|
4. 검증1 - Validation (0) | 2022.11.30 |
3. 메시지, 국제화 (0) | 2022.11.29 |
2. 타임 리프 - 스프링 통합과 폼 (0) | 2022.11.28 |