본문 바로가기
Spring/Spring Boot & Thymeleaf

[Spring Boot & Thymeleaf] 21. 체크 박스 다중 선택

by Kloong 2022. 10. 24.

참고

더보기

Spring Boot & Thymeleaf 시리즈는 김영한 님의 "스프링 MVC 2편 - 백엔드 웹 개발 활용 기술" 강의를 정리한 글입니다. 글에 첨부된 사진은 해당 강의의 강의 자료에서 캡쳐한 것입니다. 제 Github에만 올려뒀다가, 정보 공유와 강의 홍보(?)를 위해 블로그에도 업로드합니다.

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

마크다운 형식으로 작성한 글을 블로그에 다시 올리는 거라 가독성이 많이 떨어집니다. 조금더 편하게 보시려면 아래의 Github repository에서 보시면 됩니다.

 

GitHub - Kloong1/TIL: Today I Learned.

Today I Learned. Contribute to Kloong1/TIL development by creating an account on GitHub.

github.com

타임리프 - 체크 박스 다중 선택

체크 박스를 여러개 사용해서, 다중 선택을 할 수 있게 해보자.

요구사항 일부

  • 등록 지역
    • 서울, 부산, 제주
    • 체크 박스로 다중 선택할 수 있다.

@ModelAttribute의 특별한 사용법

요구사항을 구현하기 위해서는 상품 등록 화면, 상품 상세 화면, 상품 수정 화면 모두에서 등록 지역인 서울, 부산, 제주 체크 박스를 보여줘야 한다. 이 등록 지역에 대한 정보는 컨트롤러에서 뷰로 넘겨줄 것이다.

FormItemController.java 내용 추가
@GetMapping("/add")
public String addForm(Model model) {
    model.addAttribute("item", new Item());

    Map<String, String> regions = new LinkedHashMap<>(); //key 순서 고정
    regions.put("SEOUL", "서울");
    regions.put("BUSAN", "부산");
    regions.put("JEJU", "제주");
    model.addAttribute("regions", regions);

    return "form/addForm";
}
  • Map<String, String> 에 등록 지역에 대한 정보를 넘겨줬다.
  • 문제는 이 코드를 상품 상세 정보에 대한 컨트롤러 메소드, 상품 수정에 대한 컨트롤러 메소드에도 추가해 줘야 한다는 것이다.
  • 이렇게 되면 중복이 발생한다.
  • 중복을 해결하기 위해 따로 메소드로 빼는 방법도 있지만, Spring이 제공하는 @ModelAttribute 를 사용하는 방법도 존재한다.
FormItemController.java 내용 추가
public class FormItemController {
    @ModelAttribute("regions")
    public Map<String,String> regions() {
        Map<String, String> regions = new LinkedHashMap<>(); //key 순서 고정
        regions.put("SEOUL", "서울");
        regions.put("BUSAN", "부산");
        regions.put("JEJU", "제주");
        return regions;
    }
}
  • 컨트롤러 클래스 안에있는 특정 메소드에 @ModelAttribute 가 붙어 있으면, 해당 컨트롤러 클래스의 모든 @RequestMapping 이 붙은 메소드가 호출될 때마다 그 메소드 호출 전에 @ModelAttribute가 붙은 메소드가 먼저 호출된다.
  • 이때 @ModelAttribute 가 붙은 메소드 실행 결과의 반환값은 자동으로 Model에 저장이된다. 이 때 key는 @ModelAttribute 어노테이션의 value 를 따른다.

참고
여기서는 등록 지역이 필요 없는 컨트롤러 메소드들도 많이 있으므로, 굳이 @ModelAttribute 말고 그냥 메소드로 따로 빼는게 더 낫지 않았을까 싶다.
혹은 등록 지역이 동적으로 변하지 않는다면, 미리 static 변수로 초기화 해두고 넘겨주기만 하는 방식을 사용하는 것이 더 효율적일 것이다.

체크박스 적용 - 상품 등록

form/addForm.html
<!-- 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>
  • <div th:each="region : ${regions}">
    • Model 에서 Map<String, String> regions 를 꺼내온다.
    • Map 의 모든 Entryth:each 로 반복한다.
  • <input type="checkbox" th:field="*{regions}" th:value="${region.key}" class="form-check-input">
    • th:object="${item}" 에 의해 *{regions}${item.regions} 이다.
    • th:field=*{regions} 에 의해 id, name 등의 속성이 렌더링 될 것이다.
    • 이 때 valueth:value="${region.key}" 에 의해 렌더링 된다. region 은 반복에 사용되는 변수임에 유의.
타임리프 렌더링 결과
<!-- 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>
  • th:each 에 의해 체크 박스가 여러 개 생성되었다.
  • th:field 에 의해 id, name 속성이 렌더링 되고, 히든 필드가 생겼다.
  • th:value 에 의해 value 속성이 렌더링 되었다.

멀티 체크 박스에서의 id 속성

  • th:field=*{regions} 는 타임리프에 의해 regions 라는 이름을 가지고 id, name, value 등의 속성으로 렌더링 된다.
  • 문제는 반복해서 HTML 태그를 생성할 때, name 속성은 같아도 되지만, id 속성은 모두 달라야한다.
  • 따라서 타임리프는 th:each 루프 안에서 th:field 를 사용하여 체크박스를 반복해서 만들 때 임의로 id 속성값에 넘버링을 한다.
    • id="regions1", id="regions2" 이런 식으로 만들어진다.

체크 박스와 <label>, 그리고 #ids

  • <label th:for="${#ids.prev('regions')}"
  • <label>for 속성값으로 특정 체크 박스의 id 속성값을 넣어주면, 위 이미지처럼 <label> 에 해당하는 부분(빨간 네모)을 클릭해도 체크 박스에 체크 할 수 있다.
  • 문제는 우리 예제에서 체크 박스의 idth:eachth:field 에 의해 동적으로 생성된다는 것이다.
  • 이런 경우를 위해서 타임리프는 #ids 유틸리티 객체를 제공한다.
  • for="${#ids.prev('regions')}" 이렇게 하면, 타임리프에서 th:field=*{regions} 를 렌더링하면서 사용한 id 의 넘버링 값을 가지고 for 속성값을 렌더링한다.
  • prev() 가 의미하는 것은 이전에 쓰였던 값을 가져온다는 것이다. 즉 바로 직전에 체크박스에서 id 를 렌더링 할 때 쓴 그 값을 사용한다는 것이다. <label> 에 대응하는 체크박스가 바로 앞에 있으므로 이렇게 사용하는 것이다.

실행 결과

서울, 부산 선택
HTTP 바디: regions=SEOUL&_regions=on&regions=BUSAN&_regions=on&_regions=on
로그: item.regions=[SEOUL, BUSAN]
선택 X
HTTP 바디: _regions=on&_regions=on&_regions=on
로그: item.regions=[]
  • item.regionsnull 이 아닌 빈 배열임에 주의하자.
  • 히든 필드에 의해 체크를 하나도 하지 않아도 _regions=on 이 보내지기 때문에, null 이 아닌 빈 배열이 들어간다. 아무것도 보내지 않으면 null 이 들어갈 것이다.
  • 사실 _regions가 체크박스 숫자만큼 보내질 이유는 없다. 뭐라도 보내지기만 하면 null 대신 비어있는 무언가를 알아서 채워넣을 수 있을테니 말이다. 그런데 그냥 타임리프가 체크 박스 수만큼 생성해서 보내는 거라서 별 신경 안써도 된다고 한다.

체크박스 적용 - 상품 상세, 상품 수정

상품 상세

form/item.html
<!-- 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>
  • 상품 등록에서 작성한 내용을 그대로 가져온 뒤 몇 가지 수정만 했다.
    • disabled 속성을 추가했다.
    • 상품 상세에서는 th:object="${item}" 속성을 사용하지 않으므로 *{regions}${item.regions} 로 고쳤다.

체크 여부 확인

  • 멀티 체크 박스에서 등록 지역을 선택해서 저장하면, 조회시에 checked="checked" 속성이 추가된 것을 확인할 수 있다.
  • 타임리프는 th:field 에 지정한 값과 th:value 의 값을 비교해서 checked 속성을 자동으로 처리한다.

상품 수정

form/editForm.html
<!-- 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>
  • 등록과 아예 동일하다.
  • Item 객체에 값이 존재하는 경우가 있다는 점만 다르다.

댓글