Thymeleaf Form

타임리프를 이용하게 form 태그에 대한 처리를 편리하게 수행할 수 있다.

${@myBean.doSomething()} 와 같은 Spring Bean에 대한 직접 참조, th:object, th:field, 등과 같은 폼 관련 객체를 사용할 수 있다.

입력 폼 처리

<form action="item.html" th:action th:object="${item}" method="post">
  <div>
<input type="text" id="itemName" th:field="*{itemName}" class="formcontrol"
placeholder="이름을 입력하세요">
  </div>
</form>

다음과 같이 form 태그에 th:object을 명시해서 해당 태그에서 사용되는 객체를 지정한다. 그 후, 하위에 input태그에서는 해당 객체의 필드 형태로 th:field에 지정한다. 그렇게 하면 id, name, value 속성에 대해서 자동적으로 랜더링 된다.

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

*{itemName} 은 ${item.itemName} 과 동일한 의미를 보인다. 단, *{}을 사용하기 위해서는 th:object 명시를 해줘야 한다.

이와 같이 객체의 필드값에 대해 자동적으로 id,name,value 속성을 만들어 주기 때문에 간편하게 form 태그를 만들 수 있다.

이러한 효과는 수정 폼에 적용했을 때, 극대화 할 수 있다. 왜냐하면,등록 폼에서는 저장된 객체의 값이 없기 때문에 value 값이 없지만, 수정 폼의 경우 저장된 객체의 값을 해당 필드에 맞게 초기화 해주기 때문에 간편하다.

Domain

체크박스, 라디오 박스, 셀렉트 박스 예제를 위한 도메인을 추가한다.

상품 정보

public enum ItemType {

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

    private final String description;

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

    public String getDescription() {
        return description;
    }
}

배송 정보

@Data
public class DeliveryCode {
    private String code;
    private String displayName;

    public DeliveryCode(String code, String displayName) {
        this.code = code;
        this.displayName = displayName;
    }
}

Item Domain

@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;
    }
}

체크 박스

CheckBox

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

Form Controller

@PostMapping("/add")
public String addItem(Item item, RedirectAttributes   redirectAttributes) {
  log.info("item.open={}", item.getOpen());
...
}

체크박스를 선택했을 때 넘어오는 것을 결과를 확인해보면 아래와 같다.

Results

FormItemController : item.open=true //체크 박스를 선택하는 경우
FormItemController : item.open=null //체크 박스를 선택하지 않는 경우

체크 박스를 선택한 경우 정상적으로 true 값을 전달하는 것을 확인 할 수 있지만, 선택하지 않은 경우 false가 아닌 null을 전달한다(즉 아무런 값을 전달하지 않는다.)

그런데 이런 방식의 경우 수정 폼에서 문제를 발생시킬 수 있다. 가령, 사용자가 기존에 체크 되어 있던 것을 체크 해제 하게 되는 경우 서버로는 null이 전달된다. 이렇게 되면 서버 구현 방식에 따라 변경되지 않은 것으로 간주하여 체크 상태에서 변경되지 않을 수 있는 문제가 발생한다.

이를 해결하기 위해 스프링에서는 아래의 트릭을 활용한다.

<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>

위와 같이 hidden field을 이용해서 _open 변수를 전달하게 된다.

이렇게 되면 체크를 한경우에는 open 과 _open을, 체크를 해제하는 경우 _open 만 전달하게 된다.

스프링에서는 open & _open 에 대해서 true 처리, _open 만 전달되는 경우에는 false 처리를 한다.

Results

FormItemController : item.open=true //체크 박스를 선택하는 경우
FormItemController : item.open=false //체크 박스를 선택하지 않는 경우

하지만 매번 이런식으로 히든 필드를 직접 추가하는 것은 번거럽기 때문에 thymeleaf 가 제공하는 기능을 활용한다.

thymeleaf checkbox

<div class="form-check">
  <input type="checkbox" th:field="*{open}" class="form-check-input">
  <!-- <input type="hidden" name="_open" value="on"/> -->
  <label for="open" class="form-check-label">판매 오픈</label>
</div>

위와 같이 th:field을 통해 item의 open 필드를 설정하게 하면 아래와 같이 자동으로 랜더링 해준다.

<div class="form-check">
  <input type="checkbox" id="open" class="form-check-input" name="open"
  value="true">
  <input type="hidden" name="_open" value="on"/>
  <label for="open" class="form-check-label">판매 오픈</label>
</div>

Controller에서 item 객체를 전달해서, html에서 th:object를 이용해서 item 객체를 지정한다. 그렇게 해서 th:field에 필드변수를 지정해서 객체의 필드에 접근한다.

이때, th:field에 전달되는 값이 true로 지정되어 있는 경우 Checkbox의 경우 자동으로 checked처리를 한다.

멀티 체크 박스

@ModelAttribute 특별 기능

@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에 대해 별도의 메소드를 적용하면, 매번 Controller의 요청이 들어올때 마다 해당 메소드가 실행되면서 Model에 해당 attribute를 저장하게 된다.

이렇게 하면, 여러 곳의 controller에 중복되는 코드를 하나의 메소드로 줄일 수 있다.

Multi Checkbox

<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.next('regions')}"
                       th:text="${region.value}" class="form-check-label">서울</label>
</div>

th:each를 이용해서 regions 객체에 대해서 반복 수행해서 여러 개의 체크박스를 만들어 주고 있다.

멀티 체크박스의 경우 여러 개의 같은 이름을 가지는 체크박스를 만들어도 되지만, id 값을 모두 달라야한다. 위와 같이 each문을 통해서 반복 수행하게 되면 아래와 같이 id에 1,2,3 이 붙으면서 서로 다른 id값을 만들어 내는 것을 확인할 수 있다.

<input type="checkbox" value="SEOUL" class="form-check-input" id="regions1"
name="regions">
<input type="checkbox" value="BUSAN" class="form-check-input" id="regions2"
name="regions">
<input type="checkbox" value="JEJU" class="form-check-input" id="regions3"
name="regions">

각각의 label 태그는 체크박스 각각에 대해서 적용되야 하는데, 이때 label에는 각 체크 박스 id 값을 지정해줘야한다. 하지만, th:each을 통해 동적으로 생성되는 id는 랜더링 전에는 알 수 없다. 그래서 이를 위해 #ids 객체를 제공한다.

멀티 체크박스의 경우 th:field로 전달된 regions 객체와 th:value의 값을 비교해서, th:value 값이 regions 객체에 포함된 경우 해당 체크박스를 체크 처리한다.

Example

regions 객체에 [“SEOUL”, “BUSAN”] 이라고 저장되어 있고, 이를 model로 넘겨주게 되면 타임리프에서는 해당 2개의 체크 박스에 대해서는 체크 처리를 수행한다.

라디오 박스

ModelAttribute

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

Enum type에 대해 values()를 수행하면 enum type에 있는 모든 상수가 리스트 형태로 전달된다.

라디오 박스

<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>

위의 멀티 체크 박스와 형태가 유사하다. 차이점은 전달된 객체가 Enum Type이라는 것이다.

Enum type에 대해서 name() 메소드를 이용하면 해당 enum 상수를 문자열 형태로 반환한다. description은 ItemType enum type의 필드 변수인데, 이를 접근 할때, getter 메소드로 필드값에 접근한다.

라디오 박스의 경우에도 마찬가지고 th:field 값과 th:value 값을 비교해서 check 처리 수행여부를 결정한다.

ThymeLeaf Enum Object Access

타임리프에서 enum에 직접 접근하는 것이 가능하다.

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

하지만, 패키지 구조가 변경되는 경우 해당 코드를 바꿔야하는 문제가 발생하기 때문에 사용하는 것을 추천하지 않는다.

셀렉트 박스

ModelAttribute 추가

@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;
}

controller로 요청을 할때 마다 deliveryCode 객체를 만들기 때문에 해당 객체를 사전에 만들어놓고 참조만 하도록 하여 메모리 낭비를 최소화 할 수 있다.

셀렉트 박스

<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>

셀렉트 박스의 경우에도 마찬가지고 th:field 값과 th:value 값을 비교해서 check 처리 수행여부를 결정한다.

References

link: inflearn

link:springmvc

댓글남기기