본문 바로가기
관리자

Programming-[Backend]/Spring

[스프링 웹MVC-2] 3. 타임리프/스프링으로 폼 양식 만들기 - th:object, th:field, 체크박스, 라디오 버튼, 셀렉트 박스

728x90
반응형

 

 

 

form 프로젝트를 생성한다. 강의에서 기초 자료를 받아서 진행한다. MVC 1편에서 상품 관련 폼을 만들던 프로젝트이다.

 

스프링 통합 폼을 실습하기 위해서, 타임리프를 스프링 빈 형태로 등록해야한다. 기존 스프링은 다소 복잡한 과정을 통해 타임리프 템플릿 엔진을 스프링 빈으로 등록해야 하지만, 스프링 부트는 아래 구문으로 build.gradle 파일에 타임리프를 import 해옴으로써 이런 과정을 한번에 처리해준다.

 

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

 

 

 

1. 입력 폼 수정

 

 

상품 등록/수정 폼

 

우선 html 파일에서 객체 정보를 바로 넘길 수 있도록 하기 위해, Controller에서 객체를 넘겨줘야 한다. 이때 객체는 빈 객체여도 된다. 원래 아래의 addForm 메서드에서는 Model 객체를 받지 않았으나, 객체를 넘겨주기 위해 추가했다.

 

java/hello/itemservice/web/form/FormItemController.java

 

1
2
3
4
5
@GetMapping("/add")
    public String addForm(Model model) {
        model.addAttribute("item"new Item());
        return "form/addForm";
    }
cs

 

 

html 파일에서 th:object, th:field 문법을 활용한다. th:object는 form 태그에 작성하여 해당 태그가 Model 객체에서 넘어온 객체의 필드값들을 참조하도록 만든다. 물론 th:object로 지정한 객체는 form 태그 내부에서만 적용된다.

 

그리고 th:field는 말그대로 해당 객체의 필드값들이 매칭되도록 한다. 아래 코드에서 원래는 th:field="${item.itemName}" 이라고 작성해야 하지만, 상위 form 태그에서 item을 정의했으므로 $ 대신 *을 쓰고 필드명만 써주면 된다. th:field 문법을 적용하면 해당 input 태그의 id, name, value 속성을 자동으로 처리해준다. 반복적인 속성을 하나의 속성으로 통합시키는 것이다. id="itemName", name="itemName", value="" 으로 등록된다. 컨트롤러에서 넘겨받은 item 객체가 빈 객체이므로 value값은 빈 값이다. 어쨋든, id, name, value 속성은 삭제해도 된다. 참고로, id 속성까지 삭제하면 상위 label 태그의 for 속성이 에러가 뜨는데, 이것은 IDE의 에러이니 신경쓰지 않아도 된다. 또한 th:field 문법을 사용하면 필드명을 잘못 지정했을 때,( ex. th:field="*{itemNamexxx}" ) 서버를 띄우면 에러가 발생하여 실수한 부분을 바로 잡아낼 수 있다.

 

 

resources/templates/form/addForm.html

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="itemName">상품명</label>
            <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" name="quantity" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
        </div>
cs

 

페이지 소스 일부

 

 

이번에는 상품 수정 폼을 수정해보자. 이번에는 받아오는 item 객체에 필드값들이 지정되어 있으므로, 페이지 소스 보기를 하면 input 태그의 value값도 지정된 것을 확인할 수 있다.

 

resources/templates/form/editForm.html

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="id">상품 ID</label>
            <input type="text"  class="form-control" th:field="*{id}" readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" class="form-control" th:field="*{itemName}">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" class="form-control" th:field="*{price}">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" class="form-control" th:field="*{quantity}">
        </div>
cs

페이지 소스 일부

 

 

단순히 속성 3개를 대체하는 태그라고 생각될 수 있지만, 타임리프로 입력 폼의 태그를 처리하는 방식은 나중에 값을 Validation 하는 부분에서 더 편리한 점을 제공한다. 일단 이런 기능이 있다는 것을 알고 계속 공부해나가자.

 

 

 


 

 

2. 입력 폼 : 체크 박스

 

 

 

체크박스

 

상품 등록 화면에 해당 상품의 판매 가능 여부를 지정하는 '판매 오픈' 체크 박스 기능을 추가한다.

 

addForm.html에는 간단하게 checkbox와 label을 추가해준다.

 

resources/templates/form/addForm.html

 

1
2
3
4
5
6
        <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>
cs

 

그리고 판매 여부를 상품단에서 알 수 있도록 상품의 필드값들을 추가해준다. 판매 여부 외 등록 지역, 상품 종류, 배송 방식 필드는 이후에 사용될 필드이다.

 

java/hello/itemservice/domain/item/Item.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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;
    }
}
cs

 

※ 주의점 : 각 필드의 타입은 null로 떨어지는 경우도 발생할 수 있으므로 primitive type이 아닌 reference type으로 잡아주는 것이 좋다. (boolean -> Boolean)

 

 

ItemType과 deliveryCode는 다음과 같이 작성한다.

 

 

ItemType은 ENUM 타입으로 작성해본다. 프로퍼티 접근법을 사용하기 때문에, getter를 넣어준다.

 

 

 

java/hello/itemservice/domain/item/ItemType.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum ItemType {
 
  BOOK("도서"), FOOD("음식"), ETC("기타");
 
  private final String description;
 
  ItemType(String description) {
    this.description = description;
  }
 
  public String getDescription() {
    return description;
  }
 
}
cs

 

java/hello/itemservice/domain/item/DeliveryCode.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * [code : displayName]
 * FAST : 빠른 배송
 * NORMAL : 일반 배송
 * SLOW : 느린 배송
 */
@Data
@AllArgsConstructor
public class DeliveryCode {
 
  private String code;
  private String displayName;
 
}
cs

 

 

 

이제 임의의 데이터를 넣고 판매 등록 체크박스에 체크를 한 뒤 상품 등록을 진행해보면, Network 탭 상에 open 필드값이 on 으로 들어간 것을 확인할 수 있다. 서버단에서는 Boolean으로 설정했기 때문에, http의 on이라는 문자열을 스프링이 'true' 타입으로 변환해준다. 스프링 타입 컨버터가 이 기능을 수행해주는데, 뒤에서 배우게 된다.

 

HTML 체크박스는 체크를 해제하면 필드값 자체를 넘기지 않는다.

 

만약 체크박스를 선택하지 않고 상품등록을 하면 open이라는 필드값 자체가 전송되지 않는다. 서버단에서 log.info로 조회해보면, 이 값이 null로 조회된다. 이런 이유에서도 필드값을 reference type으로 지정하는 것이 좋다. 

 

1
2
3
4
5
6
7
8
@PostMapping("/add")
    public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
        log.info("item.open={}", item.getOpen());
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status"true);
        return "redirect:/form/items/{itemId}";
    }
cs

 

 

 

만약 클라이언트에서 체크 처리가 되어있던 부분을 해제하고 서버로 요청을 보내더라도, 필드값 자체가 넘어오지 않는다면 서버쪽에서는 체크가 해제된 것을 알 방법이 없을 것이다. 따라서 아래와 같은 스프링의 히든 필드 지정 기능을 사용한다. addForm.html의 코드에 hidden 필드를 추가한다.

 

resources/templates/form/addForm.html

 

1
2
3
4
5
6
7
<div>
            <div class="form-check">
                <input type="checkbox" id="open" name="open" class="form-check-input">
                <label type="hidden" name="_open" value="on" />
                <label for="open" class="form-check-label">판매 오픈</label>
            </div>
        </div>
cs

 

여기서 name 속성을 _open 으로 지정했는데, 이 부분은 input의 name 속성값 앞에 언더바가 추가된 형태가 되어야 한다. 이렇게 설정해주면 체크박스를 해제한채로 요청을 보내면 _open인 hidden 필드만 전송되고, 체크박스를 선택하고 요청을 보내면 hidden 필드는 전송되지 않고 원래대로 open인 필드만 전송된다. 스프링 MVC는 이런 내용을 기반으로 해당 필드값을 true 또는 false로 변경해준다.

 

체크박스 선택 해제 시

 

 

체크박스 선택 시

 

 

 

 

이렇게 스프링부트에서 체크박스의 선택 여부를 구분해준다. 그러나, 체크박스마다 일일이 hidden 속성을 갖는 input 박스를 넣어주는 것은 번거로운 일이다. 따라서 타임리프에서는 이런 기능을 자동화 해준다. 아래에서 살펴보자.

 

 

 

 

 

체크박스 - 체크 여부 확인 지원

 

타임리프의 th:field 속성을 넣어주면 된다. ${item.open} 이라고 작성할 수도 있지만, form 태그에서 th:object를 item으로 지정했으므로 여기서는 *{open} 문법을 사용하였다.

-> th:field가 id, name, value값만 지정해주는 것이 아니라 체크박스에서는 이러한 기능도 지원하기 때문에 편리하고, 많이 사용하는 것이다.

 

resources/templates/form/addForm.html

 

1
2
3
4
5
6
<div>
            <div class="form-check">
                <input type="checkbox" id="open" name="open" th:field="*{open}" class="form-check-input">
                <label for="open" class="form-check-label">판매 오픈</label>
            </div>
        </div>
cs

 

타임리프가 지원하는 기능을 쓰면, 체크박스 미체크시 _open 속성만 전송되고, 체크 시에는 open 속성이 true로 전송된다.

다만 미체크, 체크 시 서버단에서는 false, true로 받는다는 점은 기존과 동일하다.

 

 


 

disabled 속성

상세 페이지에도 판매 오픈 여부를 알 수 있도록 체크박스를 추가한다. 여기서는 체크가 불가능해야하므로, 체크박스에 disabled 속성을 추가해준다. 상단에서 th:object로 item을 정의한 것도 아니므로, ${item.open}으로 지정해준다.

 

resources/templates/form/item.html

 

1
2
3
4
5
6
<div>
        <div class="form-check">
            <input type="checkbox" id="open" name="open" th:field="${item.open}" class="form-check-input" disabled>
            <label for="open" class="form-check-label">판매 오픈</label>
        </div>
    </div>
cs

 

th:field의 checked 속성 자동 처리

그런데, 원래 input 태그에 checked 속성이 들어있으면, 해당 속성의 값이 뭐가 됐든 체크된 상태로 표시된다는 것을 이전 글에서 배웠다. 그러나 위 코드에는 checked 속성을 사용하지 않았는데 체크 표시가 되어있다. 원래는 개발자가 if문 등을 활용하여 체크가 되있으면 조건부로 checked 속성을 넣어주어야 하는데, 이것 또한 th:field를 지정함으로써 타임리프가 자동으로 처리해준다.

 

 

상품 수정에도 추가 해준다. 상품 등록에 사용한 코드를 그대로 복사하면 된다.

그리고 ItemRepository에서 item 객체의 값을 수정해주는 update 메서드에 open 필드값을 set 해주는 코드를 추가한다. 나중 실습을 위해 open 필드값뿐만 아니라 모든 필드값을 set해주도록 한다.

 

 

java/hello/itemservice/domain/item/ItemRepository.java

 

1
2
3
4
5
6
7
8
9
10
public void update(Long itemId, Item updateParam) {
        Item findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
        findItem.setOpen(updateParam.getOpen());
        findItem.setRegions(updateParam.getRegions());
        findItem.setItemType(updateParam.getItemType());
        findItem.setDeliveryCode(updateParam.getDeliveryCode());
    }
cs

 

 


 

 

체크박스 - 다중 선택

 

이제 상품의 등록 지역(region) 정보를 추가할 수 있도록 다중 선택 체크 박스를 추가하는 방법에 대해서 배워본다. 기존에 각 컨트롤러의 메서드에서 상품 정보(item)를 View로 전달하기 위해서, model 객체에 item 객체를 넣어주었다. 지역 정보도 마찬가지로 조회가 필요한 컨트롤러의 각 메서드(등록, 상세, 수정화면)에 아래와 같은 코드를 추가해주어야 한다. LinkedHashMap을 사용하는 것은 서울-부산-제주의 순서가 보장되도록 하기 위함이다.

 

 

java/hello/itemservice/web/form/FormItemController.java

 

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/add")
    public String addForm(Model model) {
        model.addAttribute("item"new Item());
 
        Map<StringString> regions = new LinkedHashMap<>();
        regions.put("SEOUL""서울");
        regions.put("BUSAN""부산");
        regions.put("JEJU""제주");
        model.addAttribute("regions", regions);
 
        return "form/addForm";
    }
cs

 

 

@ModelAttribute로 모든 컨트롤러 메서드의 model에 정보 담기

그러나 메서드마다 이 코드를 넣어주는 것은 중복의 문제가 있다. 그래서 @ModelAttribute의 특별한 기능으로, 컨트롤러 공통 부분에 @ModelAttribute를 선언하여 각 메서드의 model 객체에 공통 정보를 담아줄 수 있는 기능을 활용한다. 따로 model.addAttribute("regions", regions) 코드를 작성할 필요없이, 어노테이션의 속성값으로 "regions"를 주고 model에 추가하고자 하는 객체를 return 해주면 된다.

 

이렇게 작성하면 요청이 올때마다 regions가 생성되는 방식이라 성능 최적화가 되지 못한다. 그래서 원래는 static을 활용하는 등 따로 데이터를 생성해놓고 가져다 쓰는 방식을 사용해야 한다.

 

java/hello/itemservice/web/form/FormItemController.java

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Controller
@RequestMapping("/form/items")
@RequiredArgsConstructor
@Slf4j
public class FormItemController {
 
    private final ItemRepository itemRepository;
    
    @ModelAttribute("regions")
    public Map<StringString> regions() {
        Map<StringString> regions = new LinkedHashMap<>();
        regions.put("SEOUL""서울");
        regions.put("BUSAN""부산");
        regions.put("JEJU""제주");
        return regions;
    }
 
    @GetMapping
    public String items(Model model) {
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items);
        return "form/items";
    }
 
...
cs

 

이제 데이터는 넘어갔으니, View단에서 처리를 해야한다. 상품 등록 폼 부터 시작한다. 다음의 코드를 추가하자.

 

 

resources/templates/form/addForm.html

 

1
2
3
4
5
6
7
        <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>
cs

 

th:value

th:value값을 region의 key값으로 지정해주었다. 그래서 각 요소의 value값이 "SEOUL, BUSAN, JEJU"로 설정되었다. 나중에 체크박스에 체크가 되면, 이 value값을 서버로 전달하게 된다. 뒤쪽의 value="on" 부분은 th:field에 의해 자동으로 생성된 부분이다.

 

th:field

th:each 문을 통해서 반복문을 실행한다. 그러나 내부 th:field의 regions값은 이와 상관없는 값이다. 이 값은 상위의 <form> 태그에서 th:object="${item}" 으로 지정된 item.regions라는 필드값을 호출하는 영역임에 유의하자. 이렇게 th:field값을 item 객체의 필드값으로 지정함으로써, 다중 선택 체크박스의 체크여부값들은 item 객체의 regions 필드값으로 넘어가서 저장되는 것이다.

 

 

#ids.prev

앞선 글 [스프링 웹MVC-2] 1. 타임리프 기본 - 1 : 프로젝트 생성, 대표 문법 공부 에서 배운, 유틸리티 객체와 날짜 부분의 #ids 문법을 사용하고 있다. 소스보기를 해보면, 각 요소의 id값 앞에(prev) regions라는 문자열이 붙은채로 id값이 지정된 것을 확인할 수 있다.

 

체크박스를 클릭하는 것이 아니라, 체크박스와 함께 있는 <label> 요소인 "서울" 등을 클릭해도 체크박스의 선택 여부가 변경되는 것을 볼 수 있다. 원래는 이렇게 하려면 input 태그의 id값과 label 태그의 id값이 일치되야 한다. 그러나 #ids 타임리프 문법을 사용함으로써 이런 문제를 해결할 수 있다. prev의 요소로 지정한 'regions'는 input 태그의 th:field의 regions를 참조해서 id값을 만든다고 하니 이름이 일치되게 하자.

 

 

 

체크 여부에 따른 서버 전달값

 

서울, 부산만 체크하고 요청을 보내면 아래와 같이 Form Data가 전송된다. 3개의 선택지에 대해 hidden 속성인 _regions는 3개 모두 on으로 전송된다. 사실 서버에서는 _regions는 1개만 와도 상관없는 값이다. 체크한 서울, 부산은 그 value 값이 regions필드로 전송된다. 따라서 서버에서도 regions를 배열로 받는 것을 확인할 수 있다. 만약 아무것도 선택하지 않으면, regions에는 빈 배열이 들어가게 된다.

 

 

상품 상세와 상품 수정 View에도 단일 체크 박스를 할때와 마찬가지로 코드를 붙여넣으면 정상 작동하는 것을 확인할 수 있다.

 

 


3. 라디오 버튼

 

ENUM 타입을 활용하여 라디오 버튼을 만들어본다. 이전 글에서 정의했던 ItemTypes ENUM을 활용한다. 상품 등록 시 상품 유형을 선택할 수 있도록 addForm.html에 다음 코드를 추가한다.

 

resources/templates/form/addForm.html

 

1
2
3
4
5
6
7
8
9
<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>
Colored by Color Scripter
cs

 

지역(regions) 정보를 추가할때와 다를게 없는데, th:value 값에 .name()을 적용해준 것만 다르다. 이렇게 지정하면 ENUM의 이름을 value값으로 그대로 출력해준다. 전송되는 Form Data와 서버에서 받아들이는 값은 모두 value값이 넘어가는 것을 확인할 수 있다. 다만, 라디오버튼에서 아무것도 선택하지 않으면 itemType = null 값으로 서버에 전달된다.

 

 

타임리프에서 ENUM 직접 접근

 

<div th:each="type : ${T(hello.itemservice.domain.item.ItemType)}" ...> </div> 방식으로 타임리프에서 ENUM에 직접 접근하게 하는 방법이 있다. 이렇게 하면 따로 model 객체에 itemType을 담을 필요가 없다(컨트롤러에서 @ModelAttribute를 사용한 부분). 그러나 이 방식은 ENUM 파일의 주소가 변경되는 경우 문제가 발생할 수 있어서 권장되지는 않는다.

 

 


 

4. 셀렉트 박스

 

 

배송 방식을 지정하는 셀렉트 박스를 추가해본다. 앞서 했던 내용들과 유사하다. 컨트롤러에 @ModelAttribute를 추가하여 model 객체에 배송 방식 정보를 담는다.

 

java/hello/itemservice/web/form/FormItemController.java

 

1
2
3
4
5
6
7
8
@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;
    }
Colored by Color Scripter
cs

 

View단 코드는 아래와 같이 작성하면 된다. select 태그에 th:field로 필드값을 바인딩해주고, option 태그에 deliveryCode를 th:each로 반복한다.

 

resources/templates/form/addForm.html

 

1
2
3
4
5
6
7
8
<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}">이름</option>
            </select>
        </div>
Colored by Color Scripter
cs

 

상품 등록, 수정, 상세 화면 모두에 앞서 라디오 버튼을 했던 방식과 똑같이 코드를 추가하면 된다. 요청도 똑같이 필드값으로 지정해준 deliveryCode 값이 지정되어 전송되는 것을 확인할 수 있다.

 


 

참조

1. 인프런_스프링 MVC 2편 - 백엔드 웹개발 핵심 기술_김영한 님 강의

 

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2

728x90
반응형