간단하게 공부하고 Spring을 사용한 CRUD 게시판 만들기에 적용해볼 예정이다.
준비물 : JDK - Java 11+
, IDE - IntelliJ
or Eclipse
, Dependencies - Spring Web
, Lombok
, Thymeleaf
서버 사이드 HTML 렌더링 : 백엔드 서버에서 HTML을 동적으로 렌더링하는 용도로 사용된다. 사용법은 SSR이 다 비슷하기에 학습하기 어렵지 않고, 페이지가 어느 정도 정적이고 빠른 생산성이 필요한 경우 백엔드 개발자가 개발해야 하는 일이 생기는데 이 경우 타임리프는 좋은 선택지이다.
네츄럴 템플릿 : 타임리프는 순수한 HTML을 최대한 유지하려 한다. 확장자도 .html
이고 웹브라우저에서 직접 파일을 열어도 내용을 확인할 수 있다. ( 이 경우 동적인 결과 렌더링은 되지 않지만 HTML 마크업 언어가 어떻게 되는지 확인할 수 있다. )
템플릿 엔진 : 템플릿 양식과 특정 데이터 모델에 따른 입력자료를 합성하여 결과 문서를 출력하는 소프트웨어를 말한다.
스프링 통합 지원 : 타임리프는 스프링가 자연스럽게 통합되어 스프링의 다양한 기능을 쉽게 사용할 수 있다.
타임리프는 HTML 문서 상단에 밑의 코드를 작성하여 사용할 수 있다.
<html xmlns:th="https://www.thymeleaf.org">
타임리프에서 사용하는 문법은 아래와 같이 요약할 수 있다.
${...}
*{...}
#{...}
@{...}
~{...}
true
,false
null
+
|
( The name is ${name}|
)+
,-
,/
,%
-
and
,or
!
, not
>
,<
,>=
,<=
(gt
,lt
,ge
,le
)==
,!=
( eq
,ne
)(if) ? (then)
(if) ? (then) : (else)
(value) ?: (default value)
_
서버에서 Model에 담아준 각종 속성(attribute)들을 서버사이드 템플릿 엔진인 타임리프에서는 여러 방법으로 표현할 수 있다. 가장 기본적인 텍스트 출력 방법은 아래와 같다.
<span th:text="${attributeName}"></span>
[[...]]
<span>hello [[${attributeName}]]</span>
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("text-basic")
public String textBasic(Model model){
model.addAttribute("data","Hello Spring");
return "basic/text-basic";
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th = "http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>컨텐츠 데이터 출력하기</h1>
<ul>
<li>th:text 사용 <span th:text="${data}"></span></li>
<li>컨텐츠 안에서 직접 출력하기 [[${data}]]</li>
</ul>
</body>
</html>
Unescape 기능
HTML 엔티티로 변경하지 않고 HTML 태그로 사용하고 싶은 경우 , escape하지 않고 unescape하게 쓰고 싶은 경우
th:utext
[(...)]
를 사용한다.
기본적으로 변수 표현식은 ${...}
를 사용하고 이는 단순히 변수의 값을 표시한다. 이 변수 표현식에서는 SpringEL 이라는 스프링이 제공하는 표현식을 사용할 수 있다.
단순한 변수하면 ${data}
로 바로 표현이 가능하지만, Object나 List같은 객체는 아래와 같이 사용할 수 있다.
data.field
: data의 field 프로퍼티 접근 ( data.getField())data['field']
: 위와 같다.data.getField()
: data의 getField 메서드를 바로 호출 가능list[0].field
: List의 첫번째 요소에서 field 프로퍼티를 가져올 수 있다.list[0]['field']
: 위와 동일list[0].getField()
: 메서드 직접 호출도 가능list.get(0).xxx
: List의 get메서드를 사용하여 데이터를 찾아서 프로퍼티 접근도 가능하다.map['key'].field
: Map에서 key를 찾아 field프로퍼티에 접근한다. ( map.get('key').field
와 동일)map['key']['field']
: 위와 동일map['key'].getField()
: 메서드 호출 가능th:with
를 이용해서 지역 변수를 선언할 수 있으며, 지역 변수이기에 선언된 태그내에서만 사용이 가능하다.
<div th:with="item=${list[0]}">
<ul>
<li>이름 : <span th:text="${item.username}"></span></li>
<li>나이 : [["${item.age}"]]</li>
</ul>
</div>
@GetMapping("/variable")
public String variable(Model model){
User userA = new User("userA",10);
User userB = new User("userB",20);
List<User> list = new ArrayList<>(Arrays.asList(userA, userB));
Map<String,User> map = new HashMap<>();
map.put("userA",userA);
map.put("userB",userB);
model.addAttribute("user",userA);
model.addAttribute("users",list);
model.addAttribute("userMap",map);
return "basic/variable";
}
@Data
static class User{
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
-> 테스트를 위해 InnerClass로 User를 만들었다.
-> Model에 Object,List,Map 모두 넣어준다.
<!DOCTYPE html>
<html lang="en" xmlns:th = "http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>SpringEL 표현식</h1>
<ul>Obejct
<li>${user.username} = <span th:text="${user.username}"></span></li>
<li>${user['username']} = <span th:text="${user['username']}"></span></li>
<li>${user.getUsername()} = <span th:text="${user.getUsername()}"></span></li>
</ul>
<ul>List
<li>${users[0].username} = <span th:text="${users[0].username}"></span></li>
<li>${users[0]['username']} = <span th:text="${users[0]['username']}"></span></li>
<li>${users[0].getUsername()} = <span th:text="${users[0].getUsername()}"></span></li>
</ul>
<ul>Map
<li>${userMap['userA'].username} = <span th:text="${userMap['userA'].username}"></span></li>
<li>${userMap['userA']['username']} = <span th:text="${userMap['userA']['username']}"></span></li>
<li>${userMap['userA'].getUsername()} = <span th:text="${userMap['userA'].getUsername()}"></span></li>
</ul>
<div th:with="item=${users[0]}">
<ul>
<li>이름 : <span th:text="${item.username}"></span></li>
<li>나이 : <span th:text="${item.age}"></span></li>
</ul>
</div>
</body>
</html>
Map 부분에서 빨간줄 뜨는데 무시하고 실행하니 정상적으로 동작함
타임리프는 아래와 같이 기본 객체들을 사용할 수 있게 제공해준다.
${#request}
${#session}
${#response}
${#servletContext}
${#locale}
기본 객체들의 프로퍼티 접근을 위해서 편의메서드를 제공한다.
${param.paramData}
${@helloBean.hello('Spring')}
@GetMapping("/basic-objects")
public String basicObject(HttpSession httpSession){
httpSession.setAttribute("sessionData","Hello Session");
return "basic/basic-objects";
}
@Component("helloBean")
static class HelloBean{
public String hello(String data){
return "Hello" + data;
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th = "http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>식 기본 객체 (Expression Basic Objects) </h1>
<ul>
<li>request = <span th:text="${#session}"></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>
</body>
</html>
위의 예제는
http://localhost:8080/basic/basic-objects?paramData=HelloParam
로 실행시켜야 Request Parameter를 보여줄수 있다. 즉,${param.paramData}
의 paramData를 parameter 이름으로 지정해주어야 한다.
타임리프는 아래와 같이 편의성 유틸리티 객체들 또한 제공한다.
#message
: 메세지,국제화 처리#dates
: java.util.Date 서식 지원#calendars
: java.util.Calendar 서식#numbers
: 숫자 서식 지원#strings
: 문자 관련 편의 기능#objects
: 객체 관련 기능 제공#uris
: URI 이스케이프 지원#arrays
: 배열 관련 기능 제공#lists
,#sets
,#maps
: 컬렉션 관련 기능#ids
: 아이디 처리 관련 기능 제공#bools
: boolean 관련 기능타임리프에서 URL을 생성할 때는 @{...}
문법을 사용하면 된다.
단순한 URL 표현 : @{/hello}
( /hello)
쿼리 파라미터를 포함하는 URL 표현 :
@{/hello(param1=${param1},param2=${param2})}
/hello?param1=data1¶m2=data2
경로 변수 [ Path Variable ]
@{/hello/{param1}/{param2}(param1=${param1},param2=${param2})}
/hello/data1/data2
()
는 경로 변수로 처리된다.경로 변수 + 쿼리 파라미터
@{/hello/{param1}(param1=${param1},param2=${param2})}
/hello/data1?param2=data2
BasicController
@GetMapping("/link")
public String link(Model model){
model.addAttribute("param1","data1");
model.addAttribute("param2","data2");
return "basic/link";
}
- **link.html**
```html
<!DOCTYPE html>
<html lang="en" xmlns:th = "http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>URL 링크</h1>
<ul>
<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})}"> both</a></li>
</ul>
</body>
</html>
각각의 링크를 타고 들어갓을 경우 나타나는 url 이다.
소스 코드상에서 고정된 값을 리터럴이라 한다.
타임리프는 다음과 같은 리터럴이 있다.
문자 리터럴은 항상 작은 따옴표('
)로 감싸줘야 한다.
<span th:text="'text'"></span>
문자 리터럴에서 공백이 없다면 작은 따옴표를 생략할 수 있다.
-> 룰 : A-Z
,a-z
,0-9
,[]
,,
,-
,_
<span th:text="text"></span>
<span th:text="hello world"></span> <!-- 공백이 있기 때문에 작은 따옴표가 있어야 한다. --!>
@GetMapping("/literal")
public String literal(Model model){
model.addAttribute("data", "Spring!");
return "basic/literal";
}
<!DOCTYPE html>
<html lang="en" xmlns:th = "http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>리터럴</h1>
<ul>
<li>'hello' + 'world!' = <span th:text="'hello' + 'world'"> </span></li>
<li>'hello world!' = <span th:text="'hello world!'"> </span></li>
<li>'hello' + ${data} = <span th:text="'hello' + ${data}"> </span></li>
<li>리터럴 대체 |hello ${data}| = <span th:text="|hello ${data}|"> </span></li>
</ul>
</body>
</html>
타임리프의 연산은 자바의 연산과 차이점이 없다.
비교 연산자 : >
(gt),<
(lt),>=
(ge),<=
(le),!
(not),==
(eq),!=
(neq,ne)
조건식 : 자바의 조건식과 유사하다. , [삼항 연산자](10%2==0)?'짝수':'홀수'
Elvis 연산자 : 조건식의 편의 버전
-> ${data}?:defaultValue
No-Operation : _
인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작한다.
-> <p th:text="${nullData}?:_"> default value </p>
@GetMapping("/operation")
public String operation(Model model){
model.addAttribute("nullData", null);
model.addAttribute("data", "Spring!");
return "basic/operation";
}
<!DOCTYPE html>
<html lang="en" xmlns:th = "http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
<li>산술 연산
<ul>
<li>10+2 = <span th:text="10+2"> </span></li>
<li>10 % 2 = <span th:text="10%2"> </span></li>
</ul>
</li>
<li>비교 연산
<ul>
<li> 1 > 10 = <span th:text="1>10"> </span></li>
<li> 1 gt 10 = <span th:text="1 gt 10"> </span></li>
<li> 1 >= 10 = <span th:text="1 >= 10"> </span></li>
<li> 1 ge 10 = <span th:text="1 ge 10"> </span></li>
<li> 1 == 10 = <span th:text="1 == 10"> </span></li>
<li> 1 != 10 = <span th:text="1 != 10"> </span></li>
</ul>
</li>
<li>조건식
<ul>
<li>(10 %2 ==0)?'짝수':'홀수' = <span th:text="(10%2==0)?'짝수':'홀수'"> </span></li>
</ul>
</li>
<li>Elvis 연산자
<ul>
<li>${data}?':'데이터가없습니다.' = <span th:text="${data}?:'데이터가 없습니다.'"> </span></li>
<li>${nullData}?:'데이터가 없습니다.' = <span th:text="${nullData}?:'데이터가 없습니다.'"> </span></li>
</ul>
</li>
<li>No-Operation
<ul>
<li>#{data}?:_ = <span th:text="${data}?: _"> </span></li>
<li>${nullData}?:_=<span th:text="${nullData}?: _">데이터가 없습니다. </span></li>
</ul>
</li>
</ul>
</body>
</html>
타임리프에서 반복은 th:each
를 사용한다. java.util.Enumeration, java.util.Iterable 을 구현한 모든 객체는 해당 태그를 사용해서 반복할 수 있다. Map은 Map.Entry가 반복된다.
<tr th:each="아이템 : ${반복할리스트}">
<td th:text="${아이템.프로퍼티1}">default value</td>
<td th:text="${아이템.프로퍼티2}">defulat value</td>
</tr>
<tr th:each="아이템 : ${반복할리스트}" th:object="${아이템}">
<td th:text="*{프로퍼티1}">default value</td>
<tr>
반복자의 상태확인
th:each
로 반복을 할 때 반복하는 현재의 상태(ex:사이즈,홀수/짝수 여부, 처음/마지막 여부 등 ) 을 확인할 수 있다.
th:each="아이템 : ${반복할리스트}"
로 반복을 하는 경우 상태를 확인하고자 한다면 지정한 변수명(아이템) + Stat인 아이템Stat으로 상태값 접근이 가능하다. (itemStat.size
)
또한, 나만의 변수명으로 상태접근을 하고 싶다면 두번째 파라미터로 명시하면 된다.th:each="user, customStat : ${list}"
@GetMapping("/each")
public String each(Model model){
addUsers(model);
return "basic/each";
}
private void addUsers(Model model){
List<User> users = Arrays.asList(new User("userA",10),
new User("userB",20),
new User("userC",30));
model.addAttribute("users",users);
}
<!DOCTYPE html>
<html lang="en" xmlns:th = "http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>기본 테이블</h1>
<table border = "1">
<tr>
<th>username</th>
<th>age</th>
</tr>
<tr th:each="user : ${users}">
<td th:text="${user.username}"></td>
<td th:text="${user.age}"></td>
</tr>
</table>
<h1>반복 상태 유지</h1>
<table border = "1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
<th>etc</th>
</tr>
<tr th:each="user,userStat : ${users}">
<td th:text="${userStat.count}"></td>
<td th:text="${user.username}"></td>
<td th:text="${user.age}"></td>
<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>
</tr>
</table>
<h1> 선택 변수 표현식을 사용한 반복</h1>
<table border = "1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
<th>etc</th>
</tr>
<tr th:each="user,userStat : ${users}" th:object="${user}">
<td th:text="${userStat.count}"></td>
<td th:text="*{username}"></td>
<td th:text="*{age}"></td>
<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>
</tr>
</table>
</body>
</html>
타임 리프에서 조건식은 다음과 같이 사용할 수 있으며 조건에 해당되지 않으면 태그자체가 렌더링 되지 않는다.
<div th:switch="${조건대상 변수}">
<span th:case="비교변수1">value1</span>
<span th:case="비교변수2">value2</span>
<span th:case="*">default</span>
</div>
@GetMapping("/condition")
public String condition(Model model){
addUsers(model);
return "basic/condition";
}
<!DOCTYPE html>
<html lang="en" xmlns:th = "http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>if, unless</h1>
<table border ="1">
<tr>
<th>count</th>
<th>usernaem</th>
<th>age</th>
</tr>
<tr th:each="user,userStat : ${users}">
<td th:text="${userStat.count}"></td>
<td th:text="${user.username}"></td>
<td>
<span th:text="${user.age}"></span>
<span th:text="'미성년자'" th:if="${user.age < 20}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
</td>
</tr>
</table>
<h1>switch</h1>
<table border = "1">
<tr>
<th>count</th>
<th>username</th>
<th>age</th>
</tr>
<tr th:each="user,userStat : ${users}">
<td th:text="${userStat.count}"></td>
<td th:text="${user.username}"></td>
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="30">기타</span>
</td>
</tr>
</table>
</body>
</html>
타임리프의 유일한 자체 태그인 <th:block>
는 렌더링시 제거되는 태그이며 타임리프의 속성을 사용하기 애매한 경우 사용된다.
대표적으로 th:each
로 반복을 하고자 할때 반복의 대상이 한 요소가 아니라 동등한 레벨의 여러 요소를 그룹화하여 반복하고자 하면 th:block
이 유용하다.
<!DOCTYPE html>
<html lang="en" xmlns:th = "http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<th:block th:each="user : ${users}">
<div>
사용자 이름 <span th:text="${user.username}"></span>
사용자 나이 <span th:text="${user.age}"></span>
</div>
<div>
요약 <span th:text="${user.username + ' / '+user.age}"></span>
</div>
</th:block>
</body>
</html>
해당 기능은 타임리프가 자바스크립트를 편리하게 사용할 수 있도로 ㄱ도와주는 자바스크립트 인라인 기능을 제공한다. 간단하게 <script th:inline="javascript">
로 가능하다.
자바스크립트 인라인을 붙혔을 경우 문자열에는 자동으로 따옴표를 붙여주고, 객체는 자동으로 JSON으로 만들어준다. 또한, 자바스크립트에서 문제가 될 수 있는 문자가 있으면 이스케이프 처리도 해준다.
공통 부분을 템플릿화 하여 필요한 부분에서 해당 템플릿을 불러와 설정하는 기능이다.
<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="name"
해당 태그로 선언된 태그 내부가 템플릿이 되며 속성명이 템플릿 조각 이름이 된다. 해당 템플릿 조각을 사용하고 싶은 다른 영역에서 해당 이름을 사용해서 템플릿을 가져올 수 있다.
<div th:insert="~{template/fragment/footer :: copy}"></div>
<div th:replace="~{template/fragment/footer :: copy}"></div>
<div th:replace="template/fragment/footer :: copy"></div>
~{...}
을 사용해야 하지만 경로가 단순하면 생략할 수 있다.<div th:replace="~{template/fragment/footer::copyParam('데이터1','데이터2')}"></div>
- 공통된 부분을 하나의 템플릿으로 만들어 사용할 수 있다.
th:fragment="이름"
속성을 추가하면 템플릿이 되며 다른곳에서 이름으로 사용할 수 있다.th:insert
,th:replace
로 템플릿을 사용할 수 있다.- 기본적으로 조각표현식(
~{...}
)을 사용해야 하지만 표현이 간단하면 생략할 수 있다.- 파라미터는 (param,..)방식으로 사용이 가능하다.