[Spring Boot][5] 2. 타임리프 - 스프링 통합과 폼

🏷 프로젝트 생성

스프링 부트 스타터 사이트로 이동해서 프로젝트 생성

✔️ 프로젝트 선택

  • Project: Gradle
  • Project Language: Java
  • Spring Boot: 2.5.x

✔️ Project Metadata

  • Group: hello
  • Artifact: form-start
  • Name: form-start
  • Package name: hello.form
  • Packaging: Jar
  • Java: 11

✔️ Dependencies

  • Spring Web
  • Lombok
  • Thymeleaf


🏷 타임리프 스프링 통합

📌 타임리프는 크게 2가지 메뉴얼을 제공한다
1️⃣ 기본 메뉴얼
2️⃣ 스프링 통합 메뉴얼
타임리프는 스프링 없이도 동작하지만, 스프링과 통합을 위한 다양한 기능을 편리하게 제공한다!

타임리프 관련 설정을 변경하고 싶다면 ⬇️아래의 링크⬇️를 참고해서 application.properties 에 추가하면 된다.

참고~참고~😉



🏷 입력 폼 처리

지금부터 타임리프가 제공하는 입력 폼 기능을 적용해서 기존 프로젝트의 폼 코드를 타임리프가 지원하는 기능을 사용해서 효율적으로 개선해보자❗️

1️⃣ 등록 폼

✔️ FormItemController 변경

  • th:object 를 적용하려면 먼저 해당 오브젝트 정보를 넘겨주어야 한다.
  • 등록 폼이기 때문에 데이터가 비어있는 빈 오브젝트를 만들어서 뷰에 전달하자.

✔️ form/addForm.html 변경

<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="${item}" : <form> 에서 사용할 객체를 지정한다. 선택 변수 식(*{...})을 적용할 수 있다.
  • th:field="*{itemName}"
    • *{itemName} 는 선택 변수 식을 사용했는데, ${item.itemName} 과 같다.
    • 앞서 th:object 로 item을 선택했기 때문에 선택 변수 식을 적용할 수 있다.
    • th:fieldid, name, value 속성을 모두 자동으로 만들어준다.
      • id : th:field 에서 지정한 변수 이름과 같다. id="itemName"
      • name : th:field 에서 지정한 변수 이름과 같다. name="itemName"
      • value : th:field 에서 지정한 변수의 값을 사용한다. value=""

📌 참고
해당 예제에서 id 속성을 제거해도 th:field 가 자동으로 만들어준다.


2️⃣ 수정 폼

✔️ form/editForm.html 변경

<form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="id">상품 ID</label>
            <input type="text" id="id" class="form-control" th:field="*{id}" readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" class="form-control" th:field="*{itemName}" >
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" class="form-control" th:field="*{price}">
        </div>
        <div>
  • 수정 폼은 앞서 설명한 내용과 같다.
  • 수정 폼의 경우 id, name, value 를 모두 신경써야 했는데, 많은 부분이 th:field 덕분에 자동으로 처리되는 것을 확인할 수 있다👍🏻


🏷 요구사항 추가

타임리프를 사용해서 폼에서 체크박스, 라디오 버튼, 셀렉트 박스를 편리하게 사용하는 방법을 학습해보자!
기존 상품 서비스에 다음 요구사항이 추가되었다 😊

  • 판매 여부
    • 판매 오픈 여부
    • 체크 박스로 선택할 수 있다.
  • 등록 지역
    • 서울, 부산, 제주
    • 체크 박스로 다중 선택할 수 있다.
  • 상품 종류
    • 도서, 식품, 기타
    • 라디오 버튼으로 하나만 선택할 수 있다.
  • 배송 방식
    • 빠른 배송
    • 일반 배송
    • 느린 배송
    • 셀렉트 박스로 하나만 선택할 수 있다.

✔️ ItemType - 상품 종류
✔️ 배송 방식 - DeliveryCode
✔️ Item - 상품

⬆️ 3개의 파일을 생성한 후 작업을 시작해보자!



🏷 체크박스 - 단일1

✔️ 단순 HTML 체크박스

<!-- 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 checkbox선택이 안되면 클라이언트에서 서버로 값 자체를 보내지 않는다.
  • 수정의 경우에는 상황에 따라서 이 방식이 문제가 될 수 있다.
    사용자가 의도적으로 체크되어 있던 값을 체크를 해제해도 저장시 아무 값도 넘어가지 않기 때문에,
    서버 구현에 따라서 값이 오지 않은 것으로 판단해서 값을 변경하지 않을 수도 있다.
  • 이런 문제를 해결하기 위해서 스프링 MVC는 약간의 트릭을 사용하는데, 히든 필드를 하나 만들어서, open처럼 기존 체크 박스 이름 앞에 언더스코어()를 붙여서 전송하면 체크를 해제했다고 인식할 수 있다❗️
  • 히든 필드는 항상 전송되기 때문에 체크를 해제한 경우 여기에서 open은 전송되지 않고, _open만 전송되는데, 이 경우 스프링 MVC는 체크를 해제했다고 판단한다!!

체크 해제를 인식하기 위한 히든 필드
<input type="hidden" name="_open" value="on"/>


✔️ 기존 코드에 히든 필드 추가

<!-- 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>

✔️ 실행 로그
- 체크 박스 체크 : open=on&_open=on

  • 체크 박스 미체크 : _open=on


🏷 체크박스 - 단일2

🤔 : 개발할 때 마다 이렇게 히든 필드를 추가해야 하는 거야? 귀찮은데,,

😉 : 타임리프가 제공하는 폼 기능을 사용하면 이런 부분을 자동으로 처리할 수 있단다^^


✔️ 타임리프 - 체크박스 코드 추가

<!-- 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>

⬆️ 타임리프 체크박스 HTML 생성 결과

  • <input type="hidden" name="_open" value="on"/>
  • 이처럼 타임리프를 사용하면 체크 박스의 히든 필드와 관련된 부분도 함께 해결해준다!!

위의 내용을 상품 상세상품 수정 에도 똑같이 적용하자 👍🏻



🏷 체크박스 - 멀티

체크 박스를 멀티로 사용해서, 하나 이상을 체크할 수 있도록 해보자! 예) 등록 지역


✔️ 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;
  }
  • 등록 폼, 상세화면, 수정 폼에서 모두 서울, 부산, 제주라는 체크 박스를 반복해서 보여주어야 한다.
  • 이렇게 하려면 각각의 컨트롤러에서 model.addAttribute(...) 을 사용해서 체크 박스를 구성하는 데이터를 반복해서 넣어주어야 한다.

✔️ 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>
  • th:for="${#ids.prev('regions')}"
  • 멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있다.
  • 그런데 문제는 이렇게 반복해서 HTML 태그를 생성할 때, 생성된 HTML 태그 속성에서 name 은 같아도 되지만, id 는 모두 달라야 한다❗️
  • 따라서 타임리프는 체크박스를 each루프 안에서 반복해서 만들 때 임의로 1,2,3 숫자를 뒤에 붙여준다!

✔️ each로 체크박스가 반복 생성된 결과 - id 뒤에 숫자가 추가

<input type="checkbox" value="SEOUL" class="form-check-input" id="regions1"
  name="regions">
  <input type="checkbox" value="BUSAN" class="form-check-input" id="regions2"
  name="regions">
  <input type="checkbox" value="JEJU" class="form-check-input" id="regions3"
  name="regions">
  • HTML의 id가 타임리프에 의해 동적으로 만들어지기 때문에 <label for="id 값"> 으로 label 의 대상이 되는 id 값을 임의로 지정하는 것은 곤란하다🤔
  • 타임리프는 ids.prev(...) , ids.next(...) 을 제공해서 동적으로 생성되는 id 값을 사용할 수 있도록 한다❗️

💡 타임리프에서 name은 겹쳐도 되지만, id는 겹쳐서는 안 된다!!


⬆️ 상품 등록 폼에서 서울, 부산을 선택하고 확인한 로그 결과👍🏻

만약 지역선택을 하지 않는다면 어떻게 될까😐❓
_regions=on&_regions=on&_regions=on
➡️ 앞서 배웠듯, 웹 브라우저에서 체크를 하나도 하지 않았을 때, 클라이언트가 서버에 아무런 데이터를 보내지 않는 것을 방지한다!


✔️ item.html에도 멀티 체크박스 부분 추가

  • (주의🤚🏻) item.html 에는 th:object 를 사용하지 않았기 때문에 th:field 부분에 ${item.regions} 으로 적어주어야 한다.

✔️ editForm.html에도 멀티 체크박스 부분 추가



🏷 라디오 버튼

라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용할 수 있다! 라디오 버튼을 ❗️자바 ENUM을 활용해서❗️ 개발해보자🙂 예) 상품 종류

(체크박스는 여러개 선택 가능)


✔️ FormItemController - 추가

@ModelAttribute("itemTypes")
  public ItemType[] itemTypes() {
      return ItemType.values();
  }
  • itemTypes 를 등록 폼, 조회, 수정 폼에서 모두 사용하므로 @ModelAttribute 의 특별한 사용법을 적용하자.
  • ItemType.values() 를 사용하면 해당 ENUM의 모든 정보를 배열로 반환한다.
    예) [BOOK, FOOD, ETC]

✔️ addForm.html - 추가

<!-- 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>
            </di

💡 라디오 버튼은 이미 선택이 되어 있다면, 수정시에도 항상 하나를 선택하도록 되어 있으므로 체크 박스와 달리 별도의 히든 필드를 사용할 필요가 없다!


✔️ item.html에도 라디오 버튼 부분 추가

  • (주의🤚🏻) item.html 에는 th:object 를 사용하지 않았기 때문에 th:field 부분에 ${item.itemType} 으로 적어주어야 한다.

✔️ editForm.html에도 라디오 버튼 부분 추가

💡 타임리프에서 ENUM 직접 사용하기
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
➡️ 스프링EL 문법을 사용하여 모델에 굳이 담지 않고도 직접 사용이 가능하지만, 이 방법을 추천하지는 않는다!



🏷 셀렉트 박스

셀렉트 박스는 여러 선택지 중에 하나를 선택할 때(❗️하나만 선택해야 함❗️) 사용할 수 있다! 예) 배송 방식


✔️ FormItemController - 추가

 @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;
    }
  • DeliveryCode 라는 자바 객체를 사용
  • DeliveryCode 를 등록 폼, 조회, 수정 폼에서 모두 사용하므로 여기서도 @ModelAttribute 사용!

✔️ addForm.html - 추가

<!-- 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>

✔️ item.html에도 셀렉트 박스 부분 추가

  • (주의🤚🏻) item.html 에는 th:object 를 사용하지 않았기 때문에 th:field 부분에 ${item.deliveryCode} 으로 적어주어야 한다.

✔️ editForm.html에도 셀렉트 박스 부분 추가


⬆️ 총 실행 결과 구우우우웃 👍🏻

좋은 웹페이지 즐겨찾기