타임리프 - 스프링 통합과 폼

2025. 9. 6. 00:41·Spring

타임리프 스프링 통합

타임리프는 스프링 통합을 위한 다양한 기능을 편리하게 제공한다.

 

스프링 통합으로 추가되는 기능들

- SpringEL

- ${@myBean.doSomething()}같은 스프링 빈 호출 지원

- 편리한 폼 관리를 위한 추가속성 (th:object, th:field, th:erros, th:errorclass)

- 폼 컴포넌트 기능 (checkbox, radio button, List등)

- 스프링 메세지, 국제화 기능

- 검증, 오류처리

- 스프링의 변환 서비스 통합 (ConversionService)

 

입력 폼 처리

- th:object : 커맨드 객체를 지정한다.

- *{...} : 선택 변수 식이라고 한다. th:object에서 선택한 객체에 접근한다.

- th:field : HTML 태그의 id, name, value 속성을 자동으로 처리해준다.

@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
    Item item = itemRepository.findById(itemId);
    model.addAttribute("item", item);
    return "form/editForm";
}

 

<form action="item.html" th:action method="post">
    <div>
        <label for="id">상품 ID</label>
        <input type="text" id="id" name="id" class="form-control" value="1" th:value="${item.id}" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" name="itemName" class="form-control" value="상품A" th:value="${item.itemName}">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}">
    </div>

처리 전 코드

 

<form action="item.html" th:action th:object="${item}" method="post">
    <div>
        <label for="id">상품 ID</label>
        <input type="text" id="id" class="form-control" th:field="*{id}" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName"  class="form-control"  th:field="*{itemName}">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" class="form-control" th:field="*{price}">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" class="form-control" th:field="*{quantity}">
    </div>

처리 후 코드

 

- th:object로 <ㄹorm>에서 사용할 객체를 지정한다.

- th:field="*{itemName}" : 선택 변수 식을 사용했으나, ${item.itemName}과 같다. 위에서 th:object로 item을 선택했기 때문에 선택 변수식을 적용할 수 있다.

- th:field에서 id, name, value 속성을 자동으로 만들어준다.

 

* id 속성을 제거해도 th:field에서 자동으로 만들어준다.

 

체크 박스 - 단일1

<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에서 open= on이라는 값이 넘어간다. 스프링은 on이라는 문자를 true타입으로 변환해준다.

(스프링 타입 컨버터가 이 기능을 수행한다.)

 

그런데, 체크박스를 해제한 상태로 폼을 전송하면 open이라는 필드 자체가 서버로 전송되지 않는다.

private Boolean open; //판매여부

 

따라서 Item 객체 필드에 있는 Boolen 타입의 open이 false가 아닌 null값이 반환된다.

이런 경우에 등록 자체는 문제가 없지만 수정을 했을 때 체크를 풀게 되면 값이 변경되지 않고 그대로 남아있게 되는 오류가 발생한다.

 

이런 경우를 해결하기 위해 스프링 MVC는 약간의 트릭을 사용하는데, 히든 필드를 하나 만들어서 _open처럼 기존 체크박스 이름 옆에 언더스코어를 붙여서 전송하면 체크를 해제했다고 인식할 수 있다.

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

 

체크를 해제한 경우 open은 전송되지 않고, _open만 전송되는데 이 때, 스프링 MVC는 체크를 해제했다고 판단한다.

즉 이 코드에선 서버로 전송시 open이란 이름과 _open이란 이름이 전송되는데, open이 없을 경우 (체크를 해제한 경우) 체크를 해제했다고 판단하는 것이다.

 

체크박스 - 단일2

타임리프가 제공하는 폼 기능을 사용하면 위의 내용들을 편리하게 사용할 수 있다.

<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

 

HTML 생성 결과 : <input type="hidden" name="_open" value="on"/>

 

타임리프는 히든 필드와 관련된 부분들도 함께 해결해준다. 따라서 히든 필드를 만들지 않아도 true, false를 확인할 수 있다.

그리고 checked 속성을 자동으로 추가해준다. 체크시에 checked속성을 개발자가 직접 처리하려면 너무 번거롭다.

타임리프의 th:field를 사용하면 값이 true인 경우 자동으로 checked를 처리해준다.

 

체크박스 - 멀티

체크 박스를 멀티로 사용해서 하나 이상 체크하는 방법이다.

- 등록지역은 서울, 부산, 제주가 있고, 다중으로 선택할 수 있다.

@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에서 반환한 값이 자동으로 모델에 담기게 된다.

물론 각각 컨트롤러 메서드에서 모델에 직접 넣어서 사용해도 문제 없다.

<!-- 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')}"

멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있다. 하지만 반복해서 HTML 태그를 생성할 때, id는 유니크해야한다. 따라서 모든 타임리프 체크박스를 each 루프 안에서 반복해서 만들 때 임의로 1, 2, 3같은 숫자를 뒤에 붙여주게 된다.

id가 타임리프에 의해 동적으로 만들어지기 때문에 <label for="id 값">으로 임의로 지정하는것은 좋지 못하다. ids.prev, ids.next를 제공해서 동적으로 생성되는 id값을 사용하도록 해준다.

 

그리고 체크박스를 체크하지 않을 경우 똑같이 hidden field가 생긴다. 마찬가지로 th:field를 사용했으니 당연한 결과이다.

checked도 마찬가지로 자동으로 처리해주는것을 확인할 수 있다.

 

 

라디오 버튼

라디오 버튼은 여러 선택지 중 하나를 선택할 때 사용할 수 있다. 이번엔 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;
    }
}
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
    // Enum의 정보를 배열로 넘겨준다
    return ItemType.values();
}

 

이렇게 모델에 ENUM을 담아서 전달할 수 있다.

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

 

음식을 (FOOD) 선택한다.  선택하지 않으면 아무 값도 넘어가지 않는다.

(선택하지 않을시 로그 : item.itemType=null)

 

체크박스는 수정 시 체크를 해제하면 아무 값도 넘어가지 않기 때문에 히든 필드로 이런 문제를 해결했는데, 라디오 버튼은 이미 선택되었다면 수정시에도 항상 하나를 선택해야하기 때문에 히든필드를 사용할 필요가 없다.

 

타임리프에서 ENUM으로 직접 접근하는 방법이 있다.

<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">

스프링 EL 문법으로 ENUM을 직접 사용한 코드이다. values()를 호출하면 해당 ENUM의 정보가 배열로 반환된다.

하지만 이렇게 사용하면 패키지 위치가 변경되거나 할 때 자바 컴파일러가 타임리프 컴파일 오류까지 잡아낼 수 없으므로 권장하지 않음

 

 

셀렉트 박스

셀렉트 박스는 여러 선택지 중에 하나를 선택할 때 사용할 수 있다. 자바 객체를 활용해서 개발해보자

 

- 배송 방식은 빠른, 일반, 느린배송이 있고, 하나만 선택 가능하다.

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

 

마찬가지로 @ModelAttribute를 사용했다.

* @ModelAtttribute를 사용하면 컨트롤러 메서드 호출 시 항상 객체가 생성된다. 따라서 이렇게 사용해도 크게 메모리 낭비는 없을 수 있지만, static 처럼 사용하는게 메모리 낭비를 줄일 수 있는 방법이 된다.

 

<!-- 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도 마찬가지로 타임리프에서 select된 항목을 selected로 자동으로 처리해준다. 따라서 개발자가 할 일이 줄어들었다...

 

 

'Spring' 카테고리의 다른 글

오류 코드와 메세지처리, Validator 분리  (0) 2025.09.11
메세지, 국제화  (1) 2025.09.07
타임리프 - 기본 기능  (1) 2025.08.31
상품 도메인 개발 - 뷰 템플릿, 타임리프  (1) 2025.08.28
스프링 MVC - 기본 기능  (1) 2025.08.25
'Spring' 카테고리의 다른 글
  • 오류 코드와 메세지처리, Validator 분리
  • 메세지, 국제화
  • 타임리프 - 기본 기능
  • 상품 도메인 개발 - 뷰 템플릿, 타임리프
공부처음하는사람
공부처음하는사람
  • 공부처음하는사람
    lazzzykim
    공부처음하는사람
  • 전체
    오늘
    어제
    • 분류 전체보기 (159)
      • Kotlin (31)
      • Java (56)
      • Spring (44)
      • JPA (6)
      • Algorithm (3)
      • TroubleShooting (1)
      • 내일배움캠프 프로젝트 (14)
      • Setting (2)
      • ... (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 인기 글

  • 태그

    언체크예외
    중첩클래스
    빈 생명주기
    OCP
    김영한
    spring
    김영한의 실전 자바
    kotlin
    트랜잭션
    java
    배열
    싱글톤
    Di
    내일배움캠프
    예외처리
    김영한의 실전자바
    제네릭
    다형성
    jpa
    캡슐화
  • hELLO· Designed By정상우.v4.10.3
공부처음하는사람
타임리프 - 스프링 통합과 폼
상단으로

티스토리툴바