타임리프 스프링 통합
1. 메뉴얼
기본 메뉴얼: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
스프링 통합 메뉴얼: https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html
2. 스프링 통합으로 추가되는 기능들
• 스프링의 SpringEL 문법 통합
• ${@myBean.doSomething()} 처럼 스프링 빈 호출 지원
• 편리한 폼 관리를 위한 추가 속성
◦ th:object (기능 강화, 폼 커맨드 객체 선택)
◦ th:field , th:errors , th:errorclass
• 폼 컴포넌트 기능
◦ checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원
• 스프링의 메시지, 국제화 기능의 편리한 통합
• 스프링의 검증, 오류 처리 통합
• 스프링의 변환 서비스 통합(ConversionService)
3. 설정 방법
• 타임리프 템플릿 엔진을 스프링 빈에 등록하고, 타임리프용 뷰 리졸버를 스프링 빈으로 등록하는 방법 메뉴얼
https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#the-springstandarddialect
https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#views-and-viewresolvers
• 스프링 부트는 이런 부분을 build.gradle에 다음 한줄을 넣어주면 모두 자동화 해줌
• Gradle은 타임리프와 관련된 라이브러리를 다운로드 받고, 스프링 부트는 앞서 설명한 타임리프와 관련된 설정용
스프링 빈을 자동으로 등록해줌
※ build.gradle 파일
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' |
• 타임리프 관련 설정 변경 메뉴얼(application.properties에 설정추가)
- 스프링 부트가 제공하는 타임리프 설정메뉴얼 (thymeleaf 라고 검색)
입력 및 수정 폼 처리
1. 설명
• th:object : 커맨드 객체를 지정
• *{...} : 선택 변수 식이라고 한다. th:object 에서 선택한 객체에 접근
• th:field : HTML 태그의 id , name , value 속성을 자동으로 처리해줌
- 렌더링 전
<input type="text" th:field="*{itemName}" />
- 렌더링 후
<input type="text" id="itemName" name="itemName" th:value="*{itemName}" />
2. 등록 폼 예제
2.1 controller 소스
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "form/addForm";
}
2.2 Item 객체
@Setter @Getter
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
2.3 thyemleaf 소스
<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>
<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 가 자동으로 만들어줌
- 렌더링 전
<input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
- 렌더링 후
<input type="text" id="itemName" class="form-control" placeholder="이름을 입력하세요" name="itemName" value="">
3. 수정 폼 예제
3.1 controller 소스
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "form/editForm";
}
3.2 thyemleaf 소스
<form action="item.html" th:action th:object="${item}" method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" th:field="*{id}" class="form-control" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}" class="formcontrol">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" th:field="*{price}" class="form-control">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" th:field="*{quantity}" class="formcontrol">
</div>
• 수정 폼의 경우도 입력폼과 마찬가지로 id , name , value 를 모두 신경써야 했는데, 많은 부분이 th:field로 자동으로 처리됨
• 수정 폼 변화
- 렌더링 전
<input type="text" id="itemName" th:field="*{itemName}" class="form-control">
- 렌더링 후
<input type="text" id="itemName" class="form-control" name="itemName" value="itemA">
요구사항 추가
1. 요구사항 목록
판매 여부 | 등록 지역 | 상품 종류 | 배송 방식 |
• 판매 오픈 여부 (체크 박스) |
• 서울, 부산, 제주 (다중 체크박스로 ) |
• 도서, 식품, 기타 (라디오버튼) |
• 빠른배송, 일반배송, 느린배송 (셀렉트 박스, 드롭다운) |
2. 요구사항 예시 이미지
3. 객체 생성
3.1 상품 종류(ItemType) enum객체 생성
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;
}
}
3.2 배송 방식(DeliveryCode)객체 생성
package hello.itemservice.domain.item;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* FAST : 빠른배송
* NORMAL : 일반 배송
* SLOW : 느린 배송
*/
@Data
@AllArgsConstructor
public class DeliveryCode {
private String code;
private String displayName;
}
3.3 상품(Item)객체에 필드 추가
package hello.itemservice.domain.item;
import java.util.List;
import lombok.Data;
@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;
}
}
체크박스 - 단일 1 (html 구현)
1. 일반적인 체크박스의 데이터 확인 예제
1.1 html 소스
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
1.2 controller 소스
@PostMapping("/add")
public String addItem(Item item, RedirectAttributes redirectAttributes) {
log.info("item.open={}", item.getOpen());
...
}
• form에서 controller로 데이터가 넘어와서 어떻게 바인딩 되는지 log확인
- 체크박스를 체크 한 후 데이터 확인
item.open=true
- 체크박스를 체크하지 않은 후 데이터 확인
item.open=null
• 체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어감
• 스프링은 on 이라는 문자를 true 타입으로 자동 변환해줌
2. 체크 박스를 선택하지 않을 때 이슈
2.1 이슈 상황
• HTML에서 체크 박스를 선택하지 않고 폼을 전송하면 open이라는 필드 자체가 서버로 전송되지 않음.
• 수정의 경우 상황에 따라서 이 방식이 문제가 될 수 있음
• 사용자가 의도적으로 체크되어 있던 값을 체크를 해제해도 저장 시 아무 값도 넘어가지 않기 때문에,
서버 구현에 따라서 값이 오지 않은 것으로 판단해서 값을 변경하지 않음
2.2 해결
• 스프링 MVC는 히든 필드를 하나 만들어서 _open 처럼 기존 체크 박스 이름 앞에 언더스코어( _ )를 붙여서 전송하면 체크를 해제했다고 인식할 수 있음
• 히든 필드는 항상 전송되어 체크를 해제한 경우 open 필드는 전송되지 않고 _open 만 전송되는데 이를 통해 스프링 MVC는 체크를 해제했다고 판단함.
3. 히든필드 예제
<!-- 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>
• 체크 박스 체크 후 form 데이터 전송
- 로그 : item.open=true
- form에서 전송된 데이터 : open=on&_open=on
- 체크 박스를 체크하면 스프링 MVC가 open 에 값이 있는 것을 확인하고 사용함
- 이때 _open은 무시
• 체크 박스 미체크 후 form 데이터 전송
- 로그 : item.open=false
- form에서 전송된 데이터 : _open=on
- 체크 박스를 체크하지 않으면 스프링 MVC가 _open만 있는 것을 확인하고, open 의 값이 체크되지
않았다고 인식
체크박스 - 단일 2 (Thyemleaf로 구현)
1. 타임리프로 체크박스의 데이터 확인 예제
※ thyemleaf 소스
<!-- single checkbox -->
<div>판매 여부</div>
<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>
※ HTML 렌더링 결과
<!-- 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>
• 타임리프를 사용하면 체크 박스의 히든 필드와 관련된 부분도 함께 해결해줌
• HTML 생성 결과를 보면 히든 필드 부분이 자동으로 생성되어 있음
2. 상품 상세 페이지 적용
※ thyemleaf 소스
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" th:field="${item.open}" class="formcheck-input" disabled>
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
※ HTML 렌더링 결과
<!-- single checkbox -->
<div class="form-check">
<input type="checkbox" id="open" class="form-check-input" disabled name="open" value="true" checked="checked">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
• 타임리프의 checked="checked"
- 체크 박스에서 판매 여부를 체크해서 저장하면, 조회시에 checked 속성이 자동으로 추가 되어 있음
- 타임리프의 th:field를 사용하면 값이 true 인 경우 체크를 자동으로 처리해줌
• 상품 상세 페이지에는 checkbox에 disabled 처리가 되어있어서 hidden 필드를 자동으로 생성해주지 않음
3. 상품 수정 페이지 적용
• 상품 상세 페이지와 동일 하지만 렌더링 결과에 hidden 필드를 자동으로 생성해줌.
체크 박스 - 멀티
1. controller에 체크 박스 관련 데이터 로직 추가
@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)에 담김
2. 상품 추가 폼 thyemleaf
<!-- 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:for="${#ids.prev('regions')}"
- 멀티 체크박스는 th:for를 통해 같은 이름의 여러 체크박스를 만들 수 있음
- 반복해서 HTML 태그를 생성할 때, 생성된 HTML 태그 속성에서 name 은 같아도 되지만, id 는 모두 달라야 함
- 타임리프는 체크박스를 each 루프 안에서 반복해서 만들 때 임의로 1 , 2 , 3 숫자를 뒤에 붙여줌
• each로 체크박스가 반복 생성된 렌더링 결과(id 뒤에 숫자가 추가)
<input type="checkbox" value="SEOUL" class="form-check-input" id="regions1" name="regions">
<input type="checkbox" value="BUSAN" class="form-check-input" id="regions2" name="regions">
<input type="checkbox" value="JEJU" class="form-check-input" id="regions3" name="regions">
※ HTML의 id가 타임리프에 의해 동적으로 만들어지기 때문에 <label for="id 값"> 으로 label의 대상이 되는 id 값을 임의로 지정할수 없으나, 타임리프는 ids.prev(...) , ids.next(...)을 제공해서 동적으로 생성되는 id 값을 사용할 수 있도록 함.
3. 멀티 체크 박스 form 전송 시 controller 처리
• 서울, 부산 선택 후 form 데이터 전송 시
- 로그: item.regions=[SEOUL, BUSAN]
- form에서 전송된 데이터 : regions=SEOUL&_regions=on®ions=BUSAN&_regions=on&_regions=on
• 지역 선택 없이 form 데이터 전송 시
- 로그: item.regions=[]
- form에서 전송된 데이터 : _regions=on&_regions=on&_regions=on
※ _regions는 앞선 checkbox 예제와 같이 웹 브라우저에서 체크를 하나도 하지 않았을 때, 클라이언트가 서버에 아무런 데이터를 보내지 않는 것을 방지함.
라디오 버튼
1. controller에 라디오 버튼 관련 데이터 로직 추가
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
}
• itemTypes를 등록 폼, 조회, 수정 폼에서 모두 사용하므로 @ModelAttribute사용
• ItemType.values() 를 사용하면 해당 ENUM의 모든 정보를 배열로 반환함 예) [BOOK, FOOD, ETC]
2. 상품 추가 폼 thyemleaf
<!-- 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>
3. 라디오 버튼 form 전송 시 controller 처리
• 음식 선택 후 form 데이터 전송 시
- 로그: item.itemType=FOOD
• 라디오버튼 선택 없이 form 데이터 전송 시
- 로그: item.itemType=null
※ 라디오 버튼은 이미 선택이 되어 있다면, 수정 시에도 항상 하나를 반드시 선택하도록 되어 있으므로 체크 박스와 달리 별도의 히든 필드를 사용할 필요가 없음.
※ 체크 박스와 마찬가지로 라디오 버튼에서 상품 종류를 체크해서 저장하면, 조회시에 checked 속성이 자동으로 추가 되어 있음
※ 타임리프의 th:field를 사용하면 값이 true인 경우 체크를 자동으로 처리해줌
4. 타임리프에서 ENUM 직접 사용하기
• 소스
</div th:each="type : ${t(hello.itemservice.domain.item.itemtype).values()}">
|
- 스프링EL 문법으로 ENUM을 직접 사용할 수 있음
- ENUM에 values() 를 호출하면 해당 ENUM의 모든 정보가 배열로 반환됨
- 이렇게 사용하면 ENUM의 패키지 위치가 변경되거나 할때 자바 컴파일러가 타임리프까지 컴파일 오류를 잡을 수 없으므로 추천하지는 않음
셀렉트 박스
1. controller에 셀렉트 박스 관련 데이터 로직 추가
@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를 등록 폼, 조회, 수정 폼에서 모두 사용하므로 @ModelAttribute 사용
2. 상품 추가 폼 thyemleaf
<!-- 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>
• 렌더링 결과
<!-- SELECT -->
<div>
<div>배송 방식</div>
<select class="form-select" id="deliveryCode" name="deliveryCode">
<option value="">==배송 방식 선택==</option>
<option value="FAST">빠른 배송</option>
<option value="NORMAL">일반 배송</option>
<option value="SLOW">느린 배송</option>
</select>
</div>
※ 체크 박스와 마찬가지로 셀렉트 박스에서 배송 방식을 체크해서 저장하면, 조회시에 selected="selected" 속성이 자동으로 추가 되어 있음
※ 타임리프의 th:field를 사용하면 값이 있는 경우 선택을 자동으로 처리해줌
'일상의 흔적 > Study' 카테고리의 다른 글
인프런 스프링 MVC 2 (백엔드 웹개발 활용 기술) : validation - 4 (0) | 2023.03.19 |
---|---|
인프런 스프링 MVC 2 (백엔드 웹개발 활용 기술) : 메세지, 국제화 - 3 (0) | 2023.03.19 |
인프런 스프링 MVC 2 (백엔드 웹개발 활용 기술) : Thyemleaf 기본기능 - 1 (0) | 2023.03.18 |
인프런 스프링 MVC 1 (웹개발 핵심 기술) : 스프링 MVC 웹페이지 만들기 - 7 (0) | 2023.03.18 |
인프런 스프링 MVC 1 (웹개발 핵심 기술) : 스프링 MVC 기본기능 - 6 (0) | 2023.03.17 |