thymeleaf - 스프링 통합

박민수·2023년 11월 15일
post-thumbnail

thymeleaf - 스프링 통합

타임리프는 스프링과 통합을 위한 다양한 기능들을 제공한다.

  • 스프링의 SpringEL 문법 통합
  • ${@myBean.doSomething()} 처럼 스프링 빈 호출 지원
  • 편리한 폼 관리를 위한 추가 속성 (th:object, th:field, th:errors, th:errorclass)
  • 폼 컴포넌트 기능 (checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원)
  • 스프링의 메시지, 국제화 기능의 편리한 통합
  • 스프링의 검증, 오류 처리 통합
  • 스프링의 변환 서비스 통합(ConversionService)

설정법

build.gradle에 라이브러리를 추가해주면 Gradle은 타임리프와 관련된 라이브러리를 다운로드 받고, 스프링 부트는 타임리프와 관련된 설정용 스프링 빈을 자동으로 등록해준다.

thymeleaf 의존 관계 추가

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

문법

입력 폼 처리

  • th:object : 커맨드 객체를 지정한다.
  • *{...} : th:object에서 선택한 객체에 접근한다.
<form action="login" th:object="${user} method="post">
     <div>
         <label for="userName">아이디</label>
         <!-- *{userName}은 ${user.userName}과 같다 -->
         <input type="text" th:field="*{userName}"/>
     </div>
     <div>
         <label for="userPassword">비밀번호</label>
         <!-- *{userPassword}은 ${user.userPassword}과 같다 -->
         <input type="password" th:field="*{userPassword}"/>
     </div>
</form>
  • th:field : HTML 태그의 id, name, value 속성을 자동으로 만들어준다.
<!-- 랜더린 전 -->
<input type="text" th:field="*{userName}"/>

<!-- 랜더린 후 -->
<input type="text" id="userName" name="userName" value=""/>

체크박스

체크 박스에서 체크를 하고 저장하면, 조회시에 checked 속성이 추가된다. 이런 부분을 개발자가 직접 처리하려면 상당히 번거롭다. 타임리프의 th:field를 사용하면, 값이 true인 경우 체크를 자동으로 처리해준다.

멀티 체크박스

체크 박스를 멀티로 사용해서, 하나 이상을 체크할 수 있도록 해보자.

FormItemController 추가

@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)에 담기게 된다.
물론 이렇게 사용하지 않고, 각각의 컨트롤러 메서드에서 모델에 직접 데이터를 담아서 처리해도 된다.

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

여기서 주의할 점은 일반적으로 태그의 id값은 중복이 허용되지 않고 유일한 값이어야 한다.
따라서 타임리프에서는 체크박스를 each 루프 안에서 반복해서 만들 때, 태그의 id 값들에 임의로 1,2,3과 같은 숫자를 뒤에 붙여준다. (name 같은 중복을 허용한다)

<input type="checkbox" value="SEOUL" id="regions1" name="regions">
<input type="checkbox" value="SEOUL" id="regions2" name="regions">
<input type="checkbox" value="SEOUL" id="regions3" name="regions">

이때 label for="id 값" 으로 label의 대상이 되는 id 값을 지정해 주어야 하는데, 태그의 id 값이 타임리프에 의해 동적으로 만들어지기 때문에 id 값을 임의로 지정하는 것은 곤란하다. 타임리프는 이러한 문제를 해결하기 위해 ids.prev(), ids.next() 를 제공한다. 해당 기능을 통해 동적으로 생성되는 id 값들을 자동으로 지정해준다.

<label th:for="${#ids.prev('regions')}" th:text="${region.value}"></label>
<!-- 랜더링 후 결과 -->
<div>
    <input type="checkbox" value="SEOUL" id="regions1" name="regions">
    <input type="hidden" name="_regions" value="on">
    <label for="regions1">서울</label>
    <input type="checkbox" value="BUSAN" id="regions2" name="regions">
    <input type="hidden" name="_regions" value="on">
    <label for="regions1">부산</label>
    <input type="checkbox" value="JEJU" id="regions3" name="regions">
    <input type="hidden" name="_regions" value="on">
    <label for="regions1">제주</label>
</div>

랜더링 후 결과를 보면 hidden 타입의 input 태그가 생성된 것을 확인할 수 있다. 웹 브라우저에서 체크박스에 체크를 하나도 하지 않았을 때, 클라이언트가 서버에 아무런 데이터를 보내지 않는 것을 방지하기 위해 타임리프는 랜더링 과정에서 hidden 타입의 새로운 input 태그를 만들어 주는데, 이때 해당 태그의 name은 "_(언더바) + 기존 name" 이고, value는 on 또는 false이다. 만약 이러한 과정이 없다면 체크박스에 체크를 하지 않았을 때 value 값이 null 이기 때문에 value 값이 서버로 넘어오지 않는다.

로그를 출력해보자.

// FormItemController.addItem()에 코드 추가
log.info("item.regions={}", item.getRegions());
// 서울, 부산 선택
regions=SEOUL&_regions=on&regions=BUSAN&_regions=on&_regions=on
// 로그 : item.regions=[SEOUL,BUSAN]
// 지역 선택 X
_regions=on&_regions=on&_regions=on
// 로그 : item.regions=[]

라디오 버튼

라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용한다.

FormItemController 추가

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

ItemType은 ENUM 타입의 자바 클래스이고, 값으로는 도서, 식품, 기타 가 있다. itemTypes를 등록 폼, 조회, 수정 폼에서 모두 사용하므로 위에서 언급한 @ModelAttribute의 특별한 기능을 적용하였다. ItemType.values()를 사용하면 해당 ENUM의 모든 정보를 배열러 반환한다.

<!-- 랜더링 전 -->
<div th:each="type : ${itemTypes}">
    <input type="radio" th:field="${item.itemType}" th:value="${type.name()}" disabled>
    <label th:for="${#ids.prev('itemType')}" th:text="${type.description}">BOOK</label>
</div>

<!-- 랜더링 후 결과 -->
<div>
    <input type="radio" value="BOOK" id="itemType1" name="itemType">
    <label for="itemType1">도서</label>
</div>
<div>
    <input type="radio" value="FOOD" id="itemType2" name="itemType" checked="checked">
    <label for="itemType2">식품</label>
</div>
<div>
    <input type="radio" value="ETC" id="itemType3" name="itemType">
    <label for="itemType3">기타</label>
</div>

모델에 ENUM을 담아서 전달하는 대신에 타임리프는 자바 객체에 직접 접근할 수 있다.

@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
    return ItemType.values();
}
<!-- html 파일에서 ENUM 직접 접근 -->
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">

셀렉트 박스

셀렉트 박스는 여러 선택지 중에 하나를 선택할 때 사용한다.

FormItemController 추가

@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;
}
<!-- 랜더링 전 -->
<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>

<!-- 랜더링 후 결과 -->
<select class="form-select" id="deliveryCode" name="deliveryCode">
    <option value="">==배송 방식 선택==</option>
    <option value="FAST">빠른 배송</option>
    <option value="NORMAL">일반 배송</option>
    <option value="SLOW">느린 배송</option>
</select>

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

profile
안녕하세요 백엔드 개발자입니다.

0개의 댓글