Spring

[Spring] MVC 20편 - 웹 페이지 만들기 (2)

dev-nadan 2025. 8. 18. 20:48

 

지난 편에서는 프로젝트 기본 세팅과 상품 도메인, 저장소까지 만들었다.

이번에는 본격적으로 타임리프 뷰 템플릿을 사용해서 웹 페이지를 동적으로 구현해보자.


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)이라고 부르는데, 덕분에 프론트엔드 퍼블리셔와 협업하기도 훨씬 수월하겠다는 생각이 들었다.