스프링 MVC-타임리프 통합
타임리프 스프링 통합
타임리프는 기본적으로 템플릿 엔진으로 스프링 없이도 동작하지만, 스프링에서의 사용을 더 편리하게 해주는 여러 기능들을 지원한다.
해당 기능들로는 폼 컴포넌트 기능, 스프링의 메시지 및 국제화의 편리한 통합, 검증 및 오류처리 통합, 스프링 빈 호출 기능 등 여러가지가 있다.
스프링과의 통합은 원래 이런저런 설정들이 필요하지만 이제는 라이브러리에 추가만 하면 스프링부트에서 알아서 해주기때문에 매우 편리하다.
이전에 만들었던 간단한 상품 관리 웹을 타임리프와 스프링을 통해 리팩토링 해보자.
리팩토링
<form action="item.html" 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" id="quantity" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
</div>
기존 폼 코드를 타임리프가 지원하는 기능들로 간소하게 리팩토링 한 코드이다. 눈여겨봐야 할 부분은 ‘th:object’와 ‘th:field’가 사용된 부분이다.
th:object는 <form> 태그에서 사용할 객체를 지정한다. 선택변수식인 ‘{}’를 사용할 수 있게 하는데, 기존에는 ‘${item.quantity}’와 같이 사용해야 했던 것을 ‘{quantity}’로 간소하게 작성할 수 있게 된다.
th:field는 html의 id, name, value 값을 자동으로 넣어준다. id와 name은 field에서 지정한 변수의 이름과 같고, value로는 지정한 변수의 값을 사용한다.
체크박스-단일
타임리프가 제공하는 기능들을 사용하면 폼에서 체크박스, 라디오 박스, 셀렉트 박스 등을 편리하게 사용할 수 있다. 이제부터 하나씩 알아보자.
<!-- single 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로 만든 체크박스 코드이다. 이렇게 작성하면 값이 object로 지정해놓은 item 객체의 open으로 넘어오기 때문에, item.open으로 로그를 찍어보면 체크박스의 값을 확인할 수 있다.
체크박스를 체크하면 open=on이라는 값이 넘어가는데, 스프링 내부의 컨버터로 on을 true로 바꿔 넘겨주게 된다. 여기서 문제는 만약 체크박스를 체크하지 않으면 false를 넘겨주는 것이 아닌 아예 필드 자체가 넘어오지 않게 된다.
값이 null이면 체크되지 않은 식으로 구현했다면 문제가 없겠으나, 충분히 문제를 일으킬 소지가 있다. 따라서 스프링에서는 한가지 트릭을 사용하는데, 이름 앞에 ‘_‘를 붙인 히든 필드를 하나 만들면 히든 필드는 항상 전송되므로 히든필드만 전송되었다면 체크가 해제된 것으로 판단하여 false를 넘겨줄 수 있다.
<!-- 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>
위의 코드에 히든필드를 추가한 버전이다. 해당 코드를 테스트해보면 false 값이 넘어오는 것을 확인할 수 있다.
그런데 개발할 때마다 이렇게 히든필드를 추가해주는 것은 번거롭다. 타임리프를 통하면 이 또한 간소화하여 작성할 수 있다.
<!-- 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>
위에서 배운 field를 사용하여 코드를 간소화하였다. 체크박스에서 field를 사용하면 name과 value값을 자동으로 넣어줄 뿐만 아니라, 히든 필드도 자동으로 생성해준다.
field를 사용하면 한가지 더 장점이 있는데, 아래의 코드를 보자.
<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>
상품 상세 폼으로 open값을 전달받아 체크박스를 생성한다. 원래는 open의 값이 true인지를 확인하여 checked 변수에 적절한 값을 넣어주는 로직이 필요하나, field를 사용하면 이 또한 자동으로 값을 확인하고 변수에 적절한 값을 넣어준다.
체크박스-멀티
이번엔 단일 체크박스가 아닌, 3가지 체크박스 중 다중으로 체크가 가능한 체크박스를 만들어보자. 예시는 등록 지역인 서울, 제주, 부산 중 체크하는 것으로 하였다.
@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 어노테이션을 적용하면, 컨트롤러를 사용할 때 메서드에서 반환받은 값을 자동으로 model에 넣어주는 기능을 해준다.
이제 데이터는 처리가 되었으니 멀티 체크박스를 작성해보자.
<!-- 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>
위의 코드에서 어노테이션을 통해 model에 넣은 regions를 받아 체크박스를 작성하는 코드이다. each를 통해 html 코드를 반복해서 작성하면 name은 같아도 되지만 id는 달라야 하는데 이 부분도 타임리프가 임의로 1, 2, 3 숫자를 뒤에 붙여 처리해준다.
또한 id는 이렇게 동적으로 생성되기 때문에 밑의 th:for에서 id를 정적으로 지정할 수가 없는데, 타임리프에서 제공하는 #ids.prev()와 같은 기능을 사용하여 동적으로 생성되는 id를 사용할 수 있다.
<!-- multi checkbox -->
<div>
<div>등록 지역</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="SEOUL" class="form-check-input" id="regions1" name="regions">
<input type="hidden" name="_regions" value="on"/>
<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" id="regions2" name="regions">
<input type="hidden" name="_regions" value="on"/>
<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" id="regions3" name="regions">
<input type="hidden" name="_regions" value="on"/>
<label for="regions3" class="form-check-label">제주</label>
</div>
</div>
<!-- -->
위의 코드가 html로 렌더링된 결과이다. 동적으로 생성된 id들을 확인할 수 있다.
라디오 버튼
이번엔 체크박스가 아닌 여러 선택지 중 한 가지를 선택하는 라디오 버튼을 만들어보자. java의 ENUM으로 만든 객체인 ItemType을 사용할 것이다.
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
}
라디오 버튼도 반복해서 사용되므로 어노테이션을 사용하여 model에 넣어주었다. 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>
멀티 체크박스와 비슷하게 동적으로 생성되는 id들을 #ids.prev()를 통해 처리하였다. 체크박스는 수정 시에 체크를 해제하면 아무 값도 넘어가지 않기 때문에 히든 필드를 통해 문제를 해결하였지만, 라디오 박스는 수정 시에도 항상 하나를 선택하도록 되어있으므로 히든 필드가 필요가 없다.
셀렉트 박스
이제 여러 선택지 중 한 가지를 선택하는 셀렉트 박스를 만들어보자. {빠른 배송, 일반 배송, 느린 배송} 3가지 중 셀렉트 박스로 한 가지를 선택하는 예시를 구현할 것이다.
@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;
}
마찬가지로 반복사용 되므로 어노테이션을 사용하였고, String, String 값을 가지는 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>
<hr class="my-4">
each로 List에서 반복문으로 값을 꺼내 option 태그에 값을 넣어주는 방식이다.
끝
지금까지 타임리프를 통해 html 폼을 더 간소화하고, 더 편리하게 사용하는 방법들을 알아보았다. 웹에서 이러한 폼은 떼놓을 수 없는 부분인데, 나중에 프로젝트를 하다 만들 일이 생겨도 이 포스팅을 보고 충분히 만들 수 있을 것 같은 생각이 든다.
다음 포스팅은 스프링의 메시지, 국제화 기능과 그것을 웹에 어떻게 적용하는 지에 대해 포스팅하도록 하겠다.