타임리프와 스프링 통합에 관해 공부해보자
관련 통합 덕분에 우리는 다음과 같은 기능을 추가적으로 사용할 수 있다.
- SpringEL 문법의 통합
${@myBean.doSomething()}
처럼 스프링 빈을 직접 호출할 수 있음- 편리한 폼 관리를 위한 추가 속성
th:object
th:field
,th:errors
,th:errrorclass
- 폼 컴포넌트 기능
- checkbox, radio button, List 등을 편리하게 지원
- 스프링 메세지, 국제화
- 메세지를 통해 html 변경 없이 출력하고자 하는 메세지 변경 가능
- 국제화를 통해 HTTP Header의 Accept-Language에 따라 다른 언어로 html 렌더링 가능
- 스프링 검증, 오류 처리 통합 (Validated, WebBinder, BindingResult)
- 스프링의 변환 서비스 통합(Conversion Service?) - 복습 도중인데 이게 왜 기억이 안나죠
입력 폼 처리
기존의 상품관리 예제를 토대로 Thymeleaf의 기능을 사용해 개선해보자
모델로 빈 껍데기 객체를 넘겨주면 다룰 폼이 Item
과 관련된 폼임을 알 수 있다
@GetMapping // 컨트롤러 코드에 해당
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "form/items";
}
타임리프를 통한 HTML 파일 수정은 다음 코드와 같다
<form action="item.html" th:action method="post" th:object="${item}">
<div> <label for="itemName">상품명</label>
<input type="text" id="itemName"
th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
<!-- th:field = id와 name이 자동 생성됨 * th object를 잡았기 때문에 item.itemname으로 인식됨.-->
<!-- 그러면 id="itemName" name="itemName" 지워도 됨, id는 일단 남겨둠 (IDE 인식문제) -->
</div>
</form>
th:object
Form에서 사용할 객체를 지정하게 되는데 이 때 밑의 input type의 필드로 선택 변수식을 적용할 수 있다.
th:field
앞서 th:object
를 이용해서 item
객체를 선택하였기 때문에 선택 변수식 *{itemName}*
을 사용할 수 있다th:field
는 id
, name
, value
속성을 모두 자동으로 만들어준다.id
: th:field
에서 지정한 변수의 이름과 같다.name
: th:field
에서 지정한 변수의 이름과 같다.value
: th:field
에서 지정한 변수의 값을 사용한다.
- 개발자 입장에서 매우 편리!
Note That) 선택 변수 식 *{itemName} -> ${item.itemName} 해도 됨.
결국 이 item을 뷰를 렌더링 할때 쓰는데 addForm에서 modelAttribute로 넘어감
맞춰 놓으면 이름 실수로 틀렸을 때 오류가 난다 (값이 서버로 안넘어오는것보다 오류가 낫다)
th:field는 id, name, value 속성을 모두 자동으로 만들어준다.
수정 폼 처리
<form action="item.html" th:action th:object="${item}" method="post">
<div> <label for="id">상품 ID</label>
<!-- <input type="text" id="id" name="id" class="form-control" value="1" th:value="${item.id}" readonly> -->
<input type="text" id="id" class="form-control" value="1" th:field="*{id}" readonly>
</div> <div> <label for="itemName">상품명</label>
<input type="text" id="itemName" class="form-control" value="상품A" th:field="*{itemName}">
</div> <div> <label for="price">가격</label>
<input type="text" id="price" class="form-control" value="10000" th:field="*{price}">
</div> <div> <label for="quantity">수량</label>
<input type="text" id="quantity" class="form-control" value="10" th:field="*{quantity}">
</div>
<hr class="my-4">
<div>판매 여부</div>
<div> <div class="form-check">
<input type="checkbox" id="open" th:field="${item.open}" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div> </div> <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> <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> <!-- SELECT -->
<div>
<div>배송 방식</div>
<select th:field="${item.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">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">저장</button>
</div> <div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='item.html'"
th:onclick="|location.href='@{/form/items/{itemId}(itemId=${item.id})}'|"
type="button">취소</button>
</div> </div>
</form>
수정 폼의 경우 id
, name
, value
를 모두 신경 써주어야 했다. (수동 작성)
하지만 이제는 타임리프에 의해 modelAttribute
에서 넘어온 객체 값으로 렌더링된다.
타임리프를 이용한 체크박스, 라디오버튼, 셀렉트박스를 활용하는 방법
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input">
<input type="hidden" id="_open" value="on"/>
<label for="open" class="form-check-label">판매 오픈</label>
</div></div>
이 때 체크박스를 선택하면 HTML Form에서 POST
전송할 때 open=on
이라는 값이 넘어간다.
스프링은 on
이라는 문자를 true
타입으로 변환해준다.
-> 개발할때는 주로 true
, false
식의 boolean 형태를 이용하는데, HTML과는 맞지 않는 부분을 맞춰준다.
그런데 HTML에서 체크박스를 선택하지 않고 폼을 전송하면 open 이라는 필드 자체가 서버로 전송이 안됨
-> 문제가 생기는 이유
사실 제품을 추가한다고 가정하면 field 전송이 없으면 그냥 체크가 안된것으로 간주하면 되는데
문제는 체크가 되어있던 상품을 체크해제하고 그 상태를 저장한다고 했을때 체크가 안되어있다면
null로는 그 상품의 체크박스를 해제한 것인지 알기가 애매함
(서버 구현에 따라 값 변경이 일어나지 않을 수 있음.)
-> 스프링의 해결책 (트릭)
히든필드를 하나 만들어서 _open
처럼 기존 체크박스 앞에 언더스코어를 붙여 전송하면 체크를 해제했다고 간주한다. 따라서 체크를 해제한 경우 open
은 전송되지 않고 _open
만 전송된다.
정리.open
, _open
모두 전송되는 경우 - 체크한 것으로 인식_open
만 전송되는 경우 - 체크 해제된 것으로 인식
체크박스 - 단일 2
개발할때마다 히든 필드를 추가하기 번거롭지만 타임리프가 제공하는 폼 기능을 활용하면 자동으로 처리됨.
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName"
th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
<!-- th:field = id와 name이 자동 생성됨 * th object를 잡았기 때문에 item.itemname으로 인식됨.-->
<!-- 그러면 id="itemName" name="itemName" 지워도 됨, id는 일단 남겨둠 (IDE 인식문제) --> <label for="open" class="form-check-label"> 판매 오픈 </label>
</div>
해당 코드의 렌더링 결과는 아래와 같다.
<!-- 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>
체크박스 - 멀티
@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
를 이용해 메서드를 추가했다.
이런식의 코드 구사는 해당 Map
객체가 모든 컨트롤러 호출시 필요한 경우에 사용한다.
여기서 해당 Map
객체는 지역을 나타내는데, 지역선택은 제품 추가/수정에서 모두 필요하다.
이렇게하면 해당 컨트롤러를 요청할때 regions
에서 반환한 값이 자동으로 model
에 담기게 된다.
<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> <!-- th:for="${#ids.prev('regions')}" 멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있다.
그런데 문제는 이렇게 반복해서 HTML 태그를 생성할 때, 생성된 HTML 태그 속성에서 name 은 같아도 되지만, id 는 모두 달라야 한다. 따라서
타임리프는 체크박스를 each 루프 안에서 반복해서 만들 때 임의로 1 , 2 , 3 숫자를 뒤에 붙여준다. -->
</div>
멀티 체크박스의 html thymeleaf 기술방법은 위와 같다.
멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있는데
HTML 태그를 반복해서 생성할 때 name
은 태그에서 같아도 되는데 id
까지 같아버리면 안된다
따라서 타임리프는 체크박스를 each
루프 안에서 반복해서 만들 때 임의로 숫자를 뒤에 붙여준다.
HTML의 id
가 타임리프에 의해 동적으로 생성되는 경우,label
의 대상이 되는 id
값을 임의로 지정하면 안된다.
타임리프는 ids.prev()
, ids.next()
의 제공을 통해 동적으로 생성되는 id값을 사용할 수 있도록 한다.
라디오 버튼
라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용할 수 있다.
@ModelAttribute("itemTypes")
public ItemType[] itemTypes(){
return ItemType.values();
}
위 ModelAttribute
또한 모든 컨트롤러에서 이용될 것이기 때문에 따로 추가했다.ItemType.values()
를 사용하면 모든 ENUM의 정보를 배열로 반환한다.
<!-- 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>
실행 로그
item.itemType=FOOD: 값이 있을 때
item.itemType=null: 값이 없을 때
체크박스의 경우 체크해제시 아무 값이 안넘어가서 히든필드가 필요했는데
라디오버튼은 이미 선택이 되어있다면 수정시에도 반드시 하나를 선택하도록 되어있어서
체크 박스와 달리 별도의 히든 필드를 사용해야할 이유가 없다.
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values}"
스프링 EL문법으로 ENUM을 직접 사용할 수도 있다. ENUM에 values()
를 호출하면 해당 enum의 모든 정보가 배열로 반환된다.
그런데 이렇게 사용하는 경우 패키지 변경에서 자바 컴파일러가 thymeleaf의 변경까지 오류로 못잡아내기 때문에 권장하지 않는다.
셀렉트 박스
@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>
<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>
다음과 같이 사용한다.
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 2편' 카테고리의 다른 글
[스프링 웹 MVC 2편] 006. Validation (2) (0) | 2023.08.15 |
---|---|
[스프링 웹 MVC 2편] 005. Validation (1) (0) | 2023.08.13 |
[스프링 웹 MVC 2편] 004. 국제화 메세지 (0) | 2023.08.11 |
[스프링 웹 MVC 2편] 002. 타임리프 기본기능(2) (0) | 2023.08.11 |
[스프링 웹 MVC 2편] 001. 타임리프 기본기능 (0) | 2023.08.05 |