김영한 님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
1. 타임리프와 스프링 통합으로 추가되는 기능
스프링 SpringEL 문법 통합
${@myBean.doSomething()}
처럼 스프링 빈 호출 지원
폼 관리를 위한 추가 속성을 제공
th:object
(기능 강화, 폼 커맨드 객체 선택)
th:field
, th:errors
, th:errorclass
폼 컴포넌트 기능
스프링의 메시지, 국제화 기능의 편리한 통합
스프링의 검증( Validation ), 오류 처리 통합
스프링의 변환 서비스 통합( ConversionService )
2. 타임리프 사용
타임리프 템플릿 엔진을 스프링 빈에 등록하고, 타임리프용 뷰 리졸버를 스프링 빈으로 등록해야함
build.gradle에 타임리프 의존관계를 추가하면 이와 관련된 라이브러리를 다운받고, 타임리프 설정용 스프링 빈을 자동으로 등록한다
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
타임리프 관련 설정을 application.properties
에서 변경할 수 있다 ( 아래 링크에서 thymeleaf 검색해서 필요한 설정 찾으면 됨 )
th:object
커맨드 객체를 지정한다
<form>
에서 사용할 객체를 지정한다
*{...}
: 선택 변수 식
th:object
에서 선택한 객체에 접근할 때 사용th:field
// Controller
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "form/editForm";
}
<!-- Thymeleaf ( editForm.html ) -->
<form action="item.html" th:action th:object = "${item}" method="post">
<input type="text" id="itemName" class="form-control" th:field = "*{itemName}">
</form>
<!-- 페이지 소스 보기 ( editForm.html ) -->
<form action="" method="post">
<input type="text" id="itemName" class="form-control" name="itemName" value="itemA">
</form>
th:object
를 사용하기 위해 Controller에서 model에 item이라는 이름으로 객체를 전달th:object
에 item이 들어감 ➜ th:field
에 지정된 itemName은 item.itemName이라는 의미
*{itemName}
= ${item.itemName}
th:field
는 지정한 선택변수 이름으로 현재 태그의 id, name, value 속성을 자동으로 만들어준다
th:field
가 itemName이기 때문에 현재 태그의 name, value 속성들이 itemName으로 자동 처리
id 속성은 태그에 적어주었기 때문에 생성되지 않았음
th:object
를 사용하는 경우, Controller에서 비어있는 Item 객체를 새로 생성해서 model로 넘겨주어야 한다<!-- addForm.html -->
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input">
</div>
체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어간다
스프링은 on 이라는 문자를 true 타입으로 변환해준다
getOpen()값을 로그를 확인했을 때 true가 출력
HTML에서 체크 박스를 선택하지 않고 폼을 전송하면 open 이라는 필드 자체가 서버로 전송되지 않는다
null
이 출력문제점
선택하지 않은 경우, 아예 필드 자체가 넘어가지 않기 때문에 수정하는 경우에 문제가 발생
체크되어 있던 값을 해제한 경우, 아무 값도 넘어가지 않기 때문에 수정이 제대로 이루어지지 않을 수도 있음
참고> HTTP 요청 메시지를 서버에서 보고 싶은 경우, application.properties에 아래 코드 추가
logging.level.org.apache.coyote.http11=debug
<!-- addForm.html -->
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input">
<input type="hidden" name="open" value="on"/>
</div>
히든필드를 추가해서 문제를 해결한다
히든필드의 name을 기존 체크박스 name에 지정된 이름 앞에 _를 붙인 값으로 지정
체크박스의 name이 open이기 때문에 히든필드의 name은 _open
히든필드 동작
히든필드는 무조건 서버로 넘어감
체크박스를 체크하면 open=on&_open=on
이 넘어가게 되고, 스프링MVC가 open에 값이 있는 것을 확인하고 _open은 무시
체크박스를 체크하지 않으면 _open만 있게 되기 때문에 open이 체크되지 않았다고 인식 ➜ getOpen()값이 false
로 출력된다
<!-- Thymeleaf -->
<div class="form-check">
<input type="checkbox" id="open" name="open" th:field="${item.open} "class="form-check-input">
</div>
<!-- 페이지 소스 보기 -->
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input" value="true">
<input type="hidden" name="_open" value="on"/>
</div>
th:field
를 사용하면 히든필드를 추가하지 않아도 타임리프가 자동으로 히든필드를 생성해준다<!-- 페이지 소스 보기 ( 체크되지 않은 경우 ) -->
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input" disabled value="true">
</div>
<!-- 페이지 소스 보기 ( 체크된 경우 ) -->
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input" disabled value="true" checked="checked">
</div>
체크박스의 경우, checked 속성이 있으면 화면에 체크된 것으로 표시되고 속성이 없으면 체크되지 않은 것으로 표시한다
th:field
를 사용했을 때 value 값이 true인 경우, 타임리프는 checked 속성을 자동으로 추가해준다
@ModelAttribute
// Controller
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
model.addAttribute("regions", regions);
return "form/addForm";
}
// Controller
public class 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;
}
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "form/addForm";
}
}
메서드에 @ModelAttribute
가 붙게 되면, 메서드에서 반환하는 것이 지정된 이름으로 model에 담긴다
Controller의 어떤 메서드가 호출되어도 addAttribute()를 통해 자동으로 담기게 된다
<!-- Thymeleaf -->
<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 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>
th:each
를 통해 모델에 담긴 Map을 반복th:field
의 *{regions}
는 ${item.regions}
를 의미
위의 form에 th:object="${object}"
로 설정되어 있음
지역을 체크하고 등록 버튼을 누르면 Item 객체의 regions( List 객체 )에 선택한 값이 들어가게 됨
th:value
는 반복변수의 key 값이 들어감 ( 반복변수는 Map을 반복 )label은 체크 박스 옆에 뜨는 글자인데 위의 코드는 글자를 누르면 체크박스가 선택되도록 작성되어 있음
label의 for 속성이 input 태그의 id와 연결되어 있기 때문에 가능 ( 클릭 영역이 확장 )
이것이 가능하려면 label이 체크박스의 id를 알아야하는데 id를 지정해주지 않았고 th:field
를 통해 자동으로 생성됨
이처럼 동적으로 생성되는 id를 사용할 수 있도록 ids를 제공해준다
${#ids.prev('regions')}
의 regions는 th:field
의 regions를 의미
<!-- 페이지 소스 보기 -->
<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>
단일 체크박스의 경우와 동일하게 자동 처리해준 것을 확인 가능
_regions로 히든필드가 생긴 것을 확인할 수 있음
[ ]
가 출력됨 ( null로 출력 X )체크되어 있는 경우, 자동으로 checked 속성이 생성됨
th:value
에 Map의 모든 Key가 들어있고 이것을 가지고 th:filed
의 값을 확인하는데 th:value
에 있는 값이 th:filed
에 있으면 checked 속성을 추가// Controller
@ModelAttribute("itemTypes")
public ItemType[] itemType() {
return ItemType.values(); // 해당 Enum의 모든 정보를 배열로 반환
}
<!-- Thymeleaf -->
<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>
<!-- 페이지 소스 보기 -->
<div class="form-check form-check-inline">
<input type="radio" value="BOOK" class="form-check-input" id="itemType1" name="itemType">
<label for="itemType1" class="form-check-label">도서</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" value="FOOD" class="form-check-input" id="itemType2" name="itemType">
<label for="itemType2" class="form-check-label">음식</label>
</div>
${itemTypes}
: itemTypes은 모델명*{itemType}
= ${item.itemType}
${type.name()}
: Enum의 name을 String으로 반환해준다${type.description}
: 프로퍼티 접근법으로 반복변수 type을 통해 Enum의 description을 가져온다getItemType()을 로그로 확인해보면 Enum의 name( th:value
에 들어있는 값 )이 확인된다
체크하지 않으면 null로 출력되는데 라디오 버튼은 null이어도 된다
수정 시, 라디오 버튼은 한 가지가 선택되면 변경은 가능해도 아예 선택 하지 않을 수는 없기 때문
<!-- 체크된 경우 페이지 소스 보기 -->
<div class="form-check form-check-inline">
<input type="radio" value="BOOK" class="form-check-input" disabled id="itemType1" name="itemType">
<label for="itemType1" class="form-check-label">도서</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" value="FOOD" class="form-check-input" disabled id="itemType2" name="itemType" checked="checked">
<label for="itemType2" class="form-check-label">음식</label>
</div>
<!-- 체크되지 않은 경우 페이지 소스 보기 -->
<div class="form-check form-check-inline">
<input type="radio" value="BOOK" class="form-check-input" disabled id="itemType1" name="itemType">
<label for="itemType1" class="form-check-label">도서</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" value="FOOD" class="form-check-input" disabled id="itemType2" name="itemType">
<label for="itemType2" class="form-check-label">음식</label>
</div>
체크박스와 조금 다른 자동 처리
th:value
와 th:field
의 값이 같으면 checked 속성을 넣어준다
라디오버튼은 히든필드를 생성하지 않는다
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
위에서는 Enum의 값들을 model에 담아서 전달했는데 SpringEL 문법으로 Enum에 직접 접근할 수 있다
경로를 모두 적어주어야 하며, values()를 통해 ENUM의 모든 정보가 배열로 반환된다
// Controller
@ModelAttribute("deliveryCodes")
public List<DeliveryCode> deliveryCodes() {
List<DeliveryCode> deliveryCodes = new ArrayList<>();
deliveryCodes.add(new DeliveryCode("FAST", "빠른배송"));
deliveryCodes.add(new DeliveryCode("NORMAL", "일반배송"));
return deliveryCodes;
}
<!-- Thymeleaf -->
<select th:field="*{deliveryCode}" class="form-select">
<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="FAST">빠른배송</option>
<option value="NORMAL">일반배송</option>
</select>
*{deliveryCode}
= ${item.deliveryCode}
option 태그를 여러 개 생성해야 하기 때문에 option 태그의 속성으로 th:each를 사용
${deliveryCode.code}
, ${deliveryCode.displayName}
는 프로퍼티 접근법으로 반복변수를 통해 객체의 프로퍼티에 접근
<!-- 하나가 선택된 경우 페이지 소스 보기 -->
<select class="form-select" disabled id="deliveryCode" name="deliveryCode">
<option value="FAST" selected="selected">빠른배송</option>
<option value="NORMAL">일반배송</option>
</select>
<!-- 아무것도 선택되지 않은 경우 페이지 소스 보기 -->
<select class="form-select" disabled id="deliveryCode" name="deliveryCode">
<option value="FAST">빠른배송</option>
<option value="NORMAL">일반배송</option>
</select>
선택되는 경우, option 태그에 selected 속성이 들어간다
getDeliveryCode()값을 로그로 찍어보면 선택한 경우 code 값이 출력되지만 아무것도 선택되지 않은 경우 아무것도 출력되지 않는다