ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] 타임리프가 지원하는 form 속성 기능 (check box, radio button, select box)
    Back-End/Spring Boot 2022. 10. 25. 11:19

     

    타임리프는 스프링 없이도 동작하지만, 스프링과 통합을 위한 다양한 기능을 편리하게 제공한다. 

     

     

    📑 입력 form 처리하기

     

    타임리프가 제공하는 입력 폼 기능을 적용해 효율적으로 뷰 템플릿을 개선해보려고 한다.

     

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

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

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

     

    th:field를 사용하면 렌더링 후 다음과 같이 id ,name, value값이 생성되는 걸 확인할 수 있다.

     

     

     

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

     

    @GetMapping("/add")
    public String addForm(Model model) {
        //타임리프 사용하려면 model에 빈 객체라도 넘겨줘야한다.
        model.addAttribute("item", new Item());
        return "form/addForm";
    }

     

     

    th:object="${item}" form에서 사용할 객체를 지정한다. 이렇게 지정해두면 선택 변수식 *{...}을 적용할 수 있다.

     

    <!--컨트롤러에서 빈 item 객체를 넘긴다음 filed에서 *로 사용 가능 -->
    <form action="item.html" th:action th:object="${item}" method="post">
    
        <div>
            <label for="itemName">상품명</label>
    <!--/*
            id와 name을 적는 대신 th:field를 적음 알아서 생성해줌.!
            여기서는 IDE가 id 값이 없으면 인식하지 못해서 id는 남겨둔다. 기능에선 상관 없음.
            <input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">*/-->
            <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"  th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
        </div>
     </form>

     

    그 후 사용자가 데이터를 입력하는 input 태그에 th:field="*{itemName}" 을 사용해준다. 앞서 th:object로 item을 선택했기 때문에 선택 변수식을 적용할 수 있다. 만약 선택 변수식을 적용하지 않는다면 th:field="${item.itemName}"으로 사용해야된다. 

     

     

    th:field 속성을 넣으면 자동으로 input 태그의 id, name, value 값을 사용한다.

     

     

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

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

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

     

     

     

    th:field 속성을 사용하고 타임리프 렌더링했을 때 페이지 소스보기를 해보면 id, name, value가 생성된 걸 확인 가능하다.

     

     

     

    * 참고로 Item 클래스의 소스 코드는 다음과 같다.

    package hello.itemservice.domain.item;
    
    import lombok.Data;
    
    import java.util.List;
    
    @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 방식

     

    : 상품 종류는 ENUM을 사용하며, 설명을 위해 description 필드를 추가했다.

     

    package hello.itemservice.domain.item;
    
    public enum ItemType {
        BOOK("도서"), FOOD("음식"), ETC("기타");
    
        private final String description;
    
    
        ItemType(String description) {
            this.description = description;
        }
    
        public String getDescription() {
            return description;
        }
    }

     

     

     

    🎃 자바 Bean 방식

     

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

     

    package hello.itemservice.domain.item;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    
    /**
     * FAST: 빠른 배송
     * NORMAL : 일반 배송
     * SLOW : 느린 배송
     */
    @Data
    @AllArgsConstructor
    public class DeliveryCode {
    
        private String code;
        private String displayName;
    }

     

     

     

     

    📑 체크 박스 - 단일

     

    아래와 같이 체크박스 form에 th:field를 이용해 ${item.open} 값으로 id, name, value를 생성한다.  th:field="*{open}" 이렇게 사용해도 된다. 

     

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

     

     

    타임리프를 사용하지 않은 경우 체크박스를 선택하면 item.open = true, 체크박스 선택하지 않으면 item.open=null이기에 따로 히든 필드인 _open을 만들어서 open은 전송되지 않고 _open의 값이 on이면 체크박스는 해제됐구나라고 판단했다. 

     

    하지만 이렇게 번거롭게 할 필요 없이 렌더링 후 확인해보면, 타임리프를 사용한 결과 <input>태그 뒤에 hidden 타입의 값이 자동으로 생성되는 걸 확인할 수 있다. 

     

     

    📌 체크 박스를 체크 open=on & _open=on
    : 체크박스 체크하면 스프링 MVC가 open 값이 있는 걸 확인하고 checked 사용. 이때 _open 무시

    📌 체크박스 미 체크 _open=on
    : 체크 박스를 체크하지 않으면 스프링 MVC가 _open만 있는 것을 확인하고, open값이 체크되지 않았다고 인식한다. 이 경우 Boolean 타입을 찍어보면 결과가 null이 아닌 false  인 것을 확인할 수 있다.

     

     

     

    📑 체크 박스 - 멀티

     

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

    체크 박스를 멀티로 사용하기 위해 예를들어 상품 등록 form, 상품 상세보기, 상품 수정 form에서 모두 서울, 부산, 제주라는 체크박스를 반복해서 보여줘야 한다. 이렇게 하려면 각각 컨트롤러에서 model.addAttribute()를 사용해 체크 박스 구성하는 데이터를 반복으로 넣어줘야 한다. 

     

    하지만!! @ModelAttribute를 이용하면 컨트롤러에 있는 별도의 메서드에 적용할 수 있다.

     

     

    <컨트롤러 설정>

    //항상 자동으로 모델에 addAttribute 자동으로 잠긴다.
    @ModelAttribute("regions")
    public Map<String,String> regions(){
        Map<String, String> regions = new LinkedHashMap<>(); //순서 보장 위해 LinkedHashMap 쓰기
        regions.put("SEOUL", "서울");
        regions.put("BUSAN", "부산");
        regions.put("JEJU", "제주");
        return regions;
    }

     

    이렇게 하면 해당 컨트롤러를 요청할 때 regions에서 반환 값이 자동으로 모델에 담기게 된다. 물론) 이렇게 사용하지 않고 각각의 컨트롤러에서 직접 데이터를 담아 처리해도 된다.

     

     

    <뷰- form>

    <!-- radio button -->
    <div>
        <div>상품 종류</div>
        <div th:each="type : ${itemTypes}" class="form-check form-check-inline">
            <!--/* enum의 name은 지정해둔 값을 반환해준다. BOOK("도서")면 BOOK 반환 */-->
            <input type="radio" th:field="*{itemType}" th:value="${type.name()}"
                   class="form-check-input">
            <!--/* enum은 property 접근법이기에 ItemType 클래스에 getter가 있어야 된다. */-->
            <label th:for="${#ids.prev('itemType')}" th:text="${type.description}"
                   class="form-check-label">
                BOOK
            </label>
        </div>
    </div>

     

     

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

     

    멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있다. 그런데 이렇게 반복해서 HTML 태그를 생성할 때, 생성된 HTML 태그 속에서 name은 같아도 되지만, id는 모두 달라야 한다. 따라서 타임리프는 체크박스를 each 루프 안에 반복해서 임의로 1,2,3 숫자를 붙여준다. 

     

     

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

     

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

     

     

     

     

    이제 결과를 살펴보면, 만약 서울하고 부산을 선택한다고 했을 때

     

     regions=SEOUL&_regions=on&regions=BUSAN&_regions=on&_regions=on

     

    이 값이 넘어가게된다. log.info("item.regions={}" , item.getRegions()); 를 해서 출력해보면 배열의 형태로 출력된다.

     

     

    여기서 _regions는 앞서 설명한 기능으로, 웹 브라우저에 체크를 하나도 하지 않았을때, 클라이언트가 서버에 아무런 데이터를 보내지 않는 것을 방지한다. 만약 _regions 조차 보내지 않으면 값은 null이 된다. 

     

     

     

    📑 라디오 버튼

     

    라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용할 수 있다. 앞서 만들어둔 ENUM을 활용해보자.

     

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

     

    컨트롤러에 itemType을 등록 폼, 조회, 수정 등등 모두 사용하기 위해 @ModelAttribute의 사용법을 적용한다. ItemType.values()는 해당 ENUM에 대한 정보를 배열로 반환한다. ex) [BOOK,FOOD,ETC]

     

    <!-- radio button -->
    <div>
        <div>상품 종류</div>
        <div th:each="type : ${itemTypes}" class="form-check form-check-inline">
            <!--/* enum의 name은 지정해둔 값을 반환해준다. BOOK("도서")면 BOOK 반환 */-->
            <input type="radio" th:field="*{itemType}" th:value="${type.name()}"
                   class="form-check-input">
            <!--/* enum은 property 접근법이기에 ItemType 클래스에 getter가 있어야 된다.
                   th:text에의해 클라이언트 화면 view에 BOOK("도서")중 도서가 출력된다. */-->
            <label th:for="${#ids.prev('itemType')}" th:text="${type.description}"
                   class="form-check-label">
                BOOK
            </label>
        </div>
    </div>

     

     

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

     

    체크 박스는 한 번 선택 후 다시 클릭하면 선택 해제되는데 라디오박스는 한 번 선택하면 다시 선택 취소할 수 없다. 

     

     

     

     

    라디오 버튼 아무것도 클릭하지 않으면 itemType에 null 값 나오는 거 확인!

     

     

     

     

     

    📑 셀렉트 박스

     

    셀렉트 박스는 여러 선택지 중 하나를 선택해 사용할 수 있다.

     

    DeliveryCodeData라는 클래스를 사용해서 getInstance를 사용하면 List 형의 deliveryCodes를 리턴한다. 

    package hello.itemservice;
    
    import hello.itemservice.domain.item.DeliveryCode;
    import org.springframework.web.bind.annotation.ModelAttribute;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    public class DeliveryCodesData {
    
        private static final List<DeliveryCode> deliveryCodes= Arrays.asList(
    
            new DeliveryCode("FAST", "빠른 배송"),
            new DeliveryCode("NORMAL", "일반 배송"),
            new DeliveryCode("SLOW", "느린 배송")
            );
    
        private DeliveryCodesData() {
        }
    
    
        public static List<DeliveryCode> getInstance(){
            return deliveryCodes;
        }
    
    
    
    }

     

     

    컨트롤러에서 직접 데이터를 추가하는 방법 말고 따로 클래스를 만들어 데이터를 삽입하는 방법을 택했다. 이렇게 하면 컨트롤러가 호출될 때마다 객체를 계속 생성하지 않고 재사용하므로 더 효율적이다. 

    @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;*/
       return DeliveryCodesData.getInstance();
    
    }

     

     

    셀렉트 방식의 코드를 살펴보면, 다른 input 버튼들과 마찬가지로 th:each를 통해 항목들을 생성한다.

     

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

     

     

    반응형

    댓글

Designed by Tistory.