728x90
타임리프는 스프링 없이도 동작하지만, 스프링과 통합을 위한 다양한 기능을 편리하게 제공한다.
타임리프 메뉴얼
- 기본 메뉴얼: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
- 스프링 통합 메뉴얼: https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html
1. 스프링 통합 - 타임리프 추가기능
- 스프링 SpringEL 문법
${@myBean} 스프링 빈 호출 지원 - form 관리 추가 속성
- th:object - 기능 강화, form command 객체
- th:field, th:errors, th:errorclass
- form 컴포넌트 기능 - checkbox, radio button, list
- 메시지, 국제화 기능 통합 : 스프링 mvc2 - 1 메시지, 국제화
- 검증 오류 처리 통합 : 스프링 mvc2 - 2 검증 1 (Validation)
- 변환 서비스 통합 : 스프링 mvc2 - 8. 스프링 타입 컨버터
- 설정방법
# build.gradle
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
- 설정 변경 하고 싶을 때
# application.properties
https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application- properties.html#common-application-properties-templating
위 사이트에서 필요 부분 추가
2. 입력 폼 처리
- form 관리 추가 속성
- th:object
- th:field, th:errors, th:errorclass
- th:object : 커멘드 객체를 지정한다. form에 object가 연결되는 것을 command 객체라고 한다.
- *{...} : 선택 변수 식, th:object 에서 선택한 객체에 접근
- th:field : html tag의 id, name, value 속성을 자동으로 처리
- 예시
<input type="text" th:field="*{itemName}" />
<input type="text" id="itemName" name="itemName" th:value="*{itemName}">
- html 기능 참고
- 서버기준
- map 형식 {key:value} == {key=value} == {name=value}
더보기
id: label 명과 동일할 시 서로 연결이 됨, id는 1개 여야한다.
name: form의 이름
value: UI가 처음 나올때 기본적으로 써져있는 값 - 이걸 지우면 placeHolder 나타남
placeHolder: 이름 작성칸에 아무 값도 없을 시 나타나는 글
서버기준(중요)
map 형식 {key:value} == {name:value}
2.1. 등록 form
- FormItemController
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
/* th:object 를 사용하기 위해 빈객체 생성을 해야하지만 생성 비용 크지 않고
html form에서 id, value 작성시 컴파일 에러를 통해서 실수를 잡아준다.
th: object , th: field 사용 가능해진다.
*/
return "form/addForm";
}
- form/addForm.html
<form action="item.html" th:action th:object="${item}" method="post">
<!-- th:object="${item}"은 model로 부터 넘어온 객체 -->
<div>
<label for="itemName">상품명</label>
<!--/* <input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">*/-->
<!--/* <input type="text" id="itemName" th:field="${item.itemName}" class="form-control" placeholder="이름을 입력하세요">*/-->
<!--/* <input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">*/-->
<input type="text" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
</div>
<div>
<label for="price">가격</label>
<input type="text" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
</div>
- th:object="${item}" : <form> 에서 사용할 객체를 지정. 선택 변수 식( *{...} )을 적용 가능
- th:field
- *{itemName} : th:object가 있을 경우 사용 가능
- ${item.itemName} : th:object가 없는 경우 사용
- 기능
- id, name, value 속성을 모두 자동으로 만들어 준다. - value에는 실제 값이 들어간다.
- value=""인 이유: 해당 Item이 비어있는 객체이기 때문
- 렌더링 후 소스코드
source code
<form action="" method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" class="form-control" placeholder="이름을 입력하세요" id="itemName" name="itemName" value="">
</div>
<div>
<label for="price">가격</label>
<input type="text" class="form-control" placeholder="가격을 입력하세요" id="price" name="price" value="">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" class="form-control" placeholder="수량을 입력하세요" id="quantity" name="quantity" value="">
</div>
3. 요구사항 추가
3.1. 추가 방법
원리
- Item이란 class가 도메인이다.
= item의 정보를 가지고 서비스를 제공하는 것- 해당 요구사항은 Item의 정보를 추가하는 것이고 해당 title들을 item class에 담아야한다.
- 그래야지 나중에 controller || db 에서 item 정보들을 사용할 수가 있다.
- Item에 담을 정보들이 단순 자료형이면 Item class에서 변수 지정으로 설정만 하면 되지만 위의 요구사항처럼 하위 정보들 중에서 선택을 하는 것이면 선택지에 관련된 class, Enum 을 생성하거나 controller에 만들어서 넣는 경우가 있다.
- class,ENUM 생성하는 경우: 분리를 시켜서 향후 수정이나 다른 곳에서도 사용이 가능
- controller에 생성하는 경우: 굳이 분류를 할 필요없는 고정된 값이라고 판단되는 경우
- ItemRepository
- 현재 project는 규모가 작아서 repository에 직접 service까지 구현을 한다.
3.2. 도메인 선택지 종류
- Enum, class, String으로 한 이유는 여러가지 방법을 알기 위함. 큰 이유 없음
- Boolean인 판매여부는 자료형 이므로 따로 추가할 필요 X
4. 4가지 add -edit 방식
- 판매여부 - 체크박스 단일: 한가지 선택지 밖에 없고 선택해도 되고 안해도 된다.
- 등록지역 - 체크박스 멀티: 여러 선택지 중 다중 선택
- 상품 종류 - 라디오 버튼: 여러 선택지 중 1가지만 선택, 초기에 선택하지 않는 이상 무조건 선택된다.
- 배송방식 - 셀렉트 박스: 여러 선택지 중 1가지만 선택 선택해도 되고 안해도 된다.
- item 도메인
package hello.itemservice.domain.item;
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
private Boolean open; // 판매 여부
private List<String> regions; // 등록지역
private ItemType itemType; // 상품 종류
private String deliveryCode; // 배송 방식
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- item = 주 도메인
- item이란 domain은 우리가 필요로 하는 컨트롤러, 서비스, 리포지토리에게 중심이 되는 객체이자 data 저장 객체
- 판매 여부, 등록지역, 상품 종류, 배송 방식 의 iv는 생성자로 초기화 하는 것이 아니라 처음 domain에서 점차 추가됨 고려해서 분리한다.
4.1. 판매여부 - 체크박스 단일(자료형)
<hr class="my-4">
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" th:field="*{open}" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
- item의 private Boolean open 에 해당 checkBox의 값이 들어가는 경로
- form 전송시, {key:value} -> {name: value} 형식으로 server에서 값을 받게 된다.
check box 경우- checked 될시 value = on
- checked 안될 시 value 자체가 넘어가지 않는다.
- server에서는 @ModelAttribute가 form으로 부터 받은 값을 item 객체에 넣는다.
check box 경우- on 같은 경우 string으로 인지하지 않고 true로 저장한다.
- check하지 않은 경우 open이란 field 자체가 넘어오지 않아서 null 이 된다.
- Boolean은 객체 타입이다. 그래서 null이 가능하다.
- form 전송시, {key:value} -> {name: value} 형식으로 server에서 값을 받게 된다.
- th:field=*{open}
- input, label,value 생성
- check 박스 경우 value가 on일 시, checked attribute까지 자동으로 생성
- 체크박스와 스프링
- open=null은 수정 페이지에서 문제
- 체크 된 것을 체크 해제 할때 request params의 반환 값이 null이다.
- 결국 open의 value값이 넘어가지 않아서 기존 open=true 값 변경이 불가하다.
정리
- 체크하면
- 서버로는 open=on 이 넘어간다.
- on->true로 되서 item.open=true 가 된다.
- on->true로 되서 html value 값이 지정되고 th:field = *{open}가 value=on 일 경우, checked=checked 까지 자동 생성
- 체크안하면
- 서버로는 open=null 이 넘어간다. 사실상 넘어가는게 없다는 뜻이다.
해결 방안(트릭 사용)
- type="hidden" name="_open" value="on"
- 스프링 MVC는 히든 필드만 넘어올시, check 해제되었다고 판단한다.
- hidden: 항상 server로 값을 보낸다는 뜻
- 체크박스 체크: open=on&_open=on 값이 서버로 넘어가고 item.open=true 된다. _open=on 무시
- 체크박스 체크X: _open=on 값이 서버로 넘어가고 item.open=false 된다
- 결론적으로 null과 같이 아무 값도 넘기지 않고 true / false로만 전달이 가능하다.
- 판매여부 - 체크박스 단일 치트키
- th:field="*{open}"
- 자동으로 id, value, name, hidden, checked(checkbox value가 on일 경우) 생성
- input의 hidden tag 도 자동으로 추가한다.
- th:field="*{open}"
// 치트키 사용
<input type="checkbox" th:field="*{open}" class="form-check-input">
// 원래 노가다 방식 - item.open=true인 경우
<input type="checkbox" id="open" name="open" value="on" checked="checked" class="form-check-input">
<input type="hidden" name="_open" value="on"/>
<label for="open" class="form-check-label">판매 오픈</label>
4.2. 등록지역 - 체크박스 멀티(controller에서 생성)
- 주의: UI에서 선택할 목록이 Map 인 것이고 실제 선택한 것들은 Item에서 List로 저장을 한다.
// Item.class
private List<String> regions; // 등록지역
// FormItemController.class
@ModelAttribute("regions") // model.modelAttribute("name", xxx)의 "name" 부분이다.
// @ModelAttribute 는 별도의 method에 담기면 해당 Controller(FormItemController.class) 호출할 때마다 "regions"란 key로 region이 model에 들어감
public Map<String, String> regions(){
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
return regions;
}
@ModelAttribute 기능
- request로 들어온 Data를 Item에 담고 Model에 넣을 때 사용. - params로 들어가 있었다.
- Method에 적용할 때 해당 컨트롤러를 요청시 method 반환한 값이 자동으로 model에 담기게 된다.
- 그냥 view 로 갈 때 regions 객체도 항상 보내진다고 보면된다.
<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-inline">
<label th:for="${#ids.prev('regions')}"
th:text="${region.value}" class="form-check-label">지역</label>
</div>
</div>
thymeleaf 기능
- ${region.key}: Map의 key 추출
- ${region.value}: Map의 value 추출
- ${#ids.prev('region')}: loop 형식의 id (동적인 id)를 넣기 위해서 thymeleaf가 지원하는 기능 regions1, regions2, regions3 .... 동적으로 확장 가능
- *{regions}
- item.regions와 연관 짓기 위한 이름이지 @model로 온 regions가 아니다.
- 위의 checkbox 또한 _regions라는 hidden tag를 생성한다.
- 앞서 설명한 hidden과 동일한 방식으로 작동한다.
- 서울, 부산 check : regions=SEOUL&_regions=on®ions=BUSAN&_regions=on&_regions=on
- check X : _regions=on&_regions=on&_regions=on
4.3. 상품 종류 - 라디오 버튼(Enum)
- 하나 선택하는 순간 무르기 없다
- 처음부터 선택안하거나 하나 무조건 선택하거나 결정해야함
- 하나만 선택 가능하다.
- 하나는 무조건 선택이 되므로 hidden field를 사용할 필요가 없다.
- item.itemType=null 이 나와도 괜찮다. 수정시 아예 선택 안하거나 무조건 하나 선택하거나 이후에는 무조건 다른 하나를 선택해야만 하기 때문
package hello.itemservice.domain.item;
public enum ItemType {
Book("도서"), FOOD("음식"), ETC("기타");
private final String description;
ItemType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
// enum은 values() 사용시 배열로 반환한다.
}
- ItemType.values() : enum의 values는 배열을 반환 : [BOOK, FOOD, ETC]
<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"></label>
</div>
thymeleaf 기능
- ${type.name()}: enum의 field 명 : BOOK, FOOD, ETC
- ${type.description}: enum field의 description : 도서, 음식, 기타
4.4. 배송방식 - 셀렉트 박스(class-객체 생성)
@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;
}
<div>배송 방식</div>
<select th:field="*{deliveryCode}" class="form-select">
<option value="">==배송 방식 선택==</option>
<option th:each="deliveryCode : ${deliveryCodes}"
th:value="${deliveryCode.code}" // fast, normal, slow
th:text="${deliveryCode.displayName}"> // 빠른 배송, 일반 배송, 느린 배송
</option>
</select>
- select
- option: select 할 옵션들
- html 렌더링 후에는 selected property가 선택된 option에 생성된다.
- 서버에 전달 되는 {key:value} -> {name: 빠른배송 || 일반 배송 || 느린 배송}
- 선택된 옵션에 관해서 th:field는 selected를 자동 주입해준다.
이전 발행글 : 1. 타임리프 - 기본기능