1. 타임리프 스프링 통합
타임리프는 일단 메뉴얼을 크게 두 가지를 제공한다.
- 기본 메뉴얼: Tutorial: Using Thymeleaf
- 스프링 통합 메뉴얼: Tutorial: Thymeleaf + Spring
기본 메뉴얼은 순수한 타임리프, 즉, 스프링 없이 타임리프를 사용할 때 그리고, 앞에서 살펴본 타임리프의 기본적인 기능이 설명된 메뉴얼이다. 그리고 스프링 통합 메뉴얼이라는 것이 있다. 이건 타임리프와 스프링을 통합해서 지원하는 기능들을 가지고 메뉴얼을 만든거다.
타임리프는 스프링이 없어도 동작한다. 하지만 스프링 통합을 위한 다양한 기능을 제공한다. 그리고 이런 부분이 스프링으로 백엔드를 개발하는 개발자 입장에서 타임리프를 사용하는 이유다. 스프링 통합으로 추가되는 기능은 다음과 같다.
- 스프링의 SpringEL 문법 통합
- ${@MyBean.doSomething()}처럼 스프링 빈 호출 지원
- 편리한 폼 관리를 위한 추가 속성 → th:object, th:field, th:errors, th:errorclass
- checkbox, radio button, List 등을 편리하게 사용할 수 있는 폼 컴포넌트 기능 제공
- 스프링의 메시지, 국제화 기능의 편리한 통합
- 스프링의 검증, 오류 처리 통합
- 스프링의 변환 서비스 통합 (ConversionService)
이번에는 폼과, 폼 컴포넌트 기능들에 대해 알아볼 것이다. 그러면 타임리프를 스프링에서 쓰려면 어떻게 해야할까? 이걸 할려면 타임리프용 뷰 리졸버를 스프링 빈으로 등록해야 한다. 하지만 스프링 부트는 이런 부분을 모두 자동화해준다. "build.gradle"에 다음 내용을 작성하면 Gradle은 타임리프와 관련된 라이브러리를 다운받고, 스프링 부트는 타임리프와 관련된 설정용 스프링 빈을 자동으로 등록한다.
- implementation 'org.springframweok.boot:spring-boot-starter-thymeleaf'
타임리프 관련 설정을 변경하고 싶다면 아래를 참고해서 application.properties에 추가하자.
2. 입력 폼 처리
이전에 작성한 코드의 폼 코드를 (MVC-1 편에 마지막에 작성한 웹 페이지 코드) 타임리프가 제공하는 입력 폼 기능을 활용해 개선해보자. 일단 편리한 폼 처리를 위한 추가 속성에는 다음과 같은 것들이 있다.
- th:object: 커맨드 객체를 지정한다.
- *{...}: 선택 변수 식이다. "th:object"에서 선택학 객체에 접근한다.
- th:field: html 태그의 id, name, value 속성을 자동으로 처리한다.
일단 컨트롤러 코드부터 살펴보자.
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "form/addForm";
}
// Item class
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
아래는 해당 컨트롤러가 불러오는 html 파일의 일정부분을 가져와서 수정한 코드다.
<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:action: form data를 보낼 url을 설정하는 태그다.
- 일단 th:object="${item}"을 form 태그에 추가한다. th:object는 <form> 태그에서 사용할 객체를 지정하며, 선택 변수식을 적용할 수 있다.
- <input type="text" id="itemName" name="itemName" class=class="form-control" placeholder="이름을 입력하세요"> 라는 태그가 있다. 이때 id="itemName", name="itemName"을 그냥 th:field="item.itemName"으로 쓰면 해당 부분을 타임리프가 알아서 채워준다. 또한 value값도 채워주는데, 해당 item 객체는 아무것도 가지고 있지 않으므로 value에 ""가 들어간다.
- 이때 "item.itemName"를 단순히 "*{itemName}"으로 줄일 수 있다. 이전에 먼저 th:object="${item}"를 form 태그에 작성했다. 이 경우 *{...}안의 내용은 item에 소속되어 있다고 인정된다. 이때 *{...}를 선택 변수식이라고 한다. 그래서 item.itemName으로 된다. 어쨋든 이렇게 사용하면 id, name 속성의 내용을 알아서 채워준다. 단, id 속성은 남겨주자. 왜냐하면 <label> 태그의 for에서 id를 잡아야 하는데 이걸 지우면 인식하지 못한다. 서버에 요청시 문제가 발생하진 않지만, 그냥 코드에서 빨간색으로 오류가 나는 것처럼 표시된다.
해당 html을 서버에 요청하고 코드를 살펴보면 th:field가 id, name, value값을 잘 채워주는 것을 확인할 수 있다.
3. 요구사항 추가
타임리프를 사용해서 폼에서 체크박스, 라디오 버튼, 셀렉트 박스 등을 편리하게 사용하는 방법을 살펴보자. 기존 상품에 다음과 같은 요구 사항을 추가할 것이다.
- 판매 여부, 등록 지역, 상품 종류, 배송 방식을 상품을 등록할 때 추가해서 넣을 것이다. 판매 여부는 체크 박스 하나, 등록 지역은 다중 선택이 가능한 체크 박스, 상품 종류는 라디오 버튼, 배송 방식은 셀렉트 박스로 선택하게 할 것이다.
먼저 상품 종류는 다음과 같이 enum으로 되어있다.
public enum ItemType {
BOOK("도서"), FOOD("음식"), ETC("기타");
private final String description;
ItemType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
배송과 관련된 거는 단순한 자바 코드로 구현했다.
/**
* FAST: 빠른 배송
* NORMAL: 일반 배송
* SLOW: 느린 배송
*/
@Data
@AllArgsConstructor
public class DeliveryCode {
private String code;
private String displayName;
}
그리고 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;
}
}
4. 체크 박스 - 단일1
일단 addForm.html에 다음의 기본 html 코드를 추가하자.
<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>
그러면 지금부터 addForm.html을 서버에서 받아오면 체크할 수 있는 체크 박스가 아래와 같이 생성된다.
이때, 해당 부분을 체크하여 서버에 전송하면 다음과 같이 내용이 전달된다.
체크 상태로 서버에 전송시 open은 위와 같이 "on"으로 전달된다. 반면 체크를 해제한 상태로 보내면 결과는 아래와 같다.
보면 open에 대한 아무런 값도 전달되지 않은 것을 확인할 수 있다. 정리하면 다음과 같다.
- 체크 박스를 체크하면 HTML Form에서 open=on이라는 값이 넘어간다. 이때 Spring은 "on"을 true로 처리한다.
- 체크 박스를 해제한 상태로 서버에 전달되면 open이라는 필드 자체가 서버로 전달되지 않는다.
- 만약 위처럼 웹 브라우저를 통해 request/response를 받아보는 것이 아니라 intellij의 서버 안에서 보고 싶다면 " logging.level.org.apache.coyote.http11=debug"이 내용을 application.properties에 추가해주면 된다.
이와 같이 HTML의 checkbox는 선택이 안되면 클라언트에서 서버로 값을 보내지 않는다. 이런 경우 수정하는 경우 문제가 될 수 있다. 사용자가 과거에 체크를 하고 생성을 했다고 하자. 그럼 사용자가 의도적으로 체크되어 있던 값을 체크 해제 해도 값이 넘어가지 않기 때문에 서버 구현에 따라 값이 오지 않은 것으로 판단해 값을 변경되지 않을 수도 있다. 물론 코딩시 값이 null이면 체크를 안 한 것으로 판단하도록 구현할 수도 있다.
이런 문제를 해결하기 위해 스프링 MVC는 히든 필드 하나를 만들어서 "_open"처럼 기존 체크 박스 이름 앞에 언더바("_")를 붙여서 전송하면 체크를 해제했다고 인식한다. 히든 필드는 항상 전송된다. 만약 기존 체크박스의 체크를 해제한 경우는 open은 전송되지 않고, "_open"만 전송된다. 이런 경우 스프링 MVC는 체크를 해제했다고 판단한다. 즉, 코드를 아래와 같이 바꿔보자.
<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>
이제 체크를 해제하고 post 요청을 서버에 보내보자.
이렇게 히든 필드의 데이터인 "_open"의 데이터만 넘어간다. 이렇게 되면 item.open에 false가 들어가게 된다.
- 체크 박스를 체크한 경우: open에 값이 들어오므로 _open은 무시한다.
- 체크 박스를 해제한 경우: _open만 있는 것을 확인하고 open의 값이 체크되지 않았다고 인식한다.
5. 체크 박스 - 단일2
위와 같이 히든 필드를 계속 추가하는 것은 번거롭다. 타임리프는 이러한 번거로운 작업을 쉽게 도와준다.
<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>
이렇게 th:field만 작성해줘도, 우리가 원하는데로 동작한다. 이때 add페이지의 소스를 보면 다음과 같이 나온다.
뒤에 hidden 필드가 자동으로 추가되어 있다. 이렇게 타임리프는 히든필드를 자동으로 생성해준다.
- 추가적으로 th:field는 {item.open}과 같이 전달된 친구가 true면 checked 속성을 넣어준다. 이 경우 아직 item.open이 초기화되지 않아 기본값인 false가 들어있어 체크가 되어있진 않다. false인 경우 checked 속성을 넣지 않는다.
6. 체크 박스 - 멀티
@ModelAttribute("regions")
public Map<String, String> regions() {
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
return regions;
// model.addAttribute("regions", regions);가 자동으로 들어감.
}
각 메서드(add, edit, list)에서 항상 Map 객체에 3가지 내용을 넣고, 해당 Map 객체를 addAttribute하는 과정을 거쳐야 한다. 이때 우리는 항상 해당 부분 코드가 겹치게 되는데 Spring은 이를 해결해주기 위해 다음과 같은 기능을 제공한다. 위처럼 @ModelAttribute("regions")라고 선언하고 그 아래 로직을 작성하고, model에 추가하고 싶은 객체를 반환해준다면, 다른 메서드에서 해당 객체를 추가하지 않아도 이 컨트롤러를 호출할 때는 반환되는 친구가 addAttribute를 통해 모델에 무조건 담긴다.
- @ModelAttribute: 우리는 등록 폼, 상세화면, 수정 폼에서 모두 서울, 부산, 제주의 체크박스를 추가할 것이다. 이때 해당 정보를 model.addAttribute로 모두 추가해야하고 그러면 중복이 일어난다. 이때 @ModelAttribute를 통해 해결할 수 있다. @ModelAttribute를 이렇게 메서드 위에 붙이면, 해당 메서드가 속한 컨트롤러가 호출될 때마다, model.addAttribute("{선언한 이름}", {반환 객체}) 형식으로 모델에 데이터가 저장된다.
하지만 이게 동적으로 변하지 않는다면 그냥 어디다 생성을 해놓고, 불러오는 방식이 성능상 더 효율적일 것이다. 왜냐하면 해당 컨트롤러가 호출될 때마다 해당 객체를 계속 생성해야하기 때문이다. 근데 위에서 작성한 정도는 성능에 크게 영향이 가진 않는다.
이제 addForm.html에 다음 내용을 추가해보자.
<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>
- *{regions}는 item.regions를 말한다.
- th:value를 통해 value 태그값을 region.key로 설정한다. 따라서 SEOUL, BUSAN, JEJU가 각각 value값이 된다.
- 이때 우리는 label 부분인 서울, 부산, 제주를 클릭해도, 체크 박스가 클릭되도록 할 것이다. 이를 위해 label에서 앞에 있는 체크 박스의 아이디를 알아야 한다. 이를 위해 th:for을 사용한다.
- 그런데 checkbox 부분에 id가 없고 th:field를 통해 id가 region1, region2, region3으로 자동 생성된다. 문제는 label 입장에서는 당장 아이디가 없다는 것이다. 이러한 문제를 해결하기 위해 타임리프는 이렇게 동적으로 아이디가 생성될 때 #ids라는 것을 지원한다. 이때 #ids.prev('regions')를 하면, th:field에 지정한 친구에서 자동으로 생성하는 아이디를 보고, 그 값을 가져와서 넣어준다.
해당 코드의 결과는 아래와 같다.
이후 만약 서울과 부산에 체크하면, item.regions=[SEOUL, BUSAN]이 된다. 만약 아무 지역도 선택하지 않으면 그냥 빈 리스트인 []로 들어간다. 추가적으로 서울, 부산 체크시 아래 정보를 전달한다.
이후 상품 조회 화면에도 아래와 같은 코드를 추가한다.
<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:object를 지정하지 않았다. 따라서 th:field 사용시 *{regions}를 사용하지 않고 위처럼 ${item.regions}를 사용한다.
- 조회에서는 체크 박시 표시를 못하도록 disabled 속성을 추가한다.
상품 조회시 아래와 같이 해당 부분이 올바르게 출력된다.
7. 라디오 버튼
라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용할 수 있다. 이전에 만들었던 자바 ENUM 타입인 ItemType을 사용한다. 일단 @ModelAttribute를 통해 model에 속성 하나를 추가하자.
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
}
- 위와 같이 enum 타입의 .values를 사용하면 enum에 있는 모든 값들을 배열로 넘겨준다. 즉, ItemType[] 형식으로 넘겨준다. 즉, BOOK, FOOD, ETC 세 가지가 들어간다.
그리고 아래 코드를 addForm에 추가한다.
<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>
- ENUM 타입에 name()을 하면, 해당 타입의 이름을 String으로 반환해준다. 즉, 각각 BOOK, FOOD, ETC가 String으로 변환되어 들어간다.
localhost에 접속해보면 아래와 같이 radio 버튼이 올바르게 출력된다.
해당 부분의 소스를 보면 다음과 같다.
만약 도서를 선택하면, item.itemType=BOOK이 된다. 아무것도 체크하지 않았다면, item.itemType=NULL이 된다. 추가적으로 음식을 선택할 시, 아래와 같은 정보를 전달한다.
8. 셀렉트 박스
셀렉트 박스도 라디오 버튼과 비슷하게 여러 선택지 중에 하나를 선택할 때 사용한다. 이번에는 자바 객체를 활용해 개발해보자. 이전에 만든 DeliveryCode를 클래스를 사용할 것이다. 일단 model에 deliveryCodes를 추가하자.
@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;
}
그리고, addForm에는 아래 코드를 추가하자.
<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 박스가 나온다.
소스를 보면 다음과 같다.
만약 빠른 배송을 선택했다면, 아래 정보를 전달한다.
만약 아무것도 선택하지 않으면 아래와 같이 아무것도 전달하지 않는다.
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'BackEnd > Spring' 카테고리의 다른 글
[Spring DB-1] JDBC 이해 (0) | 2023.11.15 |
---|---|
[Spring MVC-2] 메시지, 국제화 (0) | 2023.11.10 |
[Spring MVC-2] 타임리프 - 기본 기능 (0) | 2023.09.25 |
[Spring MVC-1] 스프링 MVC - 웹 페이지 만들기 (0) | 2023.09.24 |
[Spring MVC-1] 스프링 MVC - 기본 기능 (0) | 2023.09.23 |