지난 포스팅에서 개발해본 간단한 상품 관리 웹 애플리케이션을 조금 더 고도화 해볼 것이다.
스프링과 타임리프는 입력 폼을 매우 효율적으로 처리하기 위한 기능들을 제공한다.
해당 기능을 알아보고 프로젝트에 추가된 요구사항에 맞춰 애플리케이션을 개선해보자.
지난 포스팅 : https://dong-woo.tistory.com/86
- 입력 폼 처리 (th:object, th:field)
HTML의 폼을 통해 데이터를 전송할 때 보면 다음과 같은 구조를 갖는다.
<form action="item.html" method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">
</div>
<div>
</form>
input 타입으로 데이터를 넘길 때 보면 id, name과 같은 속성들이 있다. 무엇을 의미할까?
- id : HTML 안에서 식별하는 고유한 id
- name : 서버로 데이터가 전송될 때, 서버에서 식별하기 위한 이름
- value : 서버로 데이터가 전송될 때, name에 매칭되는 값
현재 프로젝트는 대부분 이렇게 폼을 통해 데이터를 POST 방식으로 보내고, Controller에서는 해당 데이터를 @ModelAttribute를 통해 객체타입으로 받아 사용한다.
하지만 이렇게 하나하나 직접 작성해주게 되면 오타가 발생해 변수명을 맞추지 못할 수 도 있고, 여러번 반복적으로 작성해야 하는 코드가 늘어나 효율적이지 못하다.
스프링과 타임리프는 해당 입력 폼 처리를 보다 효율적으로 할 수 있는 기능을 제공한다.
th:object
th:object는 커맨드 객체를 지정해주는 것이다. 입력 폼에서 데이터를 서버사이드로 전송해주면 결국 @ModelAttribute를 통해 원하는 객체 타입으로 데이터를 가공할 것이다.
이를 생각해 GET 요청이 들어올 때 미리 해당 타입의 빈 객체를 생성해 모델에 담아 보내주는 것이다.
그러면 HTML에서는 폼 태그 시작부분에 th:object를 받아온 객체로 지정하고, 폼 태그 안에서 지정할 id, name, value 등의 속성을 간단하게 지정할 수 있다.
글로 보면 무슨 말인지 어려우니 실제 코드로 확인해보자!
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "form/addForm";
}
// 입력 폼 요청 컨트롤러 수정 (모델에 빈 객체 담아서 보내기)
<form action="item.html" th:action="@{/basic/items/add}" method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">
</div>
</form>
<!-- 기존코드 -->
<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="form-control" placeholder="이름을 입력하세요">
</div>
</form>
<!-- th:object 적용 -->
th:object를 적용시키지 않는 부분은 id, name을 서버에서 받을 객체 프로퍼티에 맞추어 직접 이름을 지정해주고 있다.
적용시킨 후의 모습을 보면 새로운 th:field 라는 기능을 사용하고 있는데, th:object로 받아온 변수를 인식하고 선택 변수 식 *{...}을 사용해 원하는 이름을 지정해준다.
해당 *{itemName}은 다음과 같이 변환된다.
- *{itemName} --> ${item.itemName}
여기까진 알겠다! 근데 th:field는 뭘까?
th:field
th:field는 id, name, value 등의 속성을 자동으로 만들어주는 기능이다.
위의 예제에서는 for="id값"을 사용해서 어쩔 수 없이 id는 남겨놨지만 사실 지워도 동작하는데 문제가 없다.
th:field를 사용하면 id, name을 모두 해당 프로퍼티 이름 (itemName)으로 만들어준다.
따라서 id, name을 직접 작성해주지 않아도 th:field만으로 해당 부분을 완벽하게 대체할 수 있다.
여기에 value, checked 등의 속성은 나오지 않았지만 뒤에 나올 것이다! 그 때 살펴보자.
참고로 커맨드 객체를 사용하지 않은 곳에서도 th:field를 사용할 수 있다.
커맨드 객체를 함께 사용했을 때 선택 변수 식을 이용해 간단하게 적었지만, 단순 th:field만 사용한다면 ${...} 식으로 모델에서 직접 프로퍼티 접근법을 통해 사용하면 된다.
- 추가된 요구사항
판매 여부
- 판매 오픈 여부
- 체크 박스로 선택 가능
판매 등록 지역
- 서울, 부산, 제주
- 체크 박스로 다중 선택 가능
상품 카테고리
- 도서, 식품, 기타
- 라디오 버튼으로 하나만 선택할 수 있다.
배송 방식
- 빠른 배송
- 일반 배송
- 느린 배송
- 셀렉트 박스로 하나만 선택할 수 있다.
- 단일 체크 박스 (판매 여부)
<!-- 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>
<!-- 기존의 히든필드를 사용해야만 하는 방식 -->
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
<!-- th:field를 사용해 간단해진 모습 -->
체크박스의 경우 순수 HTML로 작성할 때 히든필드를 사용해주지 않으면 서버 입장에서 체크여부를 true 이나 null로만 판단할 수 있다.
true나 false로 구분되어야 명확성이 생기는데, null로 응답이 온다면 true인건지 false인건지 몰라 애매할 수 있다.
이 경우 타임리프의 th:field를 사용하면 위에서 언급한 것처럼 id, name, value를 대신 생성해주면서 동시에 히든필드까지 만들어준다.
따라서 빈칸으로 응답을 보냈을 때도 null이 아닌 false로 응답하는 모습을 볼 수 있다.
- @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("BUSAN", "부산");
regions.put("JEJU", "제주");
return regions;
}
@GetMapping("/add")
.
.
.
}
결과적으로 해당 메소드명이 attributeName으로, 리턴값이 value로 모델에 담긴다.
이제 멀티 체크 박스를 추가해보자.
<!-- 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:each문을 사용했다.
내부를 보면, th:field를 사용해 커맨드 객체로 전해진 item을 이어받아, id, name 속성 값들을 프로퍼티 접근법으로 regions로 정해주고 있다. name은 자동으로 item.regions로 맞추어져 전해지지만 HTML의 id값은 고유한 값으로 생성되어야 한다.
타임리프는 체크박스를 th:each루프 안에서 반복적으로 만들어 질 때, 임의로 1, 2, 3 숫자를 뒤에 붙여서 해결해준다!
HTML의 id가 방금 타임리프에 의해 동적으로 만들어졌기 때문에 레이블의 대상이 되는 id값을 임의로 지정하는 것은 어려워졌다.
타임리프는 ids.prev(...)을 제공해서 동적으로 생성되는 id값을 사용할 수 있도록 지원한다.
- 라디오 버튼 (상품 카테고리)
라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용할 수 있다.
이번 상품 카테고리는 ENUM을 활용해서 개발해보자.
public enum ItemType {
Book("도서"), Food("음식"), ETC("기타");
private final String description;
ItemType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
// ENUM 생성
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
ItemType[] values = ItemType.values();
return values;
}
// 컨트롤러에 추가
<!-- 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를 통해 모델에 담아온 ENUM을 루프로 돌린다.
마찬가지로 th:field를 사용해 ${item.itemType}으로 id, name을 지정해준다.
서버에 전달되는 값으로는 BOOK 처럼 name()을 넘기고, 사용자에게 표시될 이름은 "도서" 처럼 description을 사용한다.
- 셀렉트 박스 (배송 방식)
셀렉트 박스는 여러 선택지 중에 하나를 선택할 때 사용할 수 있다. 셀렉트 박스는 자바 객체를 활용해서 개발해보자.
@Data
@AllArgsConstructor
public class DeliveryCode {
private String code;
private String displayName;
}
// 객체
@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;
}
// 컨트롤러에 추가
<!-- 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:field는 ${item.deliveryCode}를 사용해 id, name을 처리한다.
셀렉트 박스 자체를 여러 개 만드는 것이 아니고, 내부에 선택할 수 있는 옵션을 여러 개 만드는 것이다.
따라서 내부 옵션 태그에서 th:each문을 반복한다.
내부에서 실행되는 로직은 이전과 동일하다!
- 정리
입력 폼으로 데이터를 전달할 때 효율적으로 도와주는 타임리프의 th:object 태그에 대해 배웠다.
같이 사용하면 시너지를 발휘하는 th:field 태그에 대해 배웠다.
다만 th:field 태그는 굳이 입력 폼이 아니더라도 개별적으로 사용할 수 있다는 점을 헷갈리지 말자!
출처 : 인프런, 김영한의 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술
'Spring' 카테고리의 다른 글
[Spring] 유효성 검증 - Validation (0) | 2023.07.21 |
---|---|
[Spring] 메시지, 국제화 기능 (0) | 2023.07.19 |
[Spring] 스프링 MVC로 웹 페이지 만들기 (0) | 2023.07.15 |
[Spring] 스프링 MVC 기본 기능 - HTTP 요청, 응답 처리하기 (0) | 2023.07.14 |
[Spring] 스프링 MVC 구조 이해하기 (1) | 2023.07.11 |