타임리프는 스프링 없이도 동작하지만, 스프링과 통합을 위한 다양한 기능을 편리하게 제공한다. 그리고 이런 부분은 스프링으로 백엔드를 개발하는 개발자 입장에서 타임리프를 선택하는 하나의 이유가 된다.
스프링 통합으로 추가되는 기능들
${@myBean.doSomething()}
처럼 스프링 빈 호출 지원th:object
(기능 강화, 폼 커맨드 객체 선택)th:field
, th:errors
, th:errorclass
th:object
: 커맨드 객체를 지정한다.*{...}
: 선택 변수 식이라고 한다. th:object 에서 선택한 객체에 접근한다.th:field
: HTML 태그의 id , name , value 속성을 자동으로 처리해준다.아래의 기존 코드를 변경해보자.
<변경 전>
<form action="item.html" th:action method="post">
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}">
<변경 후>
<form action="item.html" th:action th:object="${item}" method="post">
<label for="quantity">수량</label>
<input type="text" class="form-control" th:field="*{quantity}" >
코드가 훨씬 더 간결해진것을 볼 수 있다.
실제 서버를 돌려 렌더링한 결과를 보면 아래와 같이 id, name, value가 생성된 것을 볼 수 있다.
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" value="10" readonly>
th:object="${item}"
: <form>
에서 사용할 객체를 지정한다. 선택 변수 식( *{...} )
을 적용할 수 있다.th:field="*{itemName}"
*{itemName}
는 선택 변수 식을 사용했는데, ${item.itemName}
과 같다. 앞서 th:object
로 item 을 선택했기 때문에 선택 변수 식을 적용할 수 있다.th:field
는 id , name , value 속성을 모두 자동으로 만들어준다.id , name , value는 각각 field에서 지정해준 이름과 값이다. <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 방식으로 작성했다. 어떤 문제가 발생하는지 보자.
먼저 체크박스를 체크하고 전송 버튼을 눌렀을 경우 로그를 찍어보면 itemName=itemA&price=10&quantity=1&open=on]
와 같이 open=on이라고 전달되는 것을 확인할 수 있다. 스프링은 on 이라는 문자를 true 타입으로 변환해준다.
그런데, 체크박스를 체크하지 않고 전송 버튼을 누른 뒤 로그를 확인해보면, itemName=itemA&price=10&quantity=1]
와 같이 open 값이 아예 나오지 않는 것을 확인할 수 있다.
FormItemController : item.open=true //체크 박스를 선택하는 경우
FormItemController : item.open=null //체크 박스를 선택하지 않는 경우
HTML checkbox는 선택이 안되면 클라이언트에서 서버로 값 자체를 보내지 않는다. 수정의 경우에는 상황에 따라서 이 방식이 문제가 될 수 있다. 사용자가 의도적으로 체크되어 있던 값을 체크를 해제해도 저장시 아무 값도 넘어가지 않기 때문에, 서버 구현에 따라서 값이 오지 않은 것으로 판단해서 값을 변경하지 않을 수도 있다.
그래서 스프링은 히든 필드라는 기능을 제공한다.
<input type="hidden" name="_open" value="on"/>
를 체크박스 밑에 추가하자.
그러면 우리가 체크박스를 체크하지 않아도 itemName=itemA&price=10&quantity=1&_open=on]
와 같이 로그가 확인된다.
FormItemController : item.open=true //체크 박스를 선택하는 경우
FormItemController : item.open=false //체크 박스를 선택하지 않는 경우
또한 실행로그에서 item.open이 null이 아닌, false가 찍히는 것을 확인할 수 있다.
open=on&_open=on
_open=on
_open
만 있는 것을 확인하고, open 의 값이 체크되지 않았다고 인식한다.하지만 히든 필드를 사용한다고 해도 체크박스를 만들때 마다 이렇게 히든 필드를 만드는 것은 굉장히 번거로운 일이다. 따라서 타임리프를 이용하자.
위에서 사용했던 field를 사용해보자. 위의 form에 th:object는 이미 작성되어있다.
<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>
name이 없어지고 field가 생겼다. 또한 히든 필드가 없어진 것을 확인할 수 있다.
서버를 돌리고 소스를 확인하면 아래와 같은 결과가 나온다.
<div class="form-check">
<input type="checkbox" id="open" class="form-check-input" name="open" value="true"/><input type="hidden" name="_open" value="on"/>
<label for="open" class="form-check-label">판매 오픈</label>
</div>
히든 필드를 분명 작성하지 않았는데 생긴것을 볼 수 있다. field가 이 모든 역할을 다 해주는 것이다. 따라서 히든 필드가 있기 때문에 체크박스를 미체크해도 null이 아닌 false 값이 나온다.
disabled 를 사용해서 상품 상세에서는 체크 박스가 선택되지 않도록 할 수 있다.
html의 생성결과
<hr class="my-4">
<!-- single checkbox -->
<div class="form-check">
<input type="checkbox" id="open" class="form-check-input" disabled
name="open" value="true"
checked="checked">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
disable만 넣어줬는데, checked="checked"
가 생성된 것을 볼 수 있다.
checked="checked"
@ModelAttribute의 특별한 사용법
등록 폼, 상세화면, 수정 폼에서 모두 서울, 부산, 제주라는 체크 박스를 반복해서 보여주어야 한다. 이렇게 하려면 각각의 컨트롤러에서 model.addAttribute(...) 을 사용해서 체크 박스를 구성하는 데이터를 반복해서 넣어주어야 한다.
@ModelAttribute 는 이렇게 컨트롤러에 있는 별도의 메서드에 적용할 수 있다.
이렇게하면 해당 컨트롤러를 요청할 때 regions 에서 반환한 값이 자동으로 모델( model )에 담기게 된다.
물론 이렇게 사용하지 않고, 각각의 컨트롤러 메서드에서 모델에 직접 데이터를 담아서 처리해도 된다.
<!-- multi checkbox -->
<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:for="${#ids.prev('regions')}"
멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있다. 그런데 문제는 이렇게 반복해서 HTML 태그를 생성할 때, 생성된 HTML 태그 속성에서 name 은 같아도 되지만, id 는 모두 달라야 한다. 따라서 타임리프는 체크박스를 each 루프 안에서 반복해서 만들 때 임의로 1 , 2 , 3 숫자를 뒤에 붙여준다.
<!-- multi checkbox -->
<div>
<div>등록 지역</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="SEOUL"
class="form-check-input" disabled id="regions1" name="regions">
<label for="regions1"
class="form-check-label">서울</label> </div>
<div class="form-check form-check-inline">
<input type="checkbox" value="BUSAN"
class="form-check-input" disabled id="regions2" name="regions" checked="checked">
<label for="regions2"
class="form-check-label">부산</label> </div>
<div class="form-check form-check-inline">
<input type="checkbox" value="JEJU"
class="form-check-input" disabled id="regions3" name="regions" checked="checked">
<label for="regions3"
class="form-check-label">제주</label> </div>
</div>
html 생성결과 위와 같이 id가 생성된 것을 볼 수 있다. prev 는 이전의 시퀀스 값을 가져오기 때문에 각각 region1, region2, region3을 가져왔다.
만약 서울과 부산을 선택할 경우 regions=SEOUL&_regions=on®ions=BUSAN&_regions=on&_regions=on
과 item.regions=[SEOUL, BUSAN]
의 로그를 확인할 수 있다.
아무 지역도 선택하지 않을 경우 _regions=on&_regions=on&_regions=on
과 item.regions=[]
를 확인할 수 있다.
라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용할 수 있다. 즉 중복 선택이 불가능하다.
<!-- 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>
item.itemType=FOOD: 값이 있을 때
item.itemType=null: 값이 없을 때
체크 박스는 수정시 체크를 해제하면 아무 값도 넘어가지 않기 때문에, 별도의 히든 필드로 이런 문제를 해결했다. 라디오 버튼은 이미 선택이 되어 있다면, 수정시에도 항상 하나를 선택하도록 되어 있으므로 체크박스와 달리 별도의 히든 필드를 사용할 필요가 없다.
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
}
이제까지는 Enum을 model에 담아 전달해서 사용했다. 하지만 스프링에선 Enum을 아래의 방법으로 직접 전달 할 수 있다.
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
스프링EL 문법으로 ENUM을 직접 사용할 수 있는데, ENUM에 values() 를 호출하면 해당 ENUM의 모든 정보가 배열로 반환된다.
그런데 이렇게 사용하면 ENUM의 패키지 위치가 변경되거나 할때 자바 컴파일러가 타임리프까지 컴파일 오류를 잡을 수 없으므로 추천하지는 않는다.
셀렉트 박스는 라디오 박스와 같이 여러 선택지 중에 하나를 선택할 때 사용할 수 있다.
<!-- 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 생성
<!-- SELECT -->
<div>
<DIV>배송 방식</DIV>
<select class="form-select" id="deliveryCode" name="deliveryCode">
<option value="">==배송 방식 선택==</option>
<option value="FAST">빠른 배송</option>
<option value="NORMAL">일반 배송</option>
<option value="SLOW">느린 배송</option>
</select>
</div>
빠른 배송을 선택하면 아래와 같이 selected가 된다.
<!-- SELECT -->
<div>
<DIV>배송 방식</DIV>
<select class="form-select" id="deliveryCode" name="deliveryCode">
<option value="">==배송 방식 선택==</option>
<option value="FAST" selected="selected">빠른 배송</option>
<option value="NORMAL">일반 배송</option>
<option value="SLOW">느린 배송</option>
</select>
</div>