https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

 

Tutorial: Thymeleaf + Spring

Preface This tutorial explains how Thymeleaf can be integrated with the Spring Framework, especially (but not only) Spring MVC. Note that Thymeleaf has integrations for both versions 3.x and 4.x of the Spring Framework, provided by two separate libraries c

www.thymeleaf.org

 

Thymeleaf + Spring 기능

  • SpringEL문법 통합
  • 스프링 Bean호출 가능 ex) ${@MyBean.doSomething()}
  • 편리한 <form> 관리를 위한 속성들
    th:object, th:field, th:error, th:errorclass
  • checkbox, radio bution, List 등을 편리하게 사용할 수 있는 기능
  • 스프링 메시지, 국제화 지원
  • 스프링 검증, 오류 처리 지원
  • 스프링 변환 서비스 지원

 

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

=> build.gradle에 추가하면, 라이브러리를 다운로드하고, Thymeleaf관련 Bean을 등록해줌

 

 

입력 <form> 처리

- Thymeleaf가 제공하는 기능을 활용하여 <Form>을 효율적으로 다룰 수 있음

  • th:object : 폼 커맨드 객체, <form>에 적용하여, 해당 폼에서 다룰 객체를 선택함
  • *{...} : 선택 변수 식, th:object에 있는 객체에 접근
  • th:field : id, name, value속성을 자동으로 추가해줌
    <input type="text" th:field="*{itemName}">
    => 렌더링=> value에는 실제 값이 들어감
    <input type="text" id="itemName" name="itemName" value="">​

 

th:object

- th:object를 사용하기 위해서(HTML 렌더링을 위해서)는 Controller로부터 객체를 넘겨받아야 함

- 등록 화면(addForm)을 띄우는 Controller에 model에 빈 Item객체를 넘기는 코드를 추가

 

변경 전

@GetMapping("/add")
public String addForm(Model model) {    
    return "form/addForm";
}

변경 후

@GetMapping("/add")
public String addForm(Model model) {
    model.addAttribute("item", new Item()); // form 렌더링(th:object 를 위해 빈 객체 넘기기)
    return "form/addForm";
}

 

addForm.html 에 th:object작성

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

    ......

</form>

=> 결과

<form action="" method="post">
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" class="form-control" placeholder="이름을 입력하세요" name="itemName" value="">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" class="form-control" placeholder="가격을 입력하세요" name="price" value="">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" class="form-control" placeholder="수량을 입력하세요" name="quantity" value="">
    </div>
                
    ......

=> th:object="${item}"으로 해당 <form>에서 사용한 객체를 선택함 (Controller에서 넘긴 빈 Item객체를 선택함)

=> th:field=*{itemName}"을 사용하여 ${item}의 itemName을 적용함 (th:field="${item.itmeName}"과 동일한 내용)

=> th:field=*{itemName}"으로 해당 <input>에 id, name, value속성을 자동으로 추가할 수 있음 => 개발이 간편함

=> value속성에는 실제 Controller에서 넘겨준 Item객체의 값이 들어감 (지금은 빈 객체이므로 value="")

 

=> <form action="item.html" th:action ... >에서 th:action으로 인해 th:action=""으로 렌더링이 되는데, HTML에서 <form>의 action속성 값이 없는 경우, 해당 페이지에 접근한 URL + method속성 값으로 <form> 내용을 제출함

(GET /add으로 addForm을 띄움 => POST /add로 <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" th:field="*{id}" class="form-control" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" th:field="*{itemName}" class="form-control" value="상품A">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" th:field="*{price}" class="form-control" value="10000">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" th:field="*{quantity}" class="form-control" value="10">
    </div>
    
    ......

=> 결과

<form action="" method="post">
    <div>
        <label for="id">상품 ID</label>
        <input type="text" id="id" class="form-control" readonly name="id" value="1">
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" class="form-control" value="itemA" name="itemName">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" class="form-control" value="10000" name="price">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" class="form-control" value="10" name="quantity">
    </div>
    
    ......

=> th:field를 사용하지 않았다면, 개발자가 수정할 데이터를 보여주기 위해 각 <input>마다 value="${item. 필드명}"을 작성해줘야 했었음

=> th:field="*{item}"을 사용하여 자동으로 value속성과 속성 값을 만들 수 있음

=> addForm과 달리 editForm에는 값이 있는 Item객체가 넘어오므로 value속성에 실제 Item객체의 값이 있음

 


 

체크박스, 라디오 버튼, 셀렉트 박스 다루기

  • 판매 여부 : 체크박스
  • 판매지역 : 다중 체크박스 (여러 개 선택 가능)
  • 상품 종류 : 라디오 버튼 (하나만 선택 가능)
  • 배송방식 : 셀렉트 박스 (하나만 선택 가능)

- 체크박스, 라디오 버튼, 셀렉트 박스에서 사용할 데이터들은 Enum, Map, List 등으로 관리할 수 있음

  • Enum : 정적이고, 코드가 배포되어야 변경이 되는 경우
  • Map, List, Java Obect : DB에서 실시간/동적으로 데이터가 변경되는 경우

 

상품 종류 => ENUM => 라디오 버튼

public enum ItemType {

    BOOK("도서"), FOOD("음식"), ETC("기타");

    private final String description;

    ItemType(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}

배송방식 => Java Object => 셀렉트 박스

@Data
@AllArgsConstructor
public class DeliveryCode {

    private String code;	// FAST, NORMAL, SLOW
    private String displayName;	// 빠른배송, 일반배송, 느린배송

}

상품 객체 Item

@Data
public class Item {

    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    private Boolean open;           // 판매여부
    private List<String> regions;   // 판매지역
    private ItemType itemType;      // 상품종류
    private String deliveryCode;    // 배송방식

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

 

체크박스 1 - 단일 - 판매 여부

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>

=> 이렇게 작성하고 

  1. 체크박스를 선택 O : itemName=AAA&price=234&quantity=34&open=on
    => 체크박스를 선택하면 open이름으로 on이라는 데이터가 넘어가는데, 스프링은 on을 true로 변환해줌
  2. 체크박스를 선택 X : itemName=BBB&price=123&quantity=232
    => 체크박스를 선택하지 않으면, open이라는 필트 자체가 전송되지 않음

=> 문제점 : 체크박스를 선택하지 않은 것도 의미가 있는 것인데(판매중지) 아무런 값이 안 넘어가는 문 게가 있음, 이렇게 되면 체크를 해제하고 저장버튼을 눌러도 체크 해제라는 정보가 저장되지 못함

 

해결방안

- 위 문제 해결을 위해 스프링에서는 히든 필드를 활용함

- 히든 필드는 항상 전송됨

- 기존 필드명에 _만 앞에 붙여서 전송하면 처리해줌

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

=> 히든 필드에 name="_open"이라는 이름으로 작성

  1. 체크박스를 선택 O : itemName=AAA&price=123&quantity=123&open=true&_open=on
    => open=on&_open=on 두 개가 전송됨 => true로 인식
  2. 체크박스를 선택 X : itemName=BBB&price=123&quantity=123&_open=on
    => _open=on 만 전송됨 => false로 인식

 

그런데, 개발자가 매번 input에 히든 필드를 작성하는 것은 귀찮음... => Thmyeleaf기능으로 해결 가능

<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" th:field="*{open}" class="form-check-input">        
        <!--<input type="hidden" name="_open" value="on"> --><!-- 히든 필드 th:field="*{open}" or th:field="${item.open}" 가 만들어줌-->
        <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"><input type="hidden" name="_open" value="on"/>
        <!--<input type="hidden" name="_open" value="on"> --><!-- 히든 필드 th:field="*{open}" or th:field="${item.open}" 가 만들어줌-->
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

=> <form>의 th:object="${item}"에 접근하는 th:field="*{open}"을 작성

=> <input>에 id, name, value추가

=> <input type="hidden" name="_open" value="on"/> 히든 필드도 자동으로 추가해줌

 

 

 

체크박스 2 - 다중 - 판매 지역

- 체크박스에서 사용할 데이터를 전달

// 지역정보 넘겨주기
@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

- 위와 같이 @ModelAttribute를 추가해주면, 해당 클래스의 Controller들이 실행될 때마다 작성된 내용을 실행함 (Map생성 후, 지역정보를 담은 후에 model에 Map을 담음 → HTML로 넘어감)

- 공통으로 처리가 가능함

- 하지만, 매번 Controller호출마다 실행되기 때문에, 미리 만들어놓고 가져오는 형태로 리팩토링

 

=> RegionsInit 클래스 (싱글톤 클래스와 매우 유사함)

- static으로 미리 지역정보를 초기화 해두고, @ModelAttribute메서드에서는 Map을 받아오기

public class RegionsInit {

    private static final Map<String, String> regions = new HashMap<>();
    static {
        regions.put("SEOUL", "서울");
        regions.put("BUSAN", "부산");
        regions.put("JEJU", "제주");
    }

    private RegionsInit() {
    }

    public static Map<String, String> getRegions() {
        return regions;
    }
}
@ModelAttribute("regions")
public Map<String, String> regions() {
    Map<String, String> regions = RegionsInit.getRegions();
    return regions;
}

 

addForm

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

=> 결과

<!-- multi checkbox -->
<div>
    <div>등록 지역</div>
    <div class="form-check form-check-inline">
        <input type="checkbox" value="BUSAN" 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="JEJU" 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="SEOUL" 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:field="*{regions}"이 id, name속성을 추가해줌

=> 이때, HTML에서 id속성값은 유일해야 하므로 뒤에 번호를 붙여서 만들어줌(regions1, regions2, regions3)

=> th:each="region : ${regions}"에서 region은 Map이므로 value는 th:value="${region.key}"로 생성(SEOUL, BUSAN< JEJU)하고, <label>에는 th:text="${item.value}"로 생성함(서울, 부산, 제주)

=> <label>은 <input>의 id를 알아야 함 : th:for="${#ids.prev('regions')}"을 통해 <input>의 id를 알아올 수 있음

=> 체크박스 이기 때문에, th:field에 의해 히든 필드도 생성됨 <input type="hidden" name="_regions" value="on"/>

 

ex) 부산, 제주 선택 : itemName=AAA&price=123&quantity=123&open=true&_open=on&regions=BUSAN&_regions=on&regions=JEJU&_regions=on&_regions=on

'WEB > spring' 카테고리의 다른 글

[Spring] 메시지, 국제화  (0) 2022.01.23
[Spring] Thymeleaf 기본 기능 정리  (0) 2022.01.16
[Spring] Redirect vs Forward  (0) 2022.01.02
[Spring] WEB-INF 디렉토리  (0) 2022.01.02
[Spring] Spring Bean Scope, Provider, 프록시  (0) 2021.12.30