공부/Spring

타임리프 - 스프링 통합과 폼

Stair 2025. 3. 29. 10:24
반응형

타임리프 스프링 통합

타임리프는 크게 2가지 메뉴얼을 제공한다.

 

기본매뉴얼 : https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

스프링 통합 매뉴얼 : https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

 

타임리프는 스프링 없이도 동작하지만, 스프링과 통합을 위한 다양한 기능을 편리하게 제공한다. 그리고 이런 부분은 스프링으로 백엔드를 개발하는 개발자 입장에서 타임리프를 선택하는 하나의 이유가 된다.

 

 

스프링 통합으로 추가되는 기능들

- 스프링의 SpringEL 문법 통합

- ${@myBean.doSomething()}처럼 스프링 빈 호출 지원

- 편리한 폼 관리를 위한 추가 속성

    - th:object(기능 강화, 폼 커맨드 객체 선택)

    - th:field, th:errors, th:errorclass

- 폼 컴포넌트 기능

    - checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원

- 스프링의 메시지, 국체화 기능의 편리한 통합

- 스프링의 검증, 오류 처리 통합

- 스프링의 변환 서비스 통합

 

 

설정 방법

스프링 부트는 설정 방법을 모두 자동화 해준다. build.gradle에 다음 한줄을 넣어주면 Gradle은 타임리프와 관련된 라이브러리를 다운받고, 스프링 부트는 앞서 설명한 타임리프와 관련된 설정용 스프링 빈을 자동으로 등록해준다.

 

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

 

 

 

 

입력 폼 처리

지금부터 타임리프가 제공하는 입력 폼 기능을 적용해서 기존 프로젝트의 폼 코드를 타임리프가 지원하는 기능을 사용하여 효율적으로 개선해보자.

 

- th:object : 커맨드 객체를 지정한다.

- *{...} : 선택 변수 식이라고 한다. th:object에서 선택한 객체에 접근한다.

- th:field

    - HTML 태그의 id, name, value 속성을 자동으로 처리해준다.

 

 

렌더링 전 

<input type="text" th:field="*{itemName}" />

렌더링 후

<input type="text" id="itemName" name="itemName" th:value="*{itemName}" />

 

 

등록 폼

th:object를 적용하려면 먼저 해당 오브젝트 정보를 넘겨주어야 한다. 등록 폼이기 때문에 데이터가 비어있는 빈 오브젝트를 만들어서 뷰에 전달한다.

@GetMapping("/add")
public String addForm(Model model) {
    model.addAttribute("item", new Item()); //새롭게 추가된 부분
    return "form/addForm";
}

 

타임리프 등록 폼을 변경하자.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }
    </style>
</head>
<body>

<div class="container">

    <div class="py-5 text-center">
        <h2>상품 등록 폼</h2>
    </div>

    <form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
        </div>

        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">상품 등록</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/form/items}'|"
                        type="button">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

 

- th:object="$[item}" : <form>에서 사용할 객체를 지정한다. 선택 변수 식(*{...})을 적용할 수 있다.

- th:field="*{itemName}"

    - *{itemName}은 선택 변수 식을 사용하였다. ${item.itemName}과 같다. 위 코드에서 th:object로 item을 선택했기 때문에 선택 변수 식을 적용할 수 있다.

    - th:field는 id, name, value 속성을 모두 자동으로 만들어준다.

        - id : th:field에서 지정한 변수 이름과 같다. id="itemName"

        - name : th:field에서 지정한 변수 이름과 같다. name="itemName"

        - value : th:field에서 지정한 변수의 값을 사용한다. value=" "

 

 

*참고 : id 속성을 제거하여도, th:field가 자동으로 만들어주긴 한다.

 

렌더링 된 결과를 확인해보자.

<input type="text" id="itemName" class="form-control" placeholder="이름을 입력하세요" name="itemName" value="">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" class="form-control" placeholder="가격을 입력하세요" name="price" value="">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" class="form-control" placeholder="수량을 입력하세요" name="quantity" value="">

 

등록 폼에서 이걸 왜 쓰지 싶지만, 객체를 하나 생성하는 리소스는 그렇게 크지 않다.

또한 위 처럼 th:object를 사용하게 되면 ide에서 HTML에 나는 오타 등을 잡아줄 수 있기 때문에 사용을 권장한다.

 

 

 

수정 폼

등록 폼에서는 드라마틱한 결과를 확인할 수 없었지만 수정폼에서는 조금 더 확인할 수 있다.

현재 수정폼에서는 이미 item의 값을 넘기고 있다.

@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
    Item item = itemRepository.findById(itemId);
    model.addAttribute("item", item);
    return "form/editForm";
}

그렇기 때문에 HTML만 변경해주면 된다.

editForm을 수정해보자.

<form action="item.html" th:action th:object="${item}" method="post">
    <div>
        <label for="id">상품 ID</label>
        <input type="text" id="id" class="form-control" th:field="*{id}" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" class="form-control" th:field="*{itemName}">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" class="form-control" th:field="*{price}">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" class="form-control" th:field="*{quantity}">
    </div>

 

앞서 확인한 것 처럼, id, name, value를 모두 신경써야 했는데, 많은 부분이 th:field 덕분에 자동으로 처리되는 것을 확인할 수 있다.

렌더링 된 결과를 확인해보자.

<div>
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" name="itemId" class="form-control" value="1" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" value="itemA" readonly>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control" value="10000" readonly>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" value="10" readonly>
</div>

 

 

정리

th:object, th:field 덕분에 폼을 개발할 때 약간의 편리함을 얻었다.

이것은 사실 Validation을 할 때 유용하게 사용된다. 이후 Validation에서 더 자세하게 알아보자.

 

 

 

요구사항 추가

타임리프를 사용해서 폼에서 체크박스, 라디오 버튼, 셀렉트 박스를 편리하게 사용하는 방법을 학습해보자.

기존 상품 서비스에 다음 요구사항이 추가되었다.

 

- 판매 여부

    - 판매 오픈 여부

    - 체크 박스로 선택할 수 있다.

- 등록 지역

    - 서울, 부산, 제주

    - 체크 박스로 다중 선택할 수 있다.

- 상품 종류

    - 도서, 식품, 기타

    - 라디오 버튼으로 하나만 선택할 수 있다.

- 배송 방식

    - 빠른 배송

    - 일반 배송

    - 느린 배송

    - 셀렉트 박스로 하나만 선택할 수 있다.

 

패키지는 다음과 같다.

 

우선 상품 종류를 Enum타입으로 만들어보자.

public enum ItemType {

    BOOK("도서"), FOOD("음식"), ETC("기타");

    private final String description;

    ItemType(String description) {
        this.description = description;
    }
}

 

일반적인 enum 형태는 설명이 없다.

하지만 위의 enum 형태는 설명을 위해 description 필드를 추가했다.

이렇게 되면, ItemType.BOOK.getDescription()을 호출하면, "도서가"가 반환되게 된다.

 

다음은 배송 방식인 DeliveryCode 클래스를 만들자.

/**
 * FAST : 빠른 배송
 * NORMAL : 일반 배송
 * SLOW : 느린 배송
 */
@Data
@AllArgsConstructor //모든 필드를 포함하는 생성자 생성
//@RequiredArgsConstructor //final 또는 @NonNull이 붙은 필드만 포함하는 생성자 생성
public class DeliveryCode {

    private String code;
    private String displayName;

}

 

배송 방식은 DeliveryCode라는 클래스를 사용한다. code는 FAST같은 시스템에서 전달하는 값이고, displayName은 빠른 배송 같은 고객에게 보여주는 값이다.

 

*AllArgsConstructor : AllArgsConstructor는 RequiredArgsConstructor와 다르게 모든 필드를 포함하는 생성자를 생성한다. 따라서 final이 붙어있지 않아도 생성자가 만들어진다.

 

 

다음은 기존에 있던 Item에 값을 더 추가해보자.

@Data
public class Item {

    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    private Boolean open; //판매 여부
    private List<String> regions; //등록 지역
    private ItemType itemType;
    private String deliveryCode;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

ENUM, 클래스, String 같은 다양한 상황을 준비했다. 각각 상황에 어떻게 폼의 데이터를 받을 수 있는지 알아보자.

 

 

 

체크 박스 - 단일1

체크박스를 단순 HTML을 사용하여 만들어보자.

<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" name="open" class="form-check-input">
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

 

상품이 등록되는 곳에 다음과 같이 로그를 남겨서 값이 잘 넘어오는지 확인해보자. @Slf4j를 사용하여 로그도 찍어보자.

@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {

    log.info("item.open={}", item.getOpen());

    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/form/items/{itemId}";
}

 

체크박스를 체크하고 상품을 등록하면 Form Data에 Open이 on이라는 값으로 넘어가는 것을 볼 수 있다.

 

로그를 확인해보면, 스프링에선 on이라는 문자를 true 타입으로 변환해준다. (스프링 타입 컨버터가 이 기능을 수행한다)

2025-03-28 11:11:31.635  INFO 14572 --- [nio-8080-exec-6] h.i.web.form.FormItemController          : item.open=true

 

 

주의 - 체크 박스를 선택하지 않을 때

HTML에서 만약 체크박스를 선택하지 않고 폼을 전송하면 open이라는 필즈 자체가 서버로 전송되지 않는다.

 

스프링을 확인해보면, open 값에 null이 들어가는 것을 확인할 수 있다.

2025-03-28 11:14:06.099  INFO 14572 --- [nio-8080-exec-5] h.i.web.form.FormItemController          : item.open=null

 

HTTP 메시지 바디를 보면 open의 이름도 전송이 되지 않는다.

 

** HTML checkbox는 선택이 안되면 클라이언트에서 서버로 값 자체를 보내지 않는다. 수정의 경우에는 상황에 따라서 이 방식이 문제가 될 수 있다. 사용자가 의도적으로 체크되어 있던 값을 체크를 해제해도 저장시 아무 값도 넘어가지 않기 때문에, 서버 구현에 따라서 값이 오지 않은 것으로 판단하여 값을 변경하지 않을수도 있다.

 

--> 이런 문제를 해결하기 위해서 스프링 MVC는 약간의 트릭을 사용하는데, 히든 필드를 하나 만들어서, _open처럼 기존 체크박스 이름 앞에 언더스코어(_)를 붙혀 전송하면 체크를 해제했다고 인식할 수 있도록 한다. 히든 필드는 항상 전송된다. 따라서 체크를 해제한 경우 여기에서 open은 전송되지 않고, _open만 전송되는데, 이 경우 스프링 MVC는 체크를 해제했다고 판단한다.

 

hidden 필드를 추가하고 실행해보자.

<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" name="open" class="form-check-input">
        <input type="hidden" name="_open" value="on"/> <!-- 히든 필드 추가 -->
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

 

 

체크박스를 선택한다면 당연히 open이 On으로

2025-03-28 11:20:49.326  INFO 21272 --- [nio-8080-exec-6] h.i.web.form.FormItemController          : item.open=true

item.open이 true로 넘어가지고

 

체크박스를 선택하지 않는다면

_open만 on인 상태로

2025-03-28 11:21:20.966  INFO 21272 --- [nio-8080-exec-7] h.i.web.form.FormItemController          : item.open=false

item.open이 false로 넘어가진다.

 

 

체크 박스를 체크하면 스프링 MVC가 open에 값이 있는 것을 확인하고 사용하며 _open을 무시한다.

 

체크박스를 체크하지 않으면 스프링MVC가 _open만 있는 것을 확인하고, open의 값이 체크되지 않았다고 인식한다.

이 경우 서버에서 Boolean타입을 찍어보면 false인 것을 확인할 수 있다.

 

 

 

체크 박스 - 단일2

개발할때마다 이렇게 히든 필드를 추가하는 것은 상당히 번거로운 일이다. 타임리프가 제공하는 폼 기능을 사용하면 이런 부분을 자동으로 처리할 수 있다.

 

체크 박스 코드를 addForm.html에 추가해보자. 기존 체크박스 코드를 제거하고 추가하도록 한다.

        <!-- single checkbox -->
        <div>판매 여부</div>
        <div>
            <div class="form-check">
<!--                <input type="checkbox" id="open" name="open" th:field="${item.open}" class="form-check-input">-->
                <input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
                <label for="open" class="form-check-label">판매 오픈</label>
            </div>
        </div>

addForm은 th:object로 item이 선언되어 있기 때문에 ${item.open} 대신 *{open}을 사용해도 된다.

 

2025-03-28 12:30:29.950  INFO 17728 --- [nio-8080-exec-9] h.i.web.form.FormItemController          : item.open=true

 

- <input type="hidden" name="_open" value="on" />

타임리프를 사용하면 체크 박스의 히든 필드와 관련된 부분도 함께 해결해준다. HTML 생성 결과를 보면 히든 필드 부분이 자동으로 생성되어 있다.

 

item.html에도 적용해보자.

<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" th:field="${item.open}" class="form-check-input" checked disabled>
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

 

주의 : item.html에는 th:object를 사용하지 않았기 때문에, th:field 부분에 ${item.open}으로 적어주어야 한다. disabled를 사용해서 상품 상세에서는 체크 박스가 선택되지 않도록 했다.

 

+ disabled를 사용해서 상품 상세 페이지에서는 체크박스가 선택되지 않도록 했다.

+ 타임리프의 체크 확인 checked="checked" : 체크박스에서 판매 여부를 선택해서 저장하면, 조회시에 checked 속성이 추가된 것을 확인할 수 있다. 이런 부분을 개발자가 직접 처리하려면 상당히 번거롭다. 타임리프의 th:field를 사용하면, 값이 true인 경우 체크를 자동으로 처리해준다.

 

 

editForm.html에도 적용하자.

<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

 

 

기능을 사용하다 보면 체크박스를 수정해도 반영이 되지 않는걸 확인할 수 있는데 update에서 실질적인 item에 대한 open을 update 치지 않아서 반영이 되지 않는것을 볼 수 있다.

ItemRepository의 update()메서드를 다음과 같이 수정하자.

public void update(Long itemId, Item updateParam) {
    Item findItem = findById(itemId);
    findItem.setItemName(updateParam.getItemName());
    findItem.setPrice(updateParam.getPrice());
    findItem.setQuantity(updateParam.getQuantity());
    findItem.setOpen(updateParam.getOpen());
    findItem.setRegions(updateParam.getRegions());
    findItem.setItemType(updateParam.getItemType());
    findItem.setDeliveryCode(updateParam.getDeliveryCode());
}

open 의외에 나머지 필드도 업데이트 되도록 미리 넣어주자.

이제 정상 동작하는것을 확인할 수 있다.

 

 

 

체크 박스 - 멀티

체크 박스를 멀티로 사용해서, 하나 이상을 체크할 수 있도록 해보자.

- 등록 지역

    - 서울, 부산, 제주

    - 체크 박스로 다중 선택할 수 있다.

 

FormItemController에 regions를 추가해보자.

@ModelAttribute("regions")
public Map<String, String> regions() {
    Map<String, String> regions = new LinkedHashMap<>(); //순서 보장
    regions.put("SEOUL", "서울");
    regions.put("BUSAN", "부산");
    regions.put("JEJU", "제주");

    return regions;
}

 

이 ModelAttribute는 기존 ModelAttribute와 좀 다르다.

등록 폼, 상세화면, 수정 폼에서 모두 서울, 부산 제주라는 체크박스를 반복해서 보여주어야 한다. 이렇게 하려면 각각의 컨트롤러에서 model.addAttribute를 사용하여 체크 박스를 구성하는 데이터를 반복해서 넣어주어야 한다.

@ModelAttribute는 이렇게 컨트롤러에 있는 별도의 메서드에 적용할 수 있다.

이렇게 하면 해당 컨트롤러를 요청할 때 regions에서 반환한 값이 자동으로 모델에 담기게 된다.

물론 이렇게 사용하지 않고, 각각의 컨트롤러 메서드에서 모델에 직접 데이터를 담아서 처리해도 된다.

 

addForm.html에 추가를 해보자.

<!-- multi checkbox -->
<div>
    <div>등록 지역</div>
    <div th:each="region : ${regions}" class="form-check form-check-inline">
        <input type="checkbox" th:field="*{regions}" th:value="${region.key}"
               class="form-check-input">
        <label th:for="${#ids.prev('regions')}"
               th:text="${region.value}" class="form-check-label">서울</label>
    </div>
</div>

- th:for="${#ids.prev('regions')}"

멀티 체크 박스는 같은 이름의 여러 체크박스를 만들 수 있다. 그런데 문제는 이렇게 반복해서 HTML 태그를 생성할 떄 생성된 HTML 태그 속성에서 name은 같아도 되지만 id는 모두 달라야 한다.

따라서 타임리프는 체크박스를 each 루프 안에서 반복해서 만들 떄 임의로 1,2,3 숫자를 뒤에 붙혀준다.

 

소스 코드를 보면 알 수 있다.

<div>
<div>등록 지역</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="SEOUL"
class="form-check-input" disabled id="regions1" name="regions">
<label for="regions1"
class="form-check-label">서울</label>
</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="BUSAN"
class="form-check-input" disabled id="regions2" name="regions">
<label for="regions2"
class="form-check-label">부산</label>
</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="JEJU"
class="form-check-input" disabled id="regions3" name="regions">
<label for="regions3"
class="form-check-label">제주</label>
</div>
</div>

 

HTML의 id가 타임리프에 의해 동적으로 만들어지기 때문에 <label for="id 값">으로 label의 대상이 되는 id 값을 임의로 지정하는 것은 곤란하다. 타임리프는 ids.prev(), ids.next()를 제공하여 동적으로 생성되는 id값을 사용할 수 있도록 한다.

 

실행 시키고 로그를 확인해보면 다음과 같다.

2025-03-28 13:32:41.786  INFO 19688 --- [nio-8080-exec-4] h.i.web.form.FormItemController          : item.regions=[SEOUL, BUSAN]

 

_regions는 앞서 봤던 기능과 동일하다. 웹 브라우저에서 체크를 하나도 하지 않았을 때, 클라이언트가 서버에 아무런 데이터를 보내지 않는 것을 방지한다. 참고로 _regions조차 보내지 않으면 결과는 null이 된다.

_regions가 체크박스 숫자만큼 생성될 필요는 없지만, 타임리프가 생성되는 옵션 수 만큼 생성해서 그런 것이니 무시하도록 하자.

 

 

item.html에도 추가를 해주도록 한다.

<!-- multi checkbox -->
<div>
    <div>등록 지역</div>
    <div th:each="region : ${regions}" class="form-check form-check-inline">
        <input type="checkbox" th:field="${item.regions}" th:value="${region.key}"
               class="form-check-input" disabled>
        <label th:for="${#ids.prev('regions')}"
               th:text="${region.value}" class="form-check-label">서울</label>
    </div>
</div>

**주의 : item.html에는 th:object를 사용하지 않았기 때문에 th:field 부분에 ${item.regions}로 적어주어야 한다.

또한 disabled를 사용해서 상품 상세에서는 체크 박스가 선택되지 않도록 했다.

 

타임리프의 체크를 확인해보자.

서울과 부산의 체크박스를 체크했더니 다음과 같은 결과가 나타났다.

<div class="form-check form-check-inline">
<input type="checkbox" value="SEOUL"
class="form-check-input" disabled id="regions1" name="regions" checked="checked">
<label for="regions1"
class="form-check-label">서울</label>
</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="BUSAN"
class="form-check-input" disabled id="regions2" name="regions" checked="checked">
<label for="regions2"
class="form-check-label">부산</label>

 

멀티 체크박스에서 등록 지역을 선택해서 저장하면, 조회시에 checked 속성이 추가된 것을 확인할 수 있다. 타임리프는 th:field에 지정한 값과 th:value의 값을 비교해서 체크를 자동으로 처리해준다.

 

마지막으로 editForm.html에도 체크박스를 추가해주도록 하자.

<!-- multi checkbox -->
<div>
    <div>등록 지역</div>
    <div th:each="region : ${regions}" class="form-check form-check-inline">
        <input type="checkbox" th:field="${item.regions}" th:value="${region.key}"
               class="form-check-input" >
        <label th:for="${#ids.prev('regions')}"
               th:text="${region.value}" class="form-check-label">서울</label>
    </div>
</div>

 

 

 

라디오 버튼

라디오 버튼은 여러 선택지중에 하나를 선택할 때 사용할 수 있다. 이번 시간에는 라디오 버튼을 자바 ENUM을 활용해서 개발해보자.

 

- 상품 종류

    - 도서, 식품 기타.

    - 라디오 버튼으로 하나만 선택할 수 있다.

@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
    ItemType[] values = ItemType.values();
    return values;
}

 

itemTypes를 등록 폼, 조회, 수정 폼에서 모두 사용하므로 @ModelAttribute의 특별환 사용법을 적용하자.

ItemType.values()를 사용하면 해당 ENUM의 모든 정보를 배열로 반환한다(ENUM)의 특징이다.
ex)BOOK, FOOD, ETC

 

 

상품 등록 폼에 기능을 추가해보자.

<!-- radio button -->
<div>
    <div>상품 종류</div>
    <div th:each="type : ${itemTypes}" class="form-check form-check-inline">
        <input type="radio" th:field="*{itemType}" th:value="${type.name()}"
               class="form-check-input">
        <label th:for="${#ids.prev('itemType')}" th:text="${type.description}"
               class="form-check-label">
            BOOK
        </label>
    </div>
</div>

 

로그를 찍어보면 다음과 같다.

2025-03-29 09:17:47.836  INFO 14684 --- [nio-8080-exec4] h.i.web.form.FormItemController          : item.itemType=BOOK

 

**체크 박스는 수정시 체크를 해제하면 아무 값도 넘어가지 않기에 히든필드를 사용하였지만, 라디오 버튼은 이미 선택이 되어있으면, 수정시에도 항상 하나를 선택하도록 만들어져 있기 때문에 체크박스와 달리 별도의 히든 필드를 사용할 필요가 없다.

 

상품 상세와 수정에도 라디오 버튼을 넣어주자.

 

item.html

<!-- radio button -->
<div>
    <div>상품 종류</div>
    <div th:each="type : ${itemTypes}" class="form-check form-check-inline">
        <input type="radio" th:field="${item.itemType}" th:value="${type.name()}"
               class="form-check-input" disabled>
        <label th:for="${#ids.prev('itemType')}" th:text="${type.description}"
               class="form-check-label">
            BOOK
        </label>
    </div>
</div>

 

item.html에는 th:object를 사용하지 않기 때문에, th:field 부분에 ${item.itemType}으로 적어주어야 한다.

+ disabled를 사용해서 상품 상세에서는 라디오 버튼이 선택되지 않도록 했다.

 

 

editForm.html에도 추가해주도록 하자.

<!-- radio button -->
<div>
    <div>상품 종류</div>
    <div th:each="type : ${itemTypes}" class="form-check form-check-inline">
        <input type="radio" th:field="*{itemType}" th:value="${type.name()}"
               class="form-check-input">
        <label th:for="${#ids.prev('itemType')}" th:text="${type.description}"
               class="form-check-label">
            BOOK
        </label>
    </div>
</div>

 

 

타임리프를 확인해보자.

<!-- radio button -->
<div>
<div>상품 종류</div>
<div class="form-check form-check-inline">
<input type="radio" value="BOOK"
class="form-check-input" id="itemType1" name="itemType">
<label for="itemType1" class="form-check-label">도서</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" value="FOOD"
class="form-check-input" id="itemType2" name="itemType">
<label for="itemType2" class="form-check-label">음식</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" value="ETC"
class="form-check-input" id="itemType3" name="itemType">
<label for="itemType3" class="form-check-label">기타</label>
</div>
</div>

 

 

추가 : 타임리프에서 ENUM을 직접 사용할수도 있다.

<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">

 

--> 이렇게 사용하면 ENUM의 패키지 위치가 변경되거나 할 때 자바 컴파일러가 타임리프까지 컴파일 오류를 잡을 수 없으므로 추천하지 않는다.

 

 

 

셀렉트 박스

셀렉트 박스는 여러 선택지 중에 하나를 선택할 수 있다. 이번 시간에는 셀렉트 박스를 자바 객체를 활용해서 개발해보자.

 

- 배송 방식

    - 빠른 배송

    - 일반 배송

    - 느린 배송

    - 셀렉트 박스로 하나만 선택할 수 있다.

@ModelAttribute("deliveryCodes")
public List<DeliveryCode> deliveryCodes() {
    List<DeliveryCode> deliveryCodes = new ArrayList<>();
    deliveryCodes.add(new DeliveryCode("FAST", "빠른 배송"));
    deliveryCodes.add(new DeliveryCode("NORMAL", "일반 배송"));
    deliveryCodes.add(new DeliveryCode("SLOW", "느린 배송"));
    return deliveryCodes;
}

 

DeliveryCode라는 자바 객체를 사용하는 방법으로 진행하였다.

DeliveryCode를 등록 폼, 조회, 수정 폼에서 모두 사용하기 때문에 @ModelAttribute의 특별한 사용법을 적용하자.

 

참고 : @ModelAttribute가 있는 deliveryCodes()메서드는 컨트롤러가 호출될 때마다 사용되므로 deliveryCodes 객체도 계속 생성된다. 이런 부분은 미리 생성해두고 재사용하는 것이 더 효율적이다.

 

addFrom.html 추가

<!-- SELECT -->
<div>
    <div>배송 방식</div>
    <select th:field="*{deliveryCode}" class="form-select">
        <option value="">==배송 방식 선택==</option>
        <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}"
                th:text="${deliveryCode.displayName}">FAST</option>
    </select>
</div>
<hr class="my-4">

 

editForm.html에 추가

<!-- SELECT -->
<div>
    <div>배송 방식</div>
    <select th:field="*{deliveryCode}" class="form-select">
        <option value="">==배송 방식 선택==</option>
        <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}"
                th:text="${deliveryCode.displayName}">FAST</option>
    </select>
</div>
<hr class="my-4">

 

item.html에 추가

<!-- SELECT -->
<div>
    <div>배송 방식</div>
    <select th:field="${item.deliveryCode}" class="form-select" disabled>
        <option value="">==배송 방식 선택==</option>
        <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}"
                th:text="${deliveryCode.displayName}">FAST</option>
    </select>
</div>
<hr class="my-4">

주의 : item.html에는 th:object를 사용하지 않기 때문에 th:field 부분에 ${item.deliveryCode}로 적어주어야 한다.

disabled를 사용해서 상품 상세에서는 셀렉트 박스가 선택되지 않도록 했다.

 

 

 

 

반응형

'공부 > Spring' 카테고리의 다른 글

검증1 - Validation  (0) 2025.04.01
메시지, 국제화  (0) 2025.03.30
타임리프 - 기본 기능  (0) 2025.03.27
스프링 MVC 웹 페이지 만들기  (0) 2025.03.25
스프링 MVC - 기본 기능  (0) 2025.03.20