[Spring MVC 2편] 2. Thymeleaf Form

HJ·2023년 1월 15일
0

Spring MVC 2편

목록 보기
2/13
post-thumbnail

김영한 님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard


1. Thymeleaf와 Spring

1. 타임리프와 스프링 통합으로 추가되는 기능

  • 스프링 SpringEL 문법 통합

  • ${@myBean.doSomething()} 처럼 스프링 빈 호출 지원

  • 폼 관리를 위한 추가 속성을 제공

    • th:object (기능 강화, 폼 커맨드 객체 선택)

    • th:field , th:errors , th:errorclass

  • 폼 컴포넌트 기능

    • checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원
  • 스프링의 메시지, 국제화 기능의 편리한 통합

    • 국제화 : 미국에서 접속하면 영어로, 한국에서 접속하면 한국어로 보이게 하는 기능
  • 스프링의 검증( Validation ), 오류 처리 통합

  • 스프링의 변환 서비스 통합( ConversionService )


2. 타임리프 사용

  • 타임리프 템플릿 엔진을 스프링 빈에 등록하고, 타임리프용 뷰 리졸버를 스프링 빈으로 등록해야함

  • build.gradle에 타임리프 의존관계를 추가하면 이와 관련된 라이브러리를 다운받고, 타임리프 설정용 스프링 빈을 자동으로 등록한다

    • implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
  • 타임리프 관련 설정을 application.properties에서 변경할 수 있다 ( 아래 링크에서 thymeleaf 검색해서 필요한 설정 찾으면 됨 )




2. 입력 폼 처리

2-1. 폼 관련 타임리프 문법

  • th:object

    • 커맨드 객체를 지정한다

    • <form>에서 사용할 객체를 지정한다

  • *{...} : 선택 변수 식

    • th:object 에서 선택한 객체에 접근할 때 사용
  • th:field

    • HTML 태그의 id , name , value 속성을 자동으로 처리

2-2. 예시

// 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 속성은 태그에 적어주었기 때문에 생성되지 않았음

  • addForm.html에서 th:object를 사용하는 경우, Controller에서 비어있는 Item 객체를 새로 생성해서 model로 넘겨주어야 한다



3. 순수 HTML 체크박스

3-1. 문제점

<!-- addForm.html -->
<div class="form-check">
    <input type="checkbox" id="open" name="open" class="form-check-input">
</div>
  • input 태그의 name 속성에 "open"으로 지정되어 있기 때문에 Item의 open 필드에 값이 들어간다
  • 체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어간다

    • 스프링은 on 이라는 문자를 true 타입으로 변환해준다

    • getOpen()값을 로그를 확인했을 때 true가 출력

  • HTML에서 체크 박스를 선택하지 않고 폼을 전송하면 open 이라는 필드 자체가 서버로 전송되지 않는다

    • getOpen()값을 로그를 확인헀을 때 null이 출력
  • 문제점

    • 선택하지 않은 경우, 아예 필드 자체가 넘어가지 않기 때문에 수정하는 경우에 문제가 발생

    • 체크되어 있던 값을 해제한 경우, 아무 값도 넘어가지 않기 때문에 수정이 제대로 이루어지지 않을 수도 있음

  • 참고> HTTP 요청 메시지를 서버에서 보고 싶은 경우, application.properties에 아래 코드 추가

    • logging.level.org.apache.coyote.http11=debug

3-2. 문제 해결

<!-- 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로 출력된다




4. Thymeleaf 체크박스

4-1. 상품 등록 폼

<!-- 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를 사용하면 히든필드를 추가하지 않아도 타임리프가 자동으로 히든필드를 생성해준다

4-2. 상품 상세 폼

<!-- 페이지 소스 보기 ( 체크되지 않은 경우 ) -->
<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 속성을 자동으로 추가해준다




5. 멀티 체크박스

5-1. @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에 위의 코드를 추가해야하는데 화면이 표시되는 모든 메서드에 동일한 코드를 중복해서 작성해야하는 문제가 발생

// 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()를 통해 자동으로 담기게 된다


5-2. 상품 등록 폼

<!-- 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를 의미


5-3. 상품 상세 폼

<!-- 페이지 소스 보기 -->
<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로 히든필드가 생긴 것을 확인할 수 있음

      • getResions()를 로그를 확인했을 때, 아무것도 체크하지 않으면 [ ] 가 출력됨 ( null로 출력 X )
    • 체크되어 있는 경우, 자동으로 checked 속성이 생성됨

      • th:value에 Map의 모든 Key가 들어있고 이것을 가지고 th:filed의 값을 확인하는데 th:value에 있는 값이 th:filed에 있으면 checked 속성을 추가



6. 라디오 버튼

6-1. 상품 등록 폼

// 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이어도 된다

    • 수정 시, 라디오 버튼은 한 가지가 선택되면 변경은 가능해도 아예 선택 하지 않을 수는 없기 때문


6-2. 상품 상세 폼

<!-- 체크된 경우 페이지 소스 보기 -->
<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:valueth:field의 값이 같으면 checked 속성을 넣어준다

    • 라디오버튼은 히든필드를 생성하지 않는다




7. 타임리프에서 Enum 직접 접근하기

<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
  • 위에서는 Enum의 값들을 model에 담아서 전달했는데 SpringEL 문법으로 Enum에 직접 접근할 수 있다

  • 경로를 모두 적어주어야 하며, values()를 통해 ENUM의 모든 정보가 배열로 반환된다




8. 셀렉트 박스

8-1. 상품 등록 폼

// 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}는 프로퍼티 접근법으로 반복변수를 통해 객체의 프로퍼티에 접근


8-2. 상품 상세 폼

<!-- 하나가 선택된 경우 페이지 소스 보기 -->
<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 속성이 들어간다

    • 이전과 동일하게 값을 비교하면서 일치하는 태그에 selected 속성을 넣어준다
  • getDeliveryCode()값을 로그로 찍어보면 선택한 경우 code 값이 출력되지만 아무것도 선택되지 않은 경우 아무것도 출력되지 않는다

    • null, false 등이 출력되는 것이 아니라 아예 출력되지 않는다
profile
공부한 내용을 정리해서 기록하고 다시 보기 위한 공간

0개의 댓글