반응형

타임리프 스프링 통합

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 라고 검색)

https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-applicationproperties.html#common-application-properties-templating

 

 

입력 및 수정 폼 처리

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&regions=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를 사용하면 값이 있는 경우 선택을 자동으로 처리해줌

반응형

+ Recent posts