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

[Spring Boot & Thymeleaf] 20. 체크 박스 단일 선택

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

타임리프 - 체크 박스 단일 선택

단순 HTML 체크 박스 - 타임리프 미적용

타임리프를 적용하지 않은 단순 HTML 체크 박스를 알아보자.

resources/templates/form/addForm.html 내용 추가
<hr class="my-4">

<!-- 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>
FormItemController.java 내용 추가
//생략
@Slf4j
public class FormItemController {
    @PostMapping("/add")
    public String addItem(@ModelAttribute Item item,
                        RedirectAttributes redirectAttributes) {
        log.info("item.open={}", item.getOpen());
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/form/items/{itemId}";
    }
    //생략...
}
  • @Slf4j 어노테이션을 추가했다.
  • log.info("item.open={}", item.getOpen());
    • open 이라는 파라미터 값이 잘 들어와서 item 객체에 저장되어있는지 출력해본다.
application.properties
logging.level.org.apache.coyote.http11=debug
  • 모든 HTTP 요청 메시지를 로그로 출력한다.
  • 폼의 입력 값이 잘 넘어오는지 확인할 수 있다.
실행 결과 로그
FormItemController : item.open=true //체크 박스에 체크한 경우
FormItemController : item.open=null //체크 박스에 체크 안 한 경우
  • 체크 박스에 체크하면 HTML Form에서 open=on 이라는 값을 넘긴다.
  • Spring은 on 이라는 값을 boolean 타입인 true 값으로 변환해준다.
    • Spring의 타입 컨버터가 이 기능을 수행한다. 자세한 내용은 뒤에서 설명한다.
체크 여부에 따른 HTTP 요청 메시지 바디
itemName=asd&price=123&quantity=1&open=on] //체크 박스에 체크한 경우

itemName=asd&price=123&quantity=1] //체크 박스에 체크 안 한 경우
  • 체크하지 않은 경우 아예 open 파라미터가 존재하지 않는다!

주의! - 순수 HTML 체크 박스에서 체크를 하지 않는 경우

  • 순수 HTML Form에서 체크 박스에 체크를 하지 않고 입력 값을 전송하면, 아예 해당 파라미터가 넘어가지 않는다!
  • 즉 여기서는 open 이라는 파라미터 자체가 서버로 전송되지 않는다!
  • 그래서 item.open=null 이 출력되는 것이다.
  • 상황에 따라서 이 방식이 문제가 될 수 있다.
    • 처음부터 체크가 되어있는 체크박스가 있다고 하자.
    • 사용자가 의도적으로 체크를 해제해도, 폼 입력값 전송 시 해당 체크박스에 대한 값이 아예 넘어가지 않는다.
    • 따라서 서버는 값이 넘어오지 않는 경우를 감지하고, 체크 해제에 대한 올바른 동작을 추가로 처리해주어야 한다. 그렇지 않으면 값이 변경되지 않을 것이다.
  • 우리가 원하는 것은 open=off 라던가 open=false 같이 체크를 하지 않았다는 사실을 명확히 알 수 있는 어떤 값이 넘어오는 것이다.
    • 그렇게 되면 다른 로직을 추가하지 않고 단순히 값을 덮어 씌우는 것으로 해결이 가능해진다.

해결 방법 - Spring MVC의 히든 필드

<input type="checkbox" id="open" name="open" class="form-check-input">
<input type="hidden" name="_open" value="on" />
  • 이런 문제를 해결하기 위해서 스프링 MVC는 히든 필드를 사용하는 방법을 제공한다.
  • name=_open 처럼 원하는 체크 박스의 name 속성 값 앞에 언더스코어( _ )를 붙인 히든 필드를 만든다.
  • 히든 필드는 화면에 보이진 않지만, 값이 항상 전송된다.
  • 따라서 체크 박스의 체크를 해제한 경우 open=on 은 전송되지 않고, _open=on 만 전송되는데, 이 경우 스프링 MVC는 체크를 해제했다고 판단한다.
체크 여부에 따른 HTTP 요청 메시지 바디
itemName=asd&price=123&quantity=1&open=on&_open=on] //체크 박스에 체크한 경우

itemName=asd&price=123&quantity=1&_open=on] //체크 박스에 체크 안 한 경우
  • open=on&_open=on
    • 체크 박스에 체크하면 스프링 MVC가 open 파라미터 값이 존재하는 것을 확인하고 사용한다.
    • 이때 _open 은 무시한다.
  • _open=on
    • 체크 박스를 체크하지 않으면 스프링 MVC가 _open 파라미터만 있는 것을 확인한다. 이 때 스프링 MVC는 체크 박스가 체크되지 않았다고 인식한다.
    • 이 경우 서버에서 해당 값을 찍어보면 null 이 아니라 false 가 정상적으로 출력되는 것을 확인할 수 있다.

체크 박스 - 타임리프 적용

개발할 때 마다 이렇게 히든 필드를 추가하는 것은 상당히 번거롭다. 타임리프가 제공하는 폼 기능을 사용하면 히든 필드를 알아서 만들어준다.

form/addForm.html 내용 추가
<!-- 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>
  • <form> 태그에 th:object="${item}" 속성이 있기 때문에 *{...}* 으로 해당 오브젝트의 멤버 변수에 접근이 가능하다.
    • 물론 ${item.open} 으로 접근해도 상관 없다.
  • th:field="*{open}*" 속성을 적용했다.
타임리프 렌더링 결과
<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <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>
</div>
  • th:field="*{open}" 에 의해 다음과 같은 내용이 추가되었다.
    • name="open" value="true" 이 추가되었다. 체크박스에 체크를 한 뒤 내용을 전송하면 open=true 형태로 전송된다.
    • <input type="hidden" name="_open" value="on"/>
      • 히든 필드가 추가되어있다.
실행 로그
FormItemController : item.open=true //체크 박스를 선택하는 경우
FormItemController : item.open=false //체크 박스를 선택하지 않는 경우
  • 아까와 달라진 것은 타임리프가 히든 필드를 알아서 추가해주는 것 정도밖에 없기 때문에 결과도 동일하게 잘 나온다.

상품 상세에 적용

상품 상세 화면에서 해당 상품의 판매 여부가 체크 박스 형태로 나타나도록 기능을 추가해보자.

form/item.html 내용 추가
<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" th:field="${item.open}" class="form-check-input" disabled>
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>
  • 마찬가지로 th:field 속성을 사용했다.
  • 여기서는 <form> 태그에 th:object="${item}" 속성이 없으므로 *{...} 를 사용할 수 없다.
  • disabled 속성으로 체크 박스의 체크 여부를 변경할 수 없게 해야 한다.
타임리프 렌더링 결과
<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <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>
</div>
  • 상품 등록 시 판매 여부 체크 박스에 체크를 해서 등록하면, 상품 상세 화면에서 checked="checked" 속성이 추가되는 것을 확인 할 수 있다.
    • 체크 박스는 checked 속성의 값과 관계 없이 해당 속성이 존재하기만 하면 체크 표시가 된다.
    • 따라서 타임리프의 도움 없이 이런 부분을 처리하려면 매우 번거롭다.
    • th:field 를 사용하면 변수의 값이 true 인 경우 checked 속성을 만들고, 없으면 만들지 않는다.
  • name="open" value="true" 도 추가되어있는 것을 확인할 수 있다.
  • disabled 옵션에 의해 타임리프가 히든 필드를 추가하지 않은 것으로 보인다.

참고
앞에서 배운 th:checked="${item.open}" 으로 체크 표시를 할 수도 있다. 단 이때는 namevalue 속성은 따로 추가해줘야 한다.
그런데 상품 상세에서는 disabled 속성에 의해 체크 여부를 변경할 수 없으므로 name 속성과 value 속성이 따로 필요 없긴 하다.

상품 수정에 적용

form/editForm.html 내용 추가
<!-- 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>
  • 상품 등록에서 적용한 코드와 완전히 동일하다.
타임리프 렌더링 결과
<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" class="form-check-input" name="open" value="true" checked="checked"><input type="hidden" name="_open" value="on"/>
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>
  • item.getOpen() 의 값이 true 인 경우 checked="checked" 속성이 추가되는 것 말고는 상품 등록의 경우와 완전히 동일하다.
    • name="open" value="true" 속성이 추가되어있다.
    • 히든 필드가 추가되어있다.

주의: ItemRepository의 update() 코드 추가 필요

판매 여부 수정 내용이 실제로 적용되기 위해서는 open 멤버 변수의 값이 실제로 바뀌어야한다. 하지만 기존의 ItemRepositoryupdate() 메소드에는 값을 변경하는 코드가 없었다. 따라서 다음과 같이 코드를 변경해야 한다.

ItemRepository 의 update()
public void update(Long itemId, Item updateParam) {
    Item findItem = findById(itemId);
    findItem.setItemName(updateParam.getItemName());
    findItem.setPrice(updateParam.getPrice());
    findItem.setQuantity(updateParam.getQuantity());
    findItem.setOpen(updateParam.getOpen());
    findItem.setItemType(updateParam.getItemType());
    findItem.setDeliveryCode(updateParam.getDeliveryCode());
    findItem.setRegions(updateParam.getRegions());
}
  • open 외에도 추가된 멤버 변수들이 있으므로 코드를 미리 추가해줬다.

참고: 체크박스에서 th:fieldid 속성 추가
체크 박스에서 th:field 를 사용했을 때, id 속성이 없으면 타임리프가 알아서 id 속성을 만들어 준다. 이 때 체크 박스는 다중 선택이 가능하므로 만약 th:field=*{open} 이렇게 하면 id="open1" 이런식으로 숫자를 추가해서 id 속성을 만든다.
이 내용에 대해서는 체크박스 다중 선택 챕터에서 자세히 알아보자.

댓글