동적인 html을 만들기 위해 사용하는 탬플릿 엔진이다. 템플릿 엔진은 타임리프, JSP, Freemarker, Groovy 등의 종류가 있지만, Thymleaf의 장점은 따로 확장파일을 만들지 않고, HTML 내의 태그로써 이를 사용할 수 있기 때문이다. 이를 보고 natural templates라고 한다.
사용자의 요청에 의해 유저에게 화면을 띄우는 과정은 아래와 같다.
사용자 요청 (Request) → Dispatcher Servlet → Controller → Service(내부 로직=컨트롤러에서 사용할 함수 구현) →
Repository(interface) → (DB) → Controller → Model(저장) → View (Thymeleaf로 Model이용) → 사용자 화면 (Response)
모든 것은 사용자가 브라우저 주소창에 URL을 입력하거나(GET), 폼(Form) 데이터를 전송(POST)하면서 시작됩니다.
GET 요청: "/users/1" 과 같은 특정 페이지 정보를 조회하고 싶을 때.
POST 요청: 회원가입 폼에 정보를 입력하고 '가입' 버튼을 눌러 데이터를 저장/처리하고 싶을 때.
이 요청은 스프링의 Dispatcher Servlet이라는 관문으로 가장 먼저 들어오게 됩니다. Dispatcher Servlet은 모든 요청을 받아서 어떤 컨트롤러에게 일을 시킬지 결정하는 똑똑한 안내원 역할을 합니다.
Dispatcher Servlet은 URL을 보고 이 요청을 처리할 담당자, 즉 컨트롤러(@Controller)의 메서드를 찾아 호출합니다.
컨트롤러는 마치 레스토랑의 메인 셰프와 같습니다. 주문(Request)을 받아서 어떤 요리(Business Logic)를 할지 결정하고, 필요한 재료(Data)를 준비하도록 지시합니다.
@Controller
public class UserController {
@GetMapping("/user-profile") // 1. GET 요청 접수
public String getUserProfile(Model model) { // 2. Model 객체를 파라미터로 받음
// 3. 비즈니스 로직 처리 (예: 서비스 계층 호출)
String userName = "홍길동";
int userAge = 30;
// 실제로는 DB에서 조회한 데이터를 가져옵니다.
// 4. Model에 데이터 담기
model.addAttribute("name", userName);
model.addAttribute("age", userAge);
// 5. 보여줄 뷰(HTML 파일)의 논리적 이름 반환
return "profile"; // "profile.html" 파일을 찾아줘!
}
}
① @GetMapping("/user-profile"): /user-profile 이라는 주소로 GET 요청이 오면 이 메서드를 실행하라고 알려줍니다.
② Model model: 화면에 데이터를 전달할 Model 객체를 준비합니다. 스프링이 알아서 이 객체를 만들어 메서드에 넣어줍니다.
③ 비즈니스 로직 처리: 지금은 간단히 변수를 선언했지만, 보통은 UserService 같은 서비스 계층을 호출해서 DB에서 데이터를 조회하는 등 실제 작업이 이루어집니다.
④ model.addAttribute("key", value): 가장 중요한 부분입니다! Model이라는 배달 상자에 "name"이라는 이름표를 붙여 "홍길동"이라는 데이터를 담습니다. 이 Model은 이제 컨트롤러의 손을 떠나 뷰(Thymeleaf)로 전달될 준비를 합니다.
⑤ return "profile": 모든 처리가 끝났으니, 이제 "profile"이라는 이름의 뷰(HTML)를 사용자에게 보여주라고 반환합니다.
컨트롤러가 열심히 준비한 데이터를 뷰(Thymeleaf 템플릿)까지 안전하게 운반하는 것이 바로 Model의 역할입니다.
Model은 Key-Value 형태로 데이터를 저장하는 단순한 상자(객체)입니다.
컨트롤러는 addAttribute() 메서드로 데이터를 담고, Thymeleaf는 이 Model에서 데이터를 꺼내 사용합니다.
컨트롤러와 뷰 사이의 데이터 다리 역할을 수행하며, 둘 사이의 의존성을 낮춰주는 중요한 역할을 합니다.
컨트롤러가 반환한 뷰 이름("profile")과 데이터가 담긴 Model은 이제 Thymeleaf 템플릿 엔진으로 전달됩니다.
Thymeleaf는 src/main/resources/templates 폴더에서 profile.html 파일을 찾아서 읽어들인 후, Model에 담겨온 데이터를 HTML 코드와 조합하여 최종적인 동적 HTML 페이지를 만듭니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>User Profile</title>
</head>
<body>
<h1><span th:text="${name}">사용자 이름</span></h1>
<p>나이: <span th:text="${age}">0</span>세</p>
</body>
</html>
th:text="${name}": Thymeleaf 문법입니다. "Model에서 name이라는 key를 가진 값을 찾아서 이 <span> 태그의 텍스트로 넣어주세요"라는 뜻입니다.
엔진이 이 파일을 처리하면, ${name}은 "홍길동"으로, ${age}는 30으로 치환됩니다.
Thymeleaf 엔진이 모든 데이터를 채워 넣어 완성한 최종 HTML 코드는 다시 Dispatcher Servlet을 거쳐 사용자의 웹 브라우저로 전송(Response)됩니다.
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
</head>
<body>
<h1><span>홍길동</span></h1>
<p>나이: <span>30</span>세</p>
</body>
</html>
타임리프를 사용하기 위해서는 컨트롤러 파일과, html 파일이 필요하다.
1. ThymeleafExController 컨트롤러
@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
@GetMapping(value="/ex01")
public String thymeleafExample01(Model model){
model.addAttribute("data", "타입리프 예제입니다.");
return "thymeleafEx/thymeleafEx01";
}
}
2. thymeleafEx/thymeleafEx01.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>타임리프 작동연습</title>
</head>
<body>
<p th:text="${data}"> Hello Thymeleaf</p>
</body>
</html>
사용자가 /thymeleaf/ex01로 요청하면, 모델을 만들어 html로 리턴하고, html에 동적인 값을 적용한 뒤 이를 반환한다.
데이터를 주고 받을 떄는 Entity 클래스 자체를 반환하는게 아닌 데이터 전달용 객체(DTO)를 사용해야 한다. DB의 설계를 외부에 노출하지 않기 위해서이며, 요청과 응답 객체가 항상 엔티티와 같지는 않기 때문이다.
th:text는 모델에 저장된 문자열을 출력할 수 있는 타임리프 문법이다.
@Getter
@Setter
public class itemDto {
private Long id;
private String itemNm;
private Integer price;
private String itemDetail;
private String sellStatCd;
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
@GetMapping(value = "/ex02")
public String thymeleafExample02(Model model){
ItemDto itemDto = new ItemDto();
itemDto.setItemDetail("상품 상세 설명");
itemDto.setItemNm("테스트 상품1");
itemDto.setPrice(10000);
itemDto.setRegTime(LocalDateTime.now());
model.addAttribute("itemDto", itemDto); //itemDto 객체를 return한다.
return "thymeleafEx/thymeleafEx02";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>상품 데이터 출력 예제</h1>
<div>
상품명 : <span th:text="${itemDto.itemNm}"></span> //객체의 itemNm 속성을 출력한다.
</div>
<div>
상품상세설명 : <span th:text="${itemDto.itemDetail}"></span>
</div>
<div>
상품등록일 : <span th:text="${itemDto.regTime}"></span>
</div>
<div>
상품가격 : <span th:text="${itemDto.price}"></span>
</div>
</body>
</html>
여러개의 데이터를 가진 컬렉션 데이터를 화면에 출력한다.
@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
List<ItemDto> itemDtoList = new ArrayList<>(); // List 객체 배열
for(int i=1;i<=10;i++){
ItemDto itemDto = new ItemDto();
itemDto.setItemDetail("상품 상세 설명"+i);
itemDto.setItemNm("테스트 상품" + i);
itemDto.setPrice(1000*i);
itemDto.setRegTime(LocalDateTime.now());
itemDtoList.add(itemDto);
}
model.addAttribute("itemDtoList", itemDtoList);
return "thymeleafEx/thymeleafEx03";
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>상품 리스트 출력 예제</h1>
<table border="1">
<thead>
<tr>
<td>순번</td>
<td>상품명</td>
<td>상품설명</td>
<td>가격</td>
<td>상품등록일</td>
</tr>
</thead>
<tbody>
<tr th:each="itemDto, status: ${itemDtoList}">
<td th:text="${status.index}"></td>
<td th:text="${itemDto.itemNm}"></td>
<td th:text="${itemDto.itemDetail}"></td>
<td th:text="${itemDto.price}"></td>
<td th:text="${itemDto.regTime}"></td>
</tr>
</tbody>
</table>
</body>
</html>
th:if는 조건문으로, 반환값이 true면 실행되고, th:unless는 반환값이 false일 경우 실행되는 코드이다.
@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
@GetMapping(value = "/ex04")
public String thymeleafExample04(Model model){
List<ItemDto> itemDtoList = new ArrayList<>();
for(int i=1;i<=10;i++){
ItemDto itemDto = new ItemDto();
itemDto.setItemDetail("상품 상세 설명"+i);
itemDto.setItemNm("테스트 상품" + i);
itemDto.setPrice(1000*i);
itemDto.setRegTime(LocalDateTime.now());
itemDtoList.add(itemDto);
}
model.addAttribute("itemDtoList", itemDtoList);
return "thymeleafEx/thymeleafEx04";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>상품 리스트 출력 예제</h1>
<table border="1">
<thead>
<tr>
<td>순번</td>
<td>상품명</td>
<td>상품설명</td>
<td>가격</td>
<td>상품등록일</td>
</tr>
</thead>
<tbody> // status.even = 짝수 순번이면 true반환
<tr th:each="itemDto, status: ${itemDtoList}">
<td th:if="${status.even}" th:text="짝수"></td> //짝수임으로 짝수를 저장
<td th:unless="${status.even}" th:text="홀수"></td> //짝수가 아님으로 홀수를 저장
<td th:text="${itemDto.itemNm}"></td>
<td th:text="${itemDto.itemDetail}"></td>
<td th:text="${itemDto.price}"></td>
<td th:text="${itemDto.regTime}"></td>
</tr>
</tbody>
</table>
</body>
</html>
th:switch, th:case는 True, false 뿐만 아닌 여러 조건을 동시에 처리할 때 사용한다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>상품 리스트 출력 예제</h1>
<table border="1">
<thead>
<tr>
<td>순번</td>
<td>상품명</td>
<td>상품설명</td>
<td>가격</td>
<td>상품등록일</td>
</tr>
</thead>
<tbody>
<tr th:each="itemDto, status: ${itemDtoList}">
<td th:switch="${status.even}"> // switch를 통해 조건을 설정한다.
<span th:case=true>짝수</span> // case를 통해 실행조건을 선택한다.
<span th:case=false>홀수</span>
</td>
<td th:text="${itemDto.itemNm}"></td>
<td th:text="${itemDto.itemDetail}"></td>
<td th:text="${itemDto.price}"></td>
<td th:text="${itemDto.regTime}"></td>
</tr>
</tbody>
</table>
</body>
</html>
th:href는 문자에 URL링크를 넣고 싶을 때 사용한다. 링크의 종류로는 Absolute URL과 Context-relative URL이 존재한다. absolute URL은 외부의 서버로 이동할 때 사용하며, 'https://' 또는 'http://'로 시작한다. Context-relative URL은 서버 내부에서 이동함으로 /로 시작하는 경로를 통해 이동할 수 있다.
@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
@GetMapping(value = "/ex05")
public String thymeleafExample05(){
return "thymeleafEx/thymeleafEx05";
}
//파라미터 전달 용도.
@GetMapping(value = "/ex06")
public String thymeleafExample06(String param1, String param2, Model model){
model.addAttribute("param1", param1); // param1이라는 문자가 저장된 인자를 "param1"로 모델네이밍
model.addAttribute("param2", param2);
return "thymeleafEx/thymeleafEx06";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Thymeleaf 링크처리 예제 페이지</h1>
<div>
<a th:href="@{/thymeleaf/ex01}">예제1 페이지 이동</a> //서버 내 이동 링크 생성
</div>
<div>
<a th:href="@{https://www.thymeleaf.org/}">thymeleaf 공식 페이지(외부) 이동</a>
</div>
<div>
//파라미터 데이터 전달.
<a th:href="@{/thymeleaf/ex06(param1 = '파라미터 데이터1', param2 = '파라미터 데이터2')}">
thymeleaf 파라미터 전달</a>
</div>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>파라미터 전달 예제</h1>
<div th:text="${param1}"></div> // 이 창에 전달된 '파라미터 데이터1' 출력
<div th:text="${param2}"></div>
</body>
</html>
html에는 3가지 영역이 있다. 헤더, 바디, 푸터, (+메뉴) 이다. 이 중 헤더와 푸터 메뉴 영역은 페이지를 이동 하더라도 똑같은 내용을 출력한다. 따라서, 페이지마다 이를 추가하지 않고 공유하며 사용해야 하며 그 기능을 지원하는 것이 Thymeleaf-layout-dialect 의존성이다.
먼저 아래처럼 gradle에 의존성(dependencies)을 직접 추가해준다.
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
fragments 패키지는 공통된 요소(헤더 등)를 보관하는 패키지다.
layout 패키지는 공통된 요소를 적용할 html 뼈대 파일이다. 예를 들어 사이트에 공통으로 적용될 배경같은 것을 보관한다.
두 패키지 모두 사용자가 직접 만들어야 하며, html파일이기에 template폴더에 이를 만들면 된다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="footer" th:fragment="footer">
푸터 영역입니다.
</div>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<div th:fragment="header"></div>
헤더 영역입니다.
</div>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> //
<head>
<meta charset="UTF-8">
<title>Title</title>
<th:block layout:fragment="script"></th:block>
<th:block layout:fragment="css"></th:block>
</head>
<body>
<div th:replace="~{fragments/header::header}"></div> // 헤더
<div layout:fragment="content" class="content"> //이 부분 내용 치환
</div>
<div th:replace="~{fragments/footer::footer}"></div>// 푸터
</body>
</html>
xmlns: "XML Namespace"의 약자이다. "지금부터 이름 공간(소속)을 선언할게"라는 의미의 명령어다. 즉, xlmlns:th 는 th:를 사용하면, html페이지에서 th: 로 시작하는 속성들은 타임리프 라이브러리를 사용한다는 의미이다. 따라서 두번째 xmlns:layout 의 의미는 앞으로 th:layout 을 지정한 URL의 라이브러리를 사용함을 의미한다.
th:replace는 해당속성이 선언된 html 태그를 다른 html파일로 치환하는 것이다. 즉, fragment/header 파일의 header 태그를 레이아웃 html로 불러온다고 생각하자.
layout:fragment="content"는 이 칸의 이름을 content로 지정한다. 다른 본문 html파일에서 content라는 이름을 통해 이 공간에 데이터를 넣는것이 가능하다. class ="content" 는 추후 css나 자바스크립트를 사용하기 위해 붙여진 이름이다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout=http://www.ultraq.net.nz/thymeleaf/layout
layout:decorate="~{layouts/layout1}">
<div layout:fragment="content">
본문 영역 입니다.
</div>
</html>
layout:decorate= 이 페이지를 다른 html파일로 옮김을 의미한다.
~{layouts/layout1}페이지를 보낼 주소이다.
layout:fragment=content 는 이 부분의 이름을 content로 설정함을 의미한다.
@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
@GetMapping(value = "/ex07")
public String thymeleafExample07(){
return "thymeleafEx/thymeleafEx07";
}
}
- 사용자가 html을 요청한다.(컨트롤러)
- 레이아웃을 불러온다.
- 레이아웃이 헤더, 푸터, 본문을 가져와 넣는다.
- 반환한다.
부트 스트랩은 웹사이트를 쉽게 만들수 있게 도와주는 프레임워크다. 부트 스트랩을 이용하면 쉽게 웹페이지를 꾸밀 수 있다. CDN(Content Delivery Network)는 물리적으로 멀리 떨어진 css, 자바 스크립트 이미지를 빨리 받을 수 있게 가까운서버에서 캐싱해둔 파일을 의미한다. 본서버가 미국에 있기에, 직접 추가하는것이 더 현명하나, CDN을 이용해 외부 URL에서 html, css, javascript 부트 스트랩 파일을 받을 수 있다.
이는 프론트 영역임으로 깊게 들어가지 않고 예제만 올리겠다.
1. 레이아웃
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link th:href="@{/css/layout1.css}" rel="stylesheet">
<!-- JS, Popper.js, and jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<th:block layout:fragment="script"></th:block>
<th:block layout:fragment="css"></th:block>
</head>
<body>
<div th:replace="~{fragments/header::header}"></div>
<div layout:fragment="content" class="content">
</div>
<div th:replace="~{fragments/footer::footer}"></div>
</body>
</html>
2. 푸터
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="footer" th:fragment="footer">
<footer class="page-footer font-small cyan darken-3">
<div class="footer-copyright text-center py-3">
2020 Shopping Mall Example WebSite
</div>
</footer>
</div>
</html>
3. 헤더
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<div th:fragment="header">
<nav class="navbar navbar-expand-sm bg-primary navbar-dark">
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarTogglerDemo03" aria-controls="navbarTogglerDemo03"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/">Shop</a>
<div class="collapse navbar-collapse" id="navbarTogglerDemo03">
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
<li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
<a class="nav-link" href="/admin/item/new">상품 등록</a>
</li>
<li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
<a class="nav-link" href="/admin/items">상품 관리</a>
</li>
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" href="/cart">장바구니</a>
</li>
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" href="/orders">구매이력</a>
</li>
<li class="nav-item" sec:authorize="isAnonymous()">
<a class="nav-link" href="/members/login">로그인</a>
</li>
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" href="/members/logout">로그아웃</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0" th:action="@{/}" method="get">
<input name="searchQuery" class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</nav>
</div>
</html>