타임리프- 스프링 통합(feat @ModelAttribute)

byeol·2023년 3월 14일
0

김영한님의 스프링 mvc2 교재를 복습하면서 내용을 정리해보고자 한다.
오늘 정리해볼 내용은 타임리프가 스프링과 통합해서 제공하는 편리한 기능들이다.

문법 위주로 정리를 해보려고 한다.

Ready, Set, Go!! 🏃‍♀️🏃‍♂️🏃

<form><input>

기초 정리
<input>태그는 사용자가 입력할 수 있는 입력 필드를 선언하기 위해서 <form> 태그 내부에 있다. <form> 태그는 <input>에 입력된 값을 action 속성의 값으로 선언된 url에 보내준다. 또한 <input>태그는 <laber for="input태그의 id"> 태그와 짝궁이 된다. <label>태그의 for속성에는 <input>태그의 id속성의 값이 온다.

th:object, *{...},th:fieild 이 3가지는 <form>태그를 좀 더 편리하게 사용할 수 있도록 도와준다.

순서를 정리해보자.

사용자가 url을 호출한다. ➡️ 이 url과 mapping 된 컨트롤러의 메소드를 호출한다. ➡️ 이 메소드는 빈 Model객체를 뷰템플릿에 보낸다 ➡️ 뷰 템플릿의 <form>태그는 <input>태그를 통해서 입력된 값을 빈 Model 객체에 저장하고 이를 <form>태그의 action 속성의 값에 선언된 url에 보낸다. ➡️ 이 url과 mapping된 컨트롤러의 메소드가 Model 객체를 받는다.

여기에 th:object, *{...},th:fieild이 3가지 타임리프 기능을 적용해서 좀 더 쉽게 풀어나가면 된다.

@GetMapping("/add")
public String addForm(Model model) {
 model.addAttribute("item", new Item());
 return "form/addForm";
}

▲ 빈 객체를 뷰에 보낸 Controller의 메소드

@Data
public class Item {
 private Long id;
 private String itemName;
 private Integer price;
 private Integer quantity;

▲ Item 클래스 구조는 대략 위와 같다.

<form 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"  th:field="*{quantity}" class="formcontrol"
             placeholder="수량을 입력하세요">
    </div>
 </form>

▲ html파일

th:object: <form>의 속성으로 <form>태그 내부에서 사용할 객체를 지정한다. 위 뷰템플릿은 컨트롤러에서 받은 Model객체를 <form>태그 내부에서 사용한다. 왜 굳이 이렇게 하느냐? 그 이유는 <form>내부에서 보통 Model 객체의 값을 가져올 때 ${item.itemName}으로 가져왔는데 이제는 *{itemName}으로 가져올 수 있기 때문이다.

th:field: <input>내부의 속성으로 이용된다. 이 속성은 *{A}안에 있는 이름을 가지고 id,name,value의 값을 자동으로 만들어 준다. 위 예시에서 <input>태그에서 id를 지워도 된다.

<input text="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>

▲ html파일

<form>태그 내부의 <input text="checkbox">태그는 체크박스를 만들어서 action 속성 url과 매핑된 컨트롤러의 메소드에 체크를 했는지 안했는지에 대한 데이터를 보낸다.

근데 사실 했냐만 보내지 안했는지는 안보내준다.
했는지는 true로 값을 저장해서 보내는데 안할 때는 null값으로 보낸다.
굳이 체크 안했으면 안받아도 되는게 아닐까?
라고 생각할지도 모르지만
만약 수정하는 페이지에서 원래는 했는데 안하는 걸로 고친다고 생각해보면
안했다는 걸 알아야 DB를 갱신할 텐데 안했다는게 넘어오질 않으니 그 사용자의
체크 상태는 수정을 해도 영원히 체크된 상태일 것이다.

<!-- 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>

▲ 히든 필드를 추가한 html파일

따라서 스프링에서 약간의 트릭을 쓴다고 한다.
바로 <input>태그의 type을 hidden으로 설정하고 name은 기존 체크박스 이름에 _를 앞에 추가한다. 또한 value 값은 "on"으로 설정한다.

스프링은 이 히든 필드와 실제 <input>태그의 값을 보고 true 인지 false인지 판단한다. 히든 필드의 경우 true,false 둘다 넘어오기 때문에
true의 경우 두개다 넘어와서 true로 판단한다.
false의 경우 히든 필드 한개만 넘어와서 false로 판단한다.

<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파일

하지만 이 기능을 더 편하게 제공하는 타임리프 기능이 있는데 바로 앞서 배웠던
th:object, *{...},th:fieild 이 3가지 <form>이다.
이 3개가 자동으로 히든 필드를 만들어준다.

<input text="checkbox"> - 하나 이상을 체크하는 멀티 체크박스

기초
<input>태그의 value 특성은 체크박스의 값을 나타내는 문자열이어야 한다. 이 값은 클라이언트에는 노출되지 않고, 양식을 제출할 때 체크박스의 name과 함께 서버로 전송된다.

지역을 등록할 수 있는 기능을 추가한다고 가정하자
지역은 서울, 부산, 제주이며 다중으로 체크할 수 있다.

따라서

▲Item 클래스
private List regions이 추가되었다.

컨트톨러에 아래의 regions메서드를 추가한다.

@ModelAttribute 특별한 사용법(타임리프❌)

사실 이 부분이 타임리프가 제공하는 기능은 아니다.
하지만 이 chapter를 배우면서 새롭게 배운 부분이다.
보통 @ModelAttribute는 사용자로부터 받은 데이터를 쪼개지 않고 객체 자체로 받을 때 사용하는 애노테이션이다.

//이 Controller가 실행되면 자동으로 model에 담아서 뷰에 준 다.
 @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는 해당 컨트롤러가 실행되면 @ModelAttribute이 붙은 메서드를 호출하여 자동으로 실행하여 Model객체를 만들어 컨트롤러와 연결된 모든 뷰 템플릿에 전달한다.

<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:each를 통해서 멀티 체크 박스를 만드는 html 파일

멀티 체크 박스를 배우면서 신기했던 것은

  • th:each를 통해서 멀티로 만드는 접근법과
  • 또한 th:for="${#ids.prev('regions')}"이 부분이다.
    멀티 박스의 경우 name이 같아도 상관없지만 id는 모두 달라야 한다. 이 기능은 타임리프의 each가 보완해서 만들어준다. 임의로 id 뒤에 1,2,3 등의 숫자를 붙여준다.
  • 따라서 <label for="input 태그의 id">태그의 for 속성의 값도 거기에 맞춰서 만들어야 한다.


▲ 렌더링한 결과

렌더링한 결과를 보면 each속성이 id 뒤에 1,2,3 등의 숫자를 붙여 줬고 th:field의 *{}이 히든 필드를 만들어줬다.
또한 th:for="${#ids.prev('regions')}" 이 부분이 <input> 태그의 id값과 똑같이 for속성의 값을 만들어 줬다.

체크 박스 정리

주의해야할 것이 있다면 체크하지 않은 값에 대한 데이터가 서버 쪽에 가지 않는다는 점이다. 즉 null값으로 가기 때문에 만약 사용자가 수정 페이지를 통해서 체크했던 속성을 체크하지 않도록 수정한다면 서버 쪽은 체크않았다에 대한 정보를 받지 못한다.

따라서 스프링은 히든 필드를 통해서 이 점을 보완했지만

타임리프의 th:object,th:field,*{...}가 이를 더 보완해서 알아서 히든 필드를 만들어준다.

멀티체크 박스의 경우
th:each를 통해서 멀티체크 박스를 만들게 된다.

여기서 th:each는 <input>태그의 id가 겹치면 안된다는 점을 보완해서 id 속성 값 뒤에 1,2,3 등의 숫자를 붙여서 id 값을 만들어준다.

<input>와 짝궁인 태그 또한 for속성을 통해서 <input> id 값을 넣어야 하는데 그 때 th:for="${#ids.prev('regions')}"이 기능을 사용한다.

단 하나의 값만 선택되는 <input type="radio"> 라디오 버튼

기초
Enum이란 관련있는 여러개의 변수에 고정값을 심어놓은 묶음(class)이라고 볼 수 있겠다.
이 값을 변하지 않은 고정값이다. 예를 들어 우리가 "BOOK"이라는 변수에는 "도서"라는 값을 변하지 않은 값으로 선언하고 이와 같은 고정값이 묶음으로 존재할 때 사용한다. 이 chapter에서는 BOOK("도서"), FOOD("식품"), ETC("기타") 이 3가지로 선언된다.
Enum 은 자체적으로 name()으로 Enum 값을 찾는 valueOf() 메소드를 제공한다. 또한 value()메서드는 Enum의 요소들을 순서대로 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;
    }
}

▲ Enum

@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
 return ItemType.values();
}

▲ Controller에서 @ModelAttribute를 이용해서 Enum의 값들을 배열로 받아 모든 뷰에 Model 객체를 뿌린다.

<!-- 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>

▲ 라디오 버튼이 적용된 html 파일

여기서th:value="${type.name()}"를 통해 서버에는 Enum에 선언된 변수의 값이 간다.
또한 체크 박스와 마찬가지로 th:each를 이용하며 이에 따라 <input>태그의 id 값도 1,2,3 이 등이 붙은 값으로 자동 생성된다. 그래서 <label> 태그의 for 속성의 값이 th:for="${#ids.prev('itemType')}" 함께 자동 생성되도록 하는 타임리프의 기능을 이용한다.


▲ 렌더링된 결과

렌더링된 결과를 보면 체크박스와 다르게 히든 필드가 없다는 것을 확인할 수 있다. 왜냐하면 라이오 버튼의 경우 무조건 하나가 선택되기 때문이다.

<select>와 <option> 셀렉트 박스

셀렉트 박스 또한 @ModelAttribute를 사용해서 객체 자체를 넘겨줄 것이다.

@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;
    }

▲ Controller에서 @ModelAttribute를 사용해서 객체의 값을 모든 뷰에 넘겨준다.

package hello.itemservice.domain.item;


import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class DeliveryCode {
    private String code;
    private String displayName;
}

▲ DeliveryCode의 구조

 <!-- 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>

▲ html 파일

▲ 렌더링 결과

profile
꾸준하게 Ready, Set, Go!

0개의 댓글

관련 채용 정보