1, 2 편에 이은 타임리프 마지막 시간입니다. 스프링 프레임워크와 통합하고 등록, 수정 폼까지 만들어보겠습니다.
입력 폼 처리
지금부터 타임리프가 제공하는 입력 폼 기능을 적용해서 기존 프로젝트의 폼 코드를 타임리프가 지원하는 기능을 사용해서 효율적으로 개선해보겠습니다.
- th:object : 커맨드 객체를 지정한다.
- *{...} : 선택 변수 식이라고 한다. th:object 에서 선택한 객체에 접근한다.
- th:field
HTML 태그의 id , name , value 속성을 자동으로 처리해준다.
등록 폼 만들기
**FormController**
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item()); // 모델에 item 담아서 넘겨줌
return "form/addForm";
}
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/form/items/{itemId}";
}
**addForm.html**
<form action="item.html" th:action th:object="${item}" method="post">
//th:object로 model에 받아온 item을 바인딩
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
// th:field로 id name value값 세팅 // *{itemName} = ${item.itemName}같음
</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>
- th:object="${item}" : <form> 에서 사용할 객체를 지정합니다. 선택 변수 식( *{...} )을 적용할 수 있습니다.
- th:field="*{itemName}"
- *{itemName} 는 선택 변수 식을 사용했는데, ${item.itemName} 과 같습니다.
앞서 th:object 로 item을 선택했기 때문에 선택 변수 식을 적용할 수 있습니다. - th:field 는 id , name , value 속성을 모두 자동으로 만들어줍니다.
- id : th:field 에서 지정한 변수 이름과 같습니다. id="itemName"
- name : th:field 에서 지정한 변수 이름과 같다.
- name="itemName"
- value : th:field 에서 지정한 변수의 값을 사용한다. value=""
참고로 해당 예제에서 id 속성을 제거해도 th:field 가 자동으로 만들어줍니다.
같은 방법으로 수정 폼도 만들어보겠습니다.
수정 폼 만들기
**Form controller**
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "form/editForm";
}
**editForm.html**
<form action="item.html" th:action th:object="${item}" method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" class="form-control" th:field="*{id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" class="form-control" th:field="*{itemName}">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" class="form-control" th:field="*{price}">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" class="form-control" th:field="*{quantity}">
</div>
등록 폼과 동일하게 form태그에서 th:object를 이용해 item을 바인딩 받고 field값을 선택변수식을 이용해 세팅해줍니다.
id, name, value를 모두 신경써야 했는데, 많은 부분이 th:field덕분에 자동으로 처리되는 것을 확인할 수 있습니다.
등록 폼보다 수정 폼에서 더 편하게 사용할 수 있습니다.
체크박스
체크박스 - 단일 1
체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어갑니다. 스프링은 on 이라는 문자를 true 타입으로 변환해줍니다.
단순 체크박스일 경우에 체크를 하면 true값이 정상적으로 리턴 되지만, 체크를 안했을 시에 false값이 아닌 null값이 돌아오게 됩니다.
HTML checkbox는 선택이 안되면 클라이언트에서 서버로 값 자체를 보내지 않습니다.
이런 문제를 해결하기 위해서 스프링 MVC는 약간의 트릭을 사용하는데, 히든 필드를 하나 만들어서, open처럼 기존 체크 박스 이름 앞에 언더스코어()를 붙여서 전송하면 체크를 해제했다고 인식할 수 있습니다.
<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>
체크박스 체크
open=on&_open=on 체크 박스를 체크하면 스프링 MVC가 open 에 값이 있는 것을 확인하고 사용한다. 이때 _open 은 무시한다.
체크 박스 미체크
_open=on 체크 박스를 체크하지 않으면 스프링 MVC가 _open 만 있는 것을 확인하고, open 의 값이 체크되지 않았다고 인식한다. 이 경우 서버에서 Boolean 타입을 찍어보면 결과가 null 이 아니라 false 인 것을 확인할 수 있다.
log.info("item.open={}", item.getOpen());
체크박스 - 단일 2
타임리프
개발할 때 마다 이렇게 히든 필드를 추가하는 것은 상당히 번거롭습니다.
타임리프가 제공하는 폼 기능을 사용하면 이런 부분을 자동으로 처리할 수 있습니다.
<div>판매 여부</div> <div>
<div class="form-check"> //만능 th:field 추가
<input type="checkbox" id="open" name="open" th:field="*{open}" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
th:field= 를 사용하면
이전에 스프링이 제공하는 기능처럼 hidden 타입의 _open필드가 자동 생성된것을 볼 수 있습니다.
이는 타임리프에서 제공해주는 기능입니다.
타임리프의 체크 확인
checked="checked"
체크 박스에서 판매 여부를 선택해서 저장하면, 조회시에 checked 속성이 추가된 것을 확인할 수 있습니다.
이런 부분을 개발자가 직접 처리하려면 상당히 번거롭습니다. 타임리프의 th:field 를 사용하면, 값이 true인 경우 체크를 자동으로 처리해줍니다.
또한, 단순 조회시(상품상세)에 체크박스가 선택되지 않도록 하려면 태그 끝에 disabled를 붙이면 체크박스가 보이지만, 체크할 수 없게 만들어줍니다.

체크박스 - 멀티
체크 박스를 멀티로 사용해서, 하나 이상을 체크할 수 있도록 해보자.
- 등록 지역 - 서울, 부산, 제주
체크 박스로 다중 선택할 수 있다.

@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의 특별한 사용법
등록 폼, 상세화면, 수정 폼에서 모두 서울, 부산, 제주라는 체크 박스를 반복해서 보여주어야 합니다.
이렇게 하려면 각각의 컨트롤러에서 model.addAttribute(...) 을 사용해서 체크 박스를 구성하는 데이터를 반복해서 넣어주어야 합니다.
→ 중복발생 !!!
@ModelAttribute 는 이렇게 컨트롤러에 있는 별도의 메서드에 적용할 수 있습니다.
이렇게 하면 해당 컨트롤러를 요청할 때 regions 에서 반환한 값이 자동으로 모델( model )에 담기게 됩니다. 물론 이렇게 사용하지 않고, 각각의 컨트롤러 메서드에서 모델에 직접 데이터를 담아서 처리해도 됩니다.
**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" >. //value에 resion 키값을 설정
<label th:for="${#ids.prev('regions')}" // id값이 있어야 하는데 자동으로 생성해줌
th:text="${region.value}" class="form-check-label">서울</label>
</div> // text에 region의 value값 설정(서울,부산,제주)
</div>

value값과 text에 성공적으로 바인딩되었고, 체크된 부분은 checked를 자동으로 해주고 있습니다.
또 #ids 문법을 사용함으로써 자동으로 id를 생성해주고 있다(regions1,2,3)
- th:for="${#ids.prev('regions')}"
멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있습니다. 그런데 문제는 이렇게 반복해서 HTML 태그를 생성할 때, 생성된 HTML 태그 속성에서 name 은 같아도 되지만, id 는 모두 달라야 합니다.
따라서 타임리프는 체크박스를 each루프 안에서 반복해서 만들 때 임의로 1,2,3 숫자를 뒤에 붙여줍니다.
이 밖에도 등록할 때는 체크박스 단일에서 사용한것처럼 기본적으로 히든 필드가 자동생성되어 null값을 방지해줍니다.
라디오 버튼
라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용할 수 있습니다. 라디오 버튼을 자바 ENUM을 활용해서 개발해봅시다.
- 상품 종류 - 도서, 식품, 기타
라디오 버튼으로 하나만 선택할 수 있다.

FormController
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
}
ItemType ENUM
public enum ItemType {
BOOK("도서"), FOOD("음식"), ETC("기타");
private final String description;
ItemType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
itemTypes 를 등록 폼, 조회, 수정 폼에서 모두 사용하므로 @ModelAttribute 의 특별한 사용법을 적용하겠습니다.
ItemType.values() 를 사용하면 해당 ENUM의 모든 정보를 배열로 반환합니다. 예) [BOOK, FOOD, ETC]
<!-- 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"> // enum의 discription값
BOOK
</label>
</div>
</div>
체크 박스는 수정시 체크를 해제하면 아무 값도 넘어가지 않기 때문에, 별도의 히든 필드로 이런 문제를 해결했습니다.
라디오 버튼은 이미 선택이 되어 있다면, 수정시에도 항상 하나를 선택하도록 되어 있으므로 체크 박스와 달리 별도의 히든 필드를 사용할 필요가 없습니다.
다른 방법으로 타임리프에서 ENUM을 직접 사용할 수도 있습니다.
<!-- radio button -->
<div>
<div>상품 종류</div> // 이 부분에 java아래 패키지명을 적어주고 .values로 배열로 만듬
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}" 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>
${T(hello.itemservice.domain.item.ItemType).values()} 스프링EL 문법으로 ENUM을 직접 사용할 수 있습니다.
ENUM에 values() 를 호출하면 해당 ENUM의 모든 정보가 배열로 반환됩니다.
그런데 이렇게 사용하면 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;
}
deliveryCode
@Data
@AllArgsConstructor
public class DeliveryCode {
private String code;
private String displayName;
}
<!-- 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">

th:each로 반복문을 돌리면서 deliveryCodes와 같은 값을 가지고 있는 th:value값이 있으면 반환해줍니다.
출처
https://www.inflearn.com/roadmaps/373
우아한형제들 최연소 기술이사 출신 김영한의 스프링 완전 정복 로드맵 - 인프런
Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.
www.inflearn.com