Spring form with Thymeleaf

강정우·2023년 12월 13일
0

Spring-boot

목록 보기
38/73
post-thumbnail

스프링에 타임리프 세팅하기

입력 form 처리

일반 객체

  • 우선 spring과 thymeleaf로 뭔가 편하게 입력 form 처리를 하고 싶다면 controller 단에서 그냥 이름만 넘기는게 아니라 빈 모델이라도 무튼 Model를 함게 넘겨줘야한다. 이를 command 객체 라고 한다.
@GetMapping("/add")
public String addForm(Model model) {
    model.addAttribute("item", new Item());
    return "form/addForm";
}
  • 그럼 이제 addForm.html에서 form 태그ㅔ th:object 속성값을 추가해주고 생성한 모델의 값을 넣어준다.
    그리고 값이 들어가는 input 태그에 th:field 속성을 넣어주면 id, name, value 속성을 thymeleaf가 만들어준다.
<form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" th:field="${item.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>
        ...
</form>
  • 참고로 id 속성도 지워도 되지만 IDE가 인식하지 못 하기 때문에 따로 지우진 않겠다.
    또한 <form\>th:object가 명시가 되어있다면 <form\> 내부에 있는 <input\>th:field값을 적을 때 조금 더 간편하게 작성할 수 있다. ${item.itemName} -> *{itemName} 으로 바꿀 수 있다.

boolean

  • bool 타입일 땐 조금 복잡해진다. 왜냐하면 thymeleaf를 사용하지 않을 때 사용자가 checked를 하지 않고 넘기면 값이 아예 안 넘어오는데 이때 서버에서 이를 어떻게 처리할 지가 문제가 되기 때문이다.
<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>
  • 그래서 명시적으로 넣어주면 좋지만 위 코드처럼 따로 작성하지 않으면 spring mvc는 히든 필드로 판단할 수 있다. 사용자는 히든 필드를 추가하는데 이때, _open 처럼 기존 체크 박스 이름 앞에 언더스코어( _ )를 붙여서 전송하면 체크를 해제했다고 인식할 수 있다.
    히든 필드는 항상 전송된다. 따라서 체크를 해제한 경우 여기에서 open 은 전송되지 않고, _open 만 전송되는데, 이 경우 스프링 MVC는 체크를 해제했다고 판단한다.

  • 만약 open을 체크하여 값이 open=on&_open=on 이 되었을 땐 _open은 무시한다.
    그런데 이게 개불편하다 매번 히든 필드를 추가해야하기 때문이다. 그래서 thymeleaf를 추가하여 이를 편리하게 처리할 수 있다.

<div>
    <div class="form-check">
        <input type="checkbox" id="open" th:field="*{open}" class="form-checkinput">
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>
  • 그냥 th:field 만 추가해주면 된다.
    그럼 id, name, value와 input 인 경우에는 히든 필드까지 다 만들어준다.

@ModelAttribute

  • 참고로 앞서 각각의 컨트롤러에 들어가던 ModelAttribute와는 용도가 조금 다르다.

  • 물론 @ModelAttribute는 같은 라이브러리에서 나오는 것이긴 하나 용도가 조금 다른 것이

public class FormItemController {

    private final ItemRepository itemRepository;

    @ModelAttribute("regions")
    public Map<String, String> regions() {
        Map<String, String> regions = new LinkedHashMap<>();
        regions.put("SEOUL", "서울");
        regions.put("PUSAN", "부산");
        regions.put("JEJU", "제주");
        return regions;
    }
    
    @GetMapping("/{itemId}")
    ...
  • 이런식으로 컨트롤러 단에다가 명시를 해주면 이게 자동으로 모든 Mapping에 들어가게 되어
@GetMapping("/add")
public String addForm(Model model) {
    model.addAttribute("item", new Item());
    
    // Map<String, String> regions = new LinkedHashMap<>();
    // regions.put("SEOUL", "서울");
    // regions.put("PUSAN", "부산");
    // regions.put("JEJU", "제주");
    // model.addAttribute("regions",regions);

    return "form/addForm";
}
  • 이렇게 각각의 요청에 작성해야할 것을 작성하지 않아도 된다. 자동으로 담기기 때문이다.

물론 성능 최적화는 고려해봐야할 부분이다. 물론2 위 정도사이즈의 맵 객체야 성능에 신경도 안 쓰이겠지만
분명 어딘가의 static 영역에 생성을 해놓고 불러다 쓰는게 좋을 것이다.
왜냐하면 컨트롤러의 함수가 실행이 될 때 마다 계속 생성이 되기 때문이다.
따라서 동적으로 변화하는 값이 아니라면 어딘가에 생성해 두고 불러다 쓰는 것이 좋다.

여러개 form

  • 이렇게 위 처럼 여러개를 만들어뒀다면 얘를 for문을 돌리면서 form을 생성해보자.
<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>
  • 이때 주의해야할 점은 바로 input과 label 태그의 관계이다. 이 둘이 id, for가 맞아야하기 때문이다.
    그래서 thymeleaf는 동적으로 id를 생성할 수 있도록 #ids 유틸리티 클래스를 지원한다.

  • 그리고 이 여러개 form 역시 input:checkbox와 동일하기 때문에 각각의 hidden field 가 생기고 이를 모두 선택하지 않았을 때는 빈 배열이 출력된다.

  • 그래서 나중에 값을 수정하는 html이 아닌 값을 확인하는 html에서도 아래 코드를 보면

<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:field="${item.regions}" 이제 이 부분에 사용자가 체크한 값이 들어가고 th:value="${region.key}" 여기에 앞서 @ModelAttribute에 작성한 값이 들어와서 둘이 비교해 가며 checked를 thymeleaf로 동적으로 만들어주는 것이다.

라디오 버튼

  • 이번에는 ENUM 타입도 함께 활용하여 공부해보자.

  • 일단 컨트롤러에서는 ENUM 타입을 똑같이 @ModelAttribute를 이용하여 넘겨보자

@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
    ItemType[] values = ItemType.values();
    return values;
}
  • 참고로 위 코드를 IDE가 밑줄 그어준 부분에 커서를 두고 opt + cmd + n 단축키를 누르면 인라인한 코드로 더 짧게 한 큐에 바꿀 수 있다.

  • 그리고 이때 enum type에 필드값이 있다면 반드시 getter 메서드가 있어야만 thymeleaf에서 해당 getter를 갖다가 쓰기 때문에 오류없이 코드가 동작한다.

public enum ItemType {
    BOOK("도서"), FOOD("식품"), ETC("기타");
    private final String description;
    ItemType(String description) {
        this.description = description;
    }
    public String getDescription() {
        return description;
    }
}
  • 그리고 이제 html 코드를 보면 type.name() 으로 각각의 enum 타입을 뽑아내고
    type.description으로 getter를 실행해서 표출해준다.
<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>
  • 참고로 라디오 버튼은 그냥 체크를 안 할 시에는 null로 들어간다.
    그 말은 hidden field를 만들지 않는 다는 것이다.

thymeleaf의 ENUM 타입 직접 접근하기

  • 참고로 앞서는 ENUM 타입을 컨트롤러에서 모델에 담아서 넘겨줬었는데 Spring EL 문법을 통하여 직접 접근이 가능하긴 하다. 그런데 해당 enum 타입이 위치한 패키지를 다 적어줘야한다.
<div>
    <div>상품 종류</div>
    <div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}" 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>
  • 다만 단점은 또 패키지가 이동하면 못 찾는다는 단점도 있기 때문에 별로 권장하지는 않는다.

select box

@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;
}
  • 위와 같이 생긴 데이터가 있다고 가정할 때
<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>
  • 얘도 역시 라디오버튼, 체크박스와 마찬가지로 th:each가 돌면서 나온 값과 th:value의 값을 비교하여 값을 selected하여 넘겨준다.
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글