[Thymeleaf] 입력 폼(Form) 처리

타임리프는 스프링과 통합을 위한 다양한 기능을 편리하게 제공합니다.

 

스프링 통합으로 추가되는 기능들
  • 스프링의 SpringEL 문법 통합
  • ${@myBean.doSomething()} 처럼 스프링 빈 호출 지원
  • 편리한 폼 관리를 위한 추가 속성
    • th:object (기능 강화, 폼 커맨드 객체 선택)
    • th:field , th:errors , th:errorclass
  • 폼 컴포넌트 기능
    • checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원
  • 스프링의 메시지, 국제화 기능의 편리한 통합
  • 스프링의 검증, 오류 처리 통합
  • 스프링의 변환 서비스 통합(ConversionService)

입력 폼 처리

th:object 커맨드 객체를 지정
*{...}  선택 변수 식 (th:object 에서 선택한 객체에 접근)
th:field HTML 태그의 id , name , value 속성을 자동으로 처리

 

th:field 필드 사용시 렌더링 전과 후
  • 렌더링 전

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

 

  • 렌더링 후

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

렌더링 후를 보면 HTML 태그의 id , name , value 속성을 자동으로 처리 해준 것을 볼 수 있습니다.

 


등록 폼

th:object 를 적용하려면 먼저 해당 오브젝트 정보를 넘겨주어야 합니다.

등록 폼이기 때문에 데이터가 비어있는 빈 오브젝트를 만들어서 뷰에 전달합니다.

 

Item 객체
@Data
public class Item {

    private Long id; // 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;
    }
}

 

Controller
@Controller
@RequestMapping("/form/items")
@RequiredArgsConstructor
public class FormItemController {
	private final ItemRepository itemRepository;

    @GetMapping("/add")
    public String addForm(Model model) {
        model.addAttribute("item", new Item());

        return "form/addForm";
    }
    
    @PostMapping("/add")
    public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        return "redirect:/form/items/{itemId}";
    }
}

 

addForm.html
<!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" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
        </div>
    </form>

</div>
</body>
</html>

th:object="${item}" 은 <form>에서 사용할 객체를 지정합니다.

th:filed="*{itemName}"에서 *{...}은 선택 변수 식으로 지정한 객체에 접근합니다. ( 즉, ${item.itemName}과 같다. )

또한 th:filed는 id, name, value 속성을 다음과 같이 모두 자동으로 만들어 줍니다.

  • id : th:field 에서 지정한 변수 이름과 같다. id="itemName"
  • name : th:field 에서 지정한 변수 이름과 같다. name="itemName"
  • value : th:field 에서 지정한 변수의 값을 사용. value=""


수정 폼

 

Controller에 아래 코드 추가
    @GetMapping("/{itemId}/edit")
    public String editForm(@PathVariable Long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "form/editForm";
    }

 

editForm.html
<!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="id">상품 ID</label>
            <input type="text" th:field="*{id}" class="form-control" readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" th:field="*{itemName}" class="form-control">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" th:field="*{price}" class="form-control">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" th:field="*{quantity}" class="form-control">
        </div>
    </form>

</div>
</body>
</html>

 

수정 폼을 보면  등록폼을 통해 등록된 객체의 value 값을 읽어오는 것을 볼 수 있습니다.

 


 

단일 체크 박스 추가

 

위에서 작성한 addForm과 editForm html에 아래 코드를 추가
        <!-- single checkbox -->
        <div>판매 여부</div>
        <div>
            <div class="form-check">
                <input type="checkbox" th:field="*{open}" class="form-check-input">
                <label for="open" class="form-check-label">판매 오픈</label>
            </div>
        </div>

th:field를 사용하지 않는다면 체크박스 체크를 하지 않았을 시 값이 Null로 넘어오는 것을 방지하기 위해

히든 필드를 추가해야하지만,

th:filed를 사용하면 체크 박스의 히든 필드와 관련된 부분도 함께 해결해줍니다.


멀티 체크 박스 추가

체크 박스를 멀티로 사용해, 하나 이상을 체크할 수 있도록 할 수 있습니다.

 

Controller에 아래 코드 추가
    @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이 특별하게 사용되었는데

해당 컨트롤러를 호출할 때 regions 에서 반환한 값이  해당 컨트롤러에 있는 어떤 메서드가 호출되던지간에 먼저 이 메서드가 호출되어 모델 객체가 생성되고 자동으로 모델( Model )에 담기게 됩니다.

 

위에서 작성한 addForm과 editForm 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>

 

  • ${region.key}, ${region.value}

타임리프에서 map에 대한 key, value가 예약어로써 Map타입의 데이터 처리를 지원해주기 때문에 ${region.key}, ${region.value}로 map의 key,value값에 접근할 수 있습니다.

 

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

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

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

 

  • th:filed와 th:value를 함께 사용한 이유 ?

앞에서 th:filed를 지정하면 HTML 태그의 id , name , value 속성을 자동으로 지정해준다고 했습니다.

하지만 멀티 체크박스가 사용되는 경우에는 th:value를 따로 지정해주어야 합니다.

그 이유는 단일 값이 아닌 여러 값이 있기 때문에 th:field="*{regions}"의 값과 th:value="${region.key}"의 값이 일치하는지 확인합니다.

그래서 일치하면 체크박스를 체크처리 해주게 됩니다.


라디오 버튼 추가

라디오 버튼은 여러 선택지 중하나만 선택할 때 사용할 수 있습니다.

 

ItemType Enum
public enum ItemType {
    BOOK("도서"),
    FOOD("음식"),
    ETC("기타");

    private final String description;

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

    public String getDescription() {
        return description;
    }
}

 

Controller에 아래 코드 추가
    @ModelAttribute("itemTypes")
    public ItemType[] itemTypes() { // enum은 values를 반환하면 value 값들을 배열로 넘겨준다.
        return ItemType.values();
    }

여기서도 @ModelAttribute를 특별하게 사용했는데, enum을 values로 반환해 enum의 value 값들을 배열로 Model 넘겨줬습니다.

 

위에서 작성한 addForm과 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"></label>
            </div>
        </div>

 

  • ${type.name()}

 .nameenum의 name을 그대로 가져올 수 있습니다. (여기서는 BOOK, FOOD, ETC)

 

  • th:for="${#ids.prev('itemType')}"

멀티 체크박스 처럼 여러 선택지가 있기 때문에 필수로 들어가야합니다.

 

 

 


셀렉트 박스 추가

여러 목록 리스트 중에 하나를 선택하는 셀렉트 박스 입니다.

 

 

Controller에 아래 코드 추가
    @ModelAttribute("deliveryCodes")
    public Map<String, String> deliveryCodes() {
        Map<String, String> deliveryCodes = new LinkedHashMap<>();
        deliveryCodes.put("FAST", "빠른 배송");
        deliveryCodes.put("NORMAL", "일반 배송");
        deliveryCodes.put("SLOW", "느린 배송");

        return deliveryCodes;
    }

 

위에서 작성한 addForm과 editForm html에 아래 코드를 추가
        <!-- SELECT -->
        <div>
            <div>배송 방식</div>
            <select th:field="*{deliveryCode}" class="form-select">
                <option value="">==배송 방식 선택==</option>
                <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.key}"
                        th:text="${deliveryCode.value}"></option>
            </select>
        </div>

 

  • <option value="">==배송 방식 선택==</option>

아무 것도 선택하지 않을 때 나오는 기본 문구 입니다. 

 

  • <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.key}" th:text="${deliveryCode.value}"></option>

FAST, NORMAL, SLOW 여러 옵션이 있기 때문에 each 반복문을 통해, deliveryCode.key와 일치하는 deliveryCode.value를 추가합니다.