스프링 통합과 폼

Jaca·2021년 10월 21일
0

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


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

  • 스프링의 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:object 를 적용하려면 먼저 해당 오브젝트 정보를 넘겨주어야 한다. 등록 폼이기 때문에 데이터가 비어있는 빈 오브젝트를 만들어서 뷰에 전달해야한다.

요구 사항

  • 판매 여부
    • 판매 오픈 여부
    • 체크 박스로 선택할 수 있다.
    • Boolean
  • 등록 지역
    • 서울, 부산, 제주
    • 체크 박스로 다중 선택할 수 있다.
    • List
  • 상품 종류
    • 도서, 식품, 기타
    • 라디오 버튼으로 하나만 선택할 수 있다.
    • ENUM
  • 배송 방식
    • 빠른 배송, 일반 배송, 느린 배송
    • 셀렉트 박스로 하나만 선택할 수 있다.
    • String

ENUM , 클래스, String 같이 다양한 데이터를 받아서 처리하는 법을 알아보자.

ENUM 상품 종류

public enum ItemType {

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

    private final String description;

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

    public String getDescription() {
        return description;
    }
}

Class 배송 방식

/**
* FAST: 빠른 배송
* NORMAL: 일반 배송 
* SLOW: 느린 배송 
*/
@Data
@AllArgsConstructor
public class DeliveryCode {
    private String code;
    private String displayName;
}

체크 박스 - 단일

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

판매 여부 부분은 체크박스로 true/false만을 체크할 것 이다.

현재 타임리프를 적용한 체크박스이나, 타임리프를 적용하지 않은 html 코드일 경우 체크 박스를 적용하지 않으면,
false가 올 것을 기대하지만 실제로는 open 필드가 아예 전송되지 않아 null이 넘어온다.
물론 이에 맞게 코딩할 수도 있겠지만, 아무래도 false가 넘어오는 것이 우리에겐 더 직관적일 것이다.

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

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

하지만 히든 필드 코드를 타임리프가 제공하는 폼 기능을 사용하면 이런 부분을 자동으로 처리할 수 있다.

위 코드가 타임리프가 적용된 코드이다. 실행 결과를 보자


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

실제 렌더링된 화면의 소스를 보면 타임리프가 히든 필드 코드를 추가해주었다.

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

체크박스 - 멀티

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

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

이러한 반복 작업을 @ModelAttribute 를 통해 별도의 메서드로 뽑아 해당 컨트롤러를 요청할 때 regions 에서 반환한 값이 자동으로 모델( model )에 담기게 된다.

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

이 메서드는 컨트롤러에 해당되어 있는 모든 요청에 적용된다

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

th:for="${#ids.prev('regions')}"
멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있다.
그런데 문제는 이렇게 반복해서 HTML 태그를 생성할 때, 생성된 HTML 태그 속성에서 name 은 같아도 되지만, id 는 모두 달라야 한다.
따라서 타임리프는 체크박스를 each루프 안에서 반복해서 만들 때 임의로 1,2,3 숫자를 뒤에 붙여준다.

<!-- multi checkbox -->
        
<div>
  <div>등록 지역</div>
  <div class="form-check form-check-inline">
    <input type="checkbox" value="SEOUL" class="form-check-input" 
           id="regions1" name="regions">
    <input type="hidden" name="_regions" value="on"/>            
    <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" 
           id="regions2" name="regions">
    <input type="hidden" name="_regions" value="on"/>               
    <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" 
           id="regions3" name="regions">
    <input type="hidden" name="_regions" value="on"/>   
    <label for="regions3" class="form-check-label">제주</label>    
  </div>  
</div>

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

각 체크박스 별로 히든 필드가 있다.
이것을 통해 싱글 체크 박스와 동일하게 _regions 필드를 만들어 체크 여부를 확인한다는 것을 알 수 있다.

라디오 버튼

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

동일하게 ENUM 타입을 모델에 넣어준다.
ItemType.values() 를 사용하면 해당 ENUM의 모든 정보를 배열로 반환한다.

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

실행 결과 로그를 찍어보면 아래와 같은 결과를 얻을 수 있다.
itemType=FOOD //음식 선택, 선택하지 않으면 아무 값도 넘어가지 않는다.

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

모델에 ENUM을 담아서 전달하는 대신에 타임리프는 자바 객체에 직접 접근할 수 있다.

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

스프링EL 문법으로 ENUM을 직접 사용할 수 있다. ENUM에 values() 를 호출하면 해당 ENUM의 모든 정보가 배열로 반환된다.
그런데 이렇게 사용하면 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;
}

동일하게 여러 곳에서 사용할 것이기에 처리를.. 하지만..
@ModelAttribute 가 있는 deliveryCodes() 메서드는 컨트롤러가 호출 될 때 마다 사용되므로 deliveryCodes 객체도 계속 생성된다.
이런 부분은 미리 생성해두고 재사용하는 것이 더 효율적이다.

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

실행 결과

<!-- SELECT -->
<div>
  <DIV>배송 방식</DIV>

  <select class="form-select" id="deliveryCode" name="deliveryCode"> 
    <option value="">==배송 방식 선택==</option>
    <option value="FAST">빠른 배송</option>
    <option value="NORMAL">일반 배송</option>
    <option value="SLOW">느린 배송</option> 
  </select>
</div>
profile
I am me

0개의 댓글