(스프링 mvc2) 타임리프 - 스프링 통합과 폼

짜스의 하루 ·2024년 3월 9일

타임리프 스프링 통합

스프링 통합으로 추가되는 기능

  • 스프링의 SpringEL문법 통합
  • ${@myBean.doSomething()} 처럼 스프링 빈 호출
  • 편리한 폼 관리를 위한 추가 속성들이 제공된다.
  • th:object (기능 강화, 폼 커맨드 객체 선택)
  • th:field , th:errors , th:errorclass
  • 폼 컴포넌트 기능
  • 스프링의 메시지, 국제화 기능의 편리한 통합
  • 스프링의 검증, 오류 처리 통합
  • 스프링의 변환 서비스 통합(ConversionService)

build.gradle에 다음 한줄을 넣어주면 Gradle은 타임리프와 관련된 라이브러리를 다운로드 받고, 스프링 부트는 앞서 설명한 타임리프와 관련된 설정용 스프링 빈을 자동으로 등록해준다.


입력 폼 처리

  • th:object : 커맨드 객체를 지정한다.
    "th:object" 속성은 주로 폼(form) 요소와 관련이 있다.

    --> 이 속성은 폼 요소에서 사용할 객체를 지정하는 데에 쓰입니다.
    예를 들어, 폼을 통해 사용자로부터 입력을 받을 때, 이 입력 값을 처리하기 위한 Java 객체를 Thymeleaf로 지정하는 데 사용됩니다.

  • *{...} : 선택 변수식이라고 한다.(th:object에서 선택한 객체에 접근할 수 있다
  • th:field : HTML 태그의 id, name, value의 속성을 자동 처리해준다.

렌더링 전
<input type="text" th:field="*{itemName}"/>
렌더링 후
<input type="text" id="itemName" name="itemName" th:value="*{itemName}" />

상품 등록 폼
--> FormItemController

  • th:object를 적용하기 위해서는 해당 오브젝트 정보를 먼저 넘겨주어야 한다. 등록 폼이기 때문에, 데이터가 비어있는 빈 오브젝트 를 만들어서 뷰에 전달한다(model에 new Item()으로 빈 객체를 생성해서 넘겨준 것이다)

addForm.html

  • th:object="${item}" : <form>에서 사용할 커맨드 객체를 지정한다. --> 그러면 선택 변수 식(*{...}를 사용할 수 있다)
  • th:field="*{itemName}": *{itemName} 는 선택 변수 식을 사용했는데, --> ${item.itemName}과 같다.
    앞서 th:object로 item을 사용했기 때문에, 선택 변수식을 사용할 수 있다.
  • th:field는 id, name, value 속성을 모두 자동으로 만들어준다.

    id : th:field 에서 지정한 변수 이름과 같다. ( id="itemName" )
    name : th:field 에서 지정한 변수 이름과 같다. ( name="itemName" )
    value : th:field 에서 지정한 변수의 값을 사용한다. ( value="" )


상품 수정 폼
--> FormItemController

editForm.html

  • 수정 폼도 앞서 설명한 내용과 같다. 수정 폼의 경우 id , name , value 를 모두 신경써야 했는데, 많은 부분이 th:field 덕분에 자동으로 처리되는 것을 확인할 수 있다

요구사항 출력

타임리프를 사용해서 폼에서 체크박스, 라디오 버튼, 셀렉트 박스를 편리하게 사용하는 방법을 학솝해보자
예시 이미지

ItemType(상품 종류) enum 생성

  • enum은 열거형 데이터 유형을 나타낸다. 열거형은 몇 가지 고정된 값들을 나열하여 표현하는 자료형이다.
public enum ItemType {

    BOOK("도서"), FOOD("음식"), ETC("기타");
    
    private final String description;

    ItemType(String description) {
        this.description = description;
    }
}
  • 설명을 위해 description을 추가했다.

DeliveryCode(배송 방식) 클래스 생성

@Data
@AllArgsConstructor
public class DeliveryCode {
    
    private String code;
    private String displayName;
}
  • 배송 방식은 DeliveryCode 라는 클래스를 사용한다.
    --> code 는 FAST 와 같이 시스템에서 전달하는 값이고, displayName 은 빠른 배송 과 같은 고객에게 보여주는 값이다.

  • @Data: 롬복 애노테이션 중 하나로, 이 애노테이션을 클래스에 추가하면 롬복이 자동으로 toString(), equals(), hashCode(), getter 및 setter 메서드를 생성해준다.

  • @AllArgsConstructor: 롬복 애노테이션 중 하나로, 이 애노테이션을 클래스에 추가하면 모든 필드를 인자로 받는 생성자를 생성해준다. 이는 객체를 생성할 때 모든 필드를 한 번에 초기화하기 위해 사용된다.

Item 클래스 수정

 private Boolean open; //판매여부(체크박스)
 private List<String> regions; //등록 지역
 private ItemType itemType; //상품 종류
 private String deliveryCode; //배송 방식

체크 박스 - 단일1

단순 HTML 체크 박스(먼저 타임리프를 사용하지 않고 순수 HTML로 처리해볼 것이다)

addForm.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>

FormItemController 클래스
log.info("item.open={}", item.getOpen());

  • 상품이 등록이 되는 곳에 다음과 같은 로그를 남겨서 값이 잘 넘어오는지 확인해볼 것이다
  • 클래스에 @Slf4j 애노테이션을 추가했다.

이후 체크 박스를 체크한 후 실행을 시키면

정상적으로 실행이 되며, opne=on이라는 값이 넘어간다.
--> 스프링은 on 이라는 문자를 (Boolean 타입이라면) true 타입으로 변환해준다.

체크 박스를 체크하지 않은 후 실행을 시키면

다음과 같은 null값이 나온다.

왜 null값이 나온걸까?

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

addForm.html
<input type="hidden" name="_open" value="on" />
히든 필드를 추가한 후 다시 실행시켜보자

  • _open=on : 체크박스를 체크하지 않으면 스프링 MVC가 _open만 있는 것을 확인하고, opne의 값이 체크 되지 않았다고 인식한다.

체크 박스 - 단일2

타임리프가 제공하는 폼 기능을 사용해보자

addForm.html
판매 여부 체크 박스의 기존 코드를 제거하고, 타임리프가 제공하는 체크 박스 코드로 변경해보자

<div>판매 여부</div>
        <div>
            <div class="form-check">
                <input type="checkbox" id="open" name="open" th:field="*{open}" class="form-check-input">
                <input type="hidden" name="_open" value="on" />
                <label for="open" class="form-check-label">판매 오픈</label>
            </div>
        </div>
  • th:field="${item.open}"으로 작성해도 무관하다.

실행해보자

  • 타임리프를 사용하면 체크박스의 히든 필드와 관련된 부분도 함께 해결해준다.
    --> HTML생성 결과를 보면 히든 필드 부분이 자동으로 생성되어 있다.
  • 따라서 판매 여부(체크박스를 선택하거나 / 선택하지 않은 경우)로그는 다음과 같이 나온다

상품 상세에도 적용해보자 - item.html

    <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>
  • (참고) disabled를 사용해서 상품 상세에서는 체크박스가 선택되지 않도록 설정해두었다.

타임리프의 체크 확인

  • checked="checked"
  • 체크박스에서 판매 여부를 선택해서 저장하면, 조회시에 checked 속성이 추가된 것을 볼 수 있다.
    --> 타임리프의 th:field 를 사용하면, 값이 true 인 경우 체크를 자동으로 처리해준다.

상품 수정에도 적용해보자 - editForm.html

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

ItemRepository : update()코드도 수정하자
findItem.setOpen(updateParam.getOpen());

  • 수정 처리가 되도록 update()코드 위에 추가해 주었다.


    이렇게 수정이 가능해졌다

  • 판매 여부를 체크하여 수정 후, 수정 페이지로 접근해보면, 판매 여부가 자동으로 선택된 것을 확인할 수 있고, 페이지 소스보기를 하면 checked="checked" 를 확인할 수 있다.

  • 판매 여부를 체크 해제 하고 수정 후, 수정 페이지로 접근해보면, 판매 여부가 선택되지 않은 것을 확인할 수 있고, 페이지 소수보기를 하면 checked="checked" 가 없는 것을 확인할 수 있다.


체크박스 - 멀티

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

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

먼저, 위와 같이 등록 지역을 추가하려면(등록 폼, 상세 폼, 수정 폼) 모두 각 페이지에서 서울 부산 제주를 선택할 수 있도록 하기 위해 컨트롤러에서 데이터를 넘겨주어야 한다.

이러한 중복을 해결하기 위해 스프링은 특별한 기능을 제공해준다

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;
    }

@ModelAttribute

  • 등록 폼, 상세 화면, 수정 폼에서 모두 서울, 부산, 제주라는 체크 박스를 반복해서 보여주어야 한다. 이렇게 하려면 각각의 컨트롤러에서 model.addAttribute()을 사용해서 체크 박스를 구성하는데 데이터를 반복해서 넣어주어야 한다.
  • @ModelAttribute() 는 이렇게 컨트롤러에 있는 별도의 메서드에 적용할 수 있다.
    --> 이렇게 하면, 해당 컨트롤러를 호출할 때, regions에서 반환한 값이 자동 모델에 담기게 된다.

  • model.addAttribute("regions", regions); 가 FormItemController 컨트롤러의 각 메서드에 자동으로 적용된다고 보면 된다.

addForm.html - 상품 등록 폼

        <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>
  • 멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있다.
    하지만, 생성된 HTML 태그 속성에서 name은 같아도 되지만, id 값은 모두 달라야 한다.
    --> 따라서 타임리프는 체크박스를 each 루프 안에서 반복해서 만들 때, 임의로 1,2,3 숫자를 뒤에 붙여준다. (ex. id="regions1" , id="regions2" , id="regions3")
  • th:field에 의해 타임리프가 체크박스 id를 자동 생성할 때, 루프가 수행되면서, 뒤에 인덱스(1,2,3...)을 자동으로 생성해준다.
  • th:for="${#ids.prev('regions')}"
    --> HTML의 id가 타임리프에 의해 동적으로 만들어지기 때문에
<label for="id값">

으로 label 의 대상이 되는 id 값으로 임의로 지정하는 것은 곤란하다.
--> 타임리프는 ids.prev(...) , ids.next(...) 을 제공해서 동적으로 생성되는 id 값을 사용할 수 있도록 한다.
(ex. label for="regions1", label for="regions2", label for="regions3")

출력 결과(log로 출력)

  • 서울과 부산을 선택했을 때 : item.regions=[SEOUL,BUSAN]

  • 아무것도 선택하지 않았을 떄 : item.regions=[]

  • 로그를 보면, 빈 배열이 들어간 것을 볼 수 있다.

  • _regions는 앞서 설명한 기능이다. 웹 브라우저에서 체크를 하나도 하지 않았을 때, 클라이언트가 서버에 아무런 데이터를 보내지 않는 것을 방지한다.

item.html - 상세 페이지와 editForm.html도 동일하게 추가해주면 된다.

타임리프의 체크 확인

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


라디오 버튼

라디오 버튼은 여러 선택지 중 하나를 선택할 수 있다. ENUM을 사용해서 적용해보자
상품 종류

  • 도서, 식품, 기타
  • 라디오 버튼으로 하나만 선택할 수 있다

FormItemController

@ModelAttribute("itemTypes")
    public ItemType[] itemTypes(){
        ItemType[] values = ItemType.values();
        return values;
    }

  • itemTypes를 (등록, 조회, 수정)에서 모두 사용해야하므로 @ModelAttribute를 이용해보자
  • ItemType.values()를 사용하면 해당 ENUM의 모든 정보를 배열로 반환한다.
    예) [BOOK, FOOD, ETC]

addForm.html - 상품 등록 폼

 <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>
  • 이전에 체크 박스햇던 것과 유사하다
  • th:value="${type.name()}" : Enum의 name()에 해당, enum의 이름을 그대로 문자로 반환한다.

출력 결과

  • 라디오 버튼은 이미 선택이 되어 있다면, 수정시에도 항상 하나를 선택하도록 되어 있으므로 체크 박스와 달리 별도의 히든 필드를 사용할 필요가 없다
  • 타임리프는 th:field 에 지정한 값과 th:value 의 값을 비교해서 체크를 자동으로 처리해준다.

item.html, editForm.html 둘다 같은 코드를 추가해주면 된다


셀렉트 박스

셀렉트 박스는 여러 선택지 중에 하나를 선택할 수 있다

배송 방식

  • 빠른 배송
  • 일반 배송
  • 느린 배송
  • 셀렉트 박스로 하나만 선택할 수 있다.

ForItemController 코드 추가

@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;

    }
  • DeliveryCodes 라는 자바 객체를 사용하는 방법을 적용해보자
  • 이전에 사용했던 것처럼 DeliveryCodes도 (등록 폼, 상세, 수정 폼)에서 모두 사용하므로 @ModelAttribute 애노테이션 사용
  • @ModelAttribute 가 있는 deliveryCodes() 메서드는 컨트롤러가 호출될 때 마다 사용되므로 deliveryCodes 객체도 계속 생성된다. 이런 부분은 미리 생성해두고 재사용하는 것이 더 효율적이다.

addForm.html

<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, editForm.html도 모두 배송 방식을 추가해 주었다.

실행 결과

  • 빠른 배송을 눌렀을 때, selected="selected" 처리를 해주었다
    --> 타임리프는 th:field에 지정된 값과 option에 있는 th:value 의 값을 비교해서 selected를 자동으로 처리해준다.
profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글