지난 편에서는 프로젝트 기본 세팅과 상품 도메인, 저장소까지 만들었다.
이번에는 본격적으로 타임리프 뷰 템플릿을 사용해서 웹 페이지를 동적으로 구현해보자.
1. 상품 목록 - 타임리프 적용
컨트롤러
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "basic/items";
}
/**
* 테스트용 데이터 추가
*/
@PostConstruct
public void init() {
itemRepository.save(new Item("testA", 10000, 10));
itemRepository.save(new Item("testB", 20000, 20));
}
}
- @RequiredArgsConstructor: final이 붙은 멤버 변수에 대해 생성자를 자동 생성한다.
- @PostConstruct: 컨트롤러가 초기화될 때 테스트 데이터를 자동으로 추가한다.
뷰 템플릿 (items.html)
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center"><h2>상품 목록</h2></div>
<div class="row">
<div class="col">
<button class="btn btn-primary float-end"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품 등록</button>
</div>
</div>
<hr class="my-4">
<table class="table">
<thead>
<tr>
<th>ID</th><th>상품명</th><th>가격</th><th>수량</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td><a th:href="@{/basic/items/{id}(id=${item.id})}" th:text="${item.id}">1</a></td>
<td><a th:href="@{/basic/items/{id}(id=${item.id})}" th:text="${item.itemName}">상품명</a></td>
<td th:text="${item.price}">10000</td>
<td th:text="${item.quantity}">10</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
타임리프 핵심 문법
- th:each : 반복문 처리 (items 리스트를 순회)
- th:text : 내용 변경 (<td> 안의 값을 동적으로 치환)
- th:href, th:onclick : 동적 URL 바인딩
- |...| : 리터럴 치환 문법 (문자열 + 변수 합치기)
2. 상품 상세
컨트롤러
@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
뷰 템플릿 (item.html)
<div>
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" class="form-control" th:value="${item.id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" class="form-control" th:value="${item.itemName}" readonly>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" class="form-control" th:value="${item.price}" readonly>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" class="form-control" th:value="${item.quantity}" readonly>
</div>
- th:value: 입력폼의 value 속성에 동적으로 값을 채움
- 수정 버튼은 th:onclick을 사용해 edit 화면으로 이동 가능
3. 상품 등록
컨트롤러
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer quantity,
Model model) {
Item item = new Item(itemName, price, quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
- 처음에는 @RequestParam을 통해 파라미터를 하나하나 받는다.
- 하지만 점차 @ModelAttribute → 생략 버전까지 발전시켜 편리하게 개선할 수 있다.
뷰 템플릿 (addForm.html)
<form th:action method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control">
</div>
<button class="btn btn-primary" type="submit">상품 등록</button>
</form>
- th:action: 현재 컨트롤러의 URL에 맞게 action 속성 자동 설정
- 하나의 URL(/basic/items/add)에서 GET은 폼을 열고, POST는 등록 처리
4. 회고
이번 단계에서는 정적 HTML을 타임리프 템플릿으로 전환하면서, 실제 웹 화면과 컨트롤러를 연결해봤다.
특히 @RequestParam, @ModelAttribute, th:* 속성들을 사용해 데이터 입력 → 컨트롤러 → 뷰 렌더링 흐름이 자연스럽게 이어진다는 걸 확인할 수 있었다.
과거에 JSP로만 작업했을 때는 프론트와 서버 코드가 뒤엉켜서 보기 힘들었는데, 타임리프는 순수 HTML을 유지하면서도 서버 렌더링이 가능하다는 점이 큰 장점이었다. 이걸 네이티브 템플릿(Natural Templates)이라고 부르는데, 덕분에 프론트엔드 퍼블리셔와 협업하기도 훨씬 수월하겠다는 생각이 들었다.
'Spring' 카테고리의 다른 글
[Spring] MVC 22편 - 웹 페이지 만들기 (4, 최종 정리) (0) | 2025.08.18 |
---|---|
[Spring] MVC 21편 - 웹 페이지 만들기 (3) (1) | 2025.08.18 |
[Spring] MVC 19편 - 웹 페이지 만들기 (1) (1) | 2025.08.18 |
[Spring] MVC 18편 - HTTP 응답과 메시지 컨버터 (0) | 2025.08.18 |
[Spring] MVC 17편 - Spring MVC 어노테이션 동작 요약 시트 (3) | 2025.08.14 |