SSR(Server Side Rendering)/Thymeleaf

Thymeleaf와 Spring Form(체크 박스, 라디오 버튼, 셀렉트 박스)

녁이 2023. 12. 13. 20:36
728x90
반응형

타임리프 스프링 통합

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

 

스프링 통합 메뉴얼: https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

 

Tutorial: Thymeleaf + Spring

Preface This tutorial explains how Thymeleaf can be integrated with the Spring Framework, especially (but not only) Spring MVC. Note that Thymeleaf has integrations for both versions 3.x and 4.x of the Spring Framework, provided by two separate libraries c

www.thymeleaf.org

 

 

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

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

이번 게시글에서는 Spring Form 에서 굉장히 많은 도움을 주는 th:field 와 th:object를 위주로 설명하겠다.

 


타임리프 설치

타임리프 적용 및 설치는 아래의 게시글을 참고하자.

2023.12.12 - [SSR(Server Side Rendering)/Thymeleaf] - Thymeleaf[타임리프]란? 타임리프의 기본 기능(표현식, escape, 스프링EL, 변수 선언)

 

Thymeleaf[타임리프]란? 타임리프의 기본 기능(표현식, escape, 스프링EL, 변수 선언)

타임리프(Thymeleaf)란? - 타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용되는 템플릿 엔진이다. ( 백엔드 서버에서 동적으로 렌더링 한다는 말은 타임 리프가 서버 사이드 HTM

junhyuk-develop.tistory.com

 


입력 폼 처리

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

 

th:field를 사용하면 자동으로 id, name, value 속성을 만들어준다.

렌더링 전

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

 

렌더링 후

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

 

위에서 *{itemName} 으로 표현했는데, 이는 th:object와 함께 사용되었을 때 가능한 표현식이다.

 

코드로 자세히 살펴보자.

 

Form 컨트롤러

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

빈 item() 객체를 model에 담아서 addForm 쪽으로 넘겨준다.

 

addForm.html

<form action="item.html" th:action th:object="${item}" method="post">
 <div>
 <label for="itemName">상품명</label>
 <input type="text" id="itemName" th:field="*{itemName}" class="formcontrol" 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="formcontrol" placeholder="수량을 입력하세요">
 </div>

 

th:object="${item}" : <form>에서 사용할 객체를 지정한다. 이를 사용하면 해당 테그 내에서 선택 변수 식( *{...} )을 적용할 수 있다.

th:field="*{itemName}" : 선택 변수 식을 사용했는데, ${item.itemName} 과 같다. 앞서 th:object 로 item 을 선택했기 때문에 선택 변수 식을 적용할 수 있다.

th:field 로 인해 id , name , value 속성을 모두 자동으로 만들어준다.

 

위에서 id 속성을 제거해도 무방하다. ide가 제대로 인식을 못해서 일단 넣어둔 상태임.

th:object , th:field 덕분에 폼을 개발할 때 약간의 편리함을 얻었다.

사실 이것의 진짜 위력은 뒤에 설명할 검증(Validation)에서 나타난다.

 


예시 이미지

 

단순 HTML 체크 박스 → 판매 여부

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

 

체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어간다.

스프링은 on 이라는 문자를 true 타입으로 변환해준다. ( 스프링 타입 컨버터가 해줌 )

 

그런데, 만약 체크를 하지 않는다면??

→ HTML에서 체크 박스를 선택하지 않고 폼을 전송하면 open 이라는 필드 자체가 서버로 전송되지 않는다.

로그를 찍어 확인해보면 item.open=null 와 같이 null값이 지정된다.

 

사용자가 의도적으로 체크되어 있던 값을 체크를 해제해도 저장시 아무 값도 넘어가지 않기 때문에, 서버 구현에 따라서 값이 오지 않은 것으로 판단해서 값을 변경하지 않을 수도 있다.

 

이를 해결하기 위해선??

이런 문제를 해결하기 위해서 스프링 MVC는 약간의 트릭을 사용하는데, 히든 필드를 하나 만들어서, _open 처럼 기존 체크 박스 이름 앞에 언더스코어( _ )를 붙여서 전송하면 체크를 해제했다고 인식할 수 있다. (히든 필드는 항상 전송)

 

 

기존 코드에 히든 필드 추가

<!-- single checkbox -->
<div>판매 여부</div>
<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>

 

히든 필드로 인해

체크 박스가 체크된다면 open=on&_open=on 

체크 박스가 미체크라면  _open=on 

→ 이를 통해 로그를 확인해보면 item.open=false 로 바뀐 것을 확인할 수 있다.

 

But, 이렇게 히든 필드를 계속해서 만들어주는 것은 너무나도 귀찮은 일.......

타임리프가 제공하는 폼 기능을 사용하면 이런 부 분을 자동으로 처리할 수 있다!!


th:field

th:field를 적용하면 위의 문제점을 해결할 수 있다.

 

<!-- single checkbox -->
<div>판매 여부</div>
<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:object 로 item을 설정해놓아서 th:field = "*{}" 형식으로 표현할 수 있는 것이다.

th:object 를 사용하지 않으면, ${item.open} 형식으로 바꿔주면 된다.

 

th:field를 넣음으로써 어떻게 되는지 보자.

 

타임리프 체크 박스 HTML 생성 결과

<!-- single checkbox -->
<div>판매 여부</div>
<div>
 <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>
</div>

 

히든 필드를 따로 넣어주지 않았지만, 타임리프가 자동으로 만들어준 것을 확인할 수 있다.

때문에, th:field를 사용함으로써, 히든 필드가 자동으로 생성되고, 체크 박스를 미체크해도 item.open=false 가 된다.

 


멀티 체크 박스 → 등록 지역

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

 

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

 

위와 같이 컨트롤러에 등록을 해놓으면, 해당 컨트롤러를 요청할 때 regions 에서 반환한 값이 자동으로 모델( model )에 담기게 된다.

해당 컨트롤러의 모든 메서드의 model에 regions 데이터가 담긴다는 말이다.

 

 

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

여기서 th:field= "*{regions}" 는 th:each 문에 있는 regions가 아니라, 위에서 등록한 th:object의 item에 있는 regions 이다.

region.key는 Map에 담겨 있는 regions들의 키 값이고, region.value는 그 키값들의 밸류 값이다.

 

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

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

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

 

결과

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

보면 name은 regions로 동일하지만, id에 regions + 숫자 가 자동으로 할당되면서 다른 것을 확인할 수 있다.

타임리프는 ids.prev(...) , ids.next(...) 을 제공해서 동적으로 생성되는 id 값을 사용할 수 있도록 한다.

 

 

타임리프 HTML 생성 결과

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

 

멀티 체크 박스도 마찬가지로 th:field를 통해서 히든 필드는 자동으로 제공된다.

 


라디오 버튼 → 상품 종류

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

라디오 버튼을 자바 ENUM을 활용해보자.

 

위에 멀티 체크 박스와 같은 순서로 진행하겠다.

 

컨트롤러에 추가

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

ItemType.values() 를 사용하면 해당 ENUM의 모든 정보를 배열로 반환한다. 예) [BOOK, FOOD, ETC]

 

 

addForm.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">
 BOOK
 </label>
 </div>
</div>

 

보면 형식은 같다. th:each문을 활용했고, th:field, th:value 등을 사용해서 enum 데이터들을 추출

type.name()으로 enum 데이터의 name 추출 → BOOK, FOOD, ETC

type.description()으로 enum 클래스에서 getDescription  → 도서, 식품, 기타

 


타임리프에서 ENUM 직접 사용

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

위의 형식으로 타임리프에서는 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;
}

deliveryCodes() 메서드는 컨트롤러가 호출 될 때 마다 사용되므로 deliveryCodes 객체도 계속 생성된다.

이런 부분은 미리 생성해두고 재사용하는 것이 더 효율적

 

 

addForm.html

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

셀렉트 박스는 <select로 시작함. 다른 부분은 위와 다를게 없다.

deliveryCode.code를 통해 데이터 접근 → FAST, NORMAL, SLOW

deliveryCode.displayName을 통해 데이터 접근 빠른 배송, 일반 배송, 느린 배송

 

 

타임리프로 생성된 HTML

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

 


정리

타임리프가 스프링과 통합되면서 제공하는 th:field와 th:object는 무조건 사용하는게 이득이다.

이것들의 진가는 스프링 validation 부분에서 밝혀진다.

다음 게시글에서는 th:error 등에 대해서도 설명하겠다.

 

728x90
반응형