스프링 통합으로 추가되는 기능들
설정 방법
타임리프 템플릿 엔진을 스프링 빈에 등록하고, 타임리프용 뷰 리졸버를 스프링 빈으로 등록하는 방법
하지만 이 귀찮은 작업들을 부트는 모두 자동화해준다.
build.gradle에 implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
를 넣어주면 자동으로 Gradle이 타임리프와 관련된 라이브러리를 다운받고 타임리프와 관련된 설정용 스프링 빈을 자동으로 등록해준다.
참고로 타임리프 관련 설정을 변경하고 싶다면 아래 링크를 찾아서 스프링 부트가 제공하는 타임리프 설정(thymeleaf 검색 필요)을 찾아보고 입맛에 맞게 세팅하면 된다.
https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-applicationproperties.html#common-application-properties-templating
Model
를 함게 넘겨줘야한다. 이를 command 객체
라고 한다.@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "form/addForm";
}
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>
<form\>
에 th:object
가 명시가 되어있다면 <form\>
내부에 있는 <input\>
의 th:field
값을 적을 때 조금 더 간편하게 작성할 수 있다. ${item.itemName}
-> *{itemName}
으로 바꿀 수 있다.<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>
참고로 앞서 각각의 컨트롤러에 들어가던 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}")
...
@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 영역에 생성을 해놓고 불러다 쓰는게 좋을 것이다.
왜냐하면 컨트롤러의 함수가 실행이 될 때 마다 계속 생성이 되기 때문이다.
따라서 동적으로 변화하는 값이 아니라면 어딘가에 생성해 두고 불러다 쓰는 것이 좋다.
<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;
}
}
<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>
<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>
@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하여 넘겨준다.