web Language/html

2. thymeleaf - 스프링 통합과 폼

wooweee 2023. 5. 6. 02:59
728x90

타임리프는 스프링 없이도 동작하지만, 스프링과 통합을 위한 다양한 기능을 편리하게 제공한다.

 

타임리프 메뉴얼

 

1. 스프링 통합 - 타임리프 추가기능

  1. 스프링 SpringEL 문법
    ${@myBean} 스프링 빈 호출 지원

  2. form 관리 추가 속성
    1. th:object - 기능 강화, form command 객체
    2. th:field, th:errors, th:errorclass
  3. form 컴포넌트 기능 - checkbox, radio button, list

  4. 메시지, 국제화 기능 통합 : 스프링 mvc2 - 1 메시지, 국제화

  5. 검증 오류 처리 통합 : 스프링 mvc2 - 2 검증 1 (Validation)

  6. 변환 서비스 통합 : 스프링 mvc2 - 8. 스프링 타입 컨버터

 

 

  • 설정방법
# build.gradle
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

 

  • 설정 변경 하고 싶을 때
# application.properties
https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application- properties.html#common-application-properties-templating
위 사이트에서 필요 부분 추가

 

 

2. 입력 폼 처리

 

  • form 관리 추가 속성
    • th:object
    • th:field, th:errors, th:errorclass

 

  • th:object  : 커멘드 객체를 지정한다. form에 object가 연결되는 것을 command 객체라고 한다.
  • *{...}          : 선택 변수 식, th:object 에서 선택한 객체에 접근
  • th:field     : html tag의 id, name, value 속성을 자동으로 처리

 

  • 예시
<input type="text" th:field="*{itemName}" />

<input type="text" id="itemName" name="itemName" th:value="*{itemName}">

 

  • html 기능 참고
    • 서버기준
    • map 형식 {key:value} == {key=value} == {name=value}
더보기

id: label 명과 동일할 시 서로 연결이 됨, id는 1개 여야한다.

name: form의 이름

value: UI가 처음 나올때 기본적으로 써져있는 값 - 이걸 지우면 placeHolder 나타남

placeHolder: 이름 작성칸에 아무 값도 없을 시 나타나는 글 

 

서버기준(중요)

map 형식 {key:value} == {name:value}

 

2.1. 등록 form

 

  • FormItemController
@GetMapping("/add")
public String addForm(Model model) {
    model.addAttribute("item", new Item());
    /* th:object 를 사용하기 위해 빈객체 생성을 해야하지만 생성 비용 크지 않고
    html form에서 id, value 작성시 컴파일 에러를 통해서 실수를 잡아준다. 
    th: object , th: field 사용 가능해진다.
    */
    return "form/addForm";
}

 

  • form/addForm.html
<form action="item.html" th:action th:object="${item}" method="post">
<!-- th:object="${item}"은 model로 부터 넘어온 객체 -->
        <div>
            <label for="itemName">상품명</label>
<!--/*        <input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">*/-->
<!--/*        <input type="text" id="itemName" th:field="${item.itemName}" class="form-control" placeholder="이름을 입력하세요">*/-->
<!--/*        <input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">*/-->
            <input type="text" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
        </div>
        
        <div>
            <label for="price">가격</label>
            <input type="text" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
        </div>
        
        <div>
            <label for="quantity">수량</label>
            <input type="text" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
        </div>

 

  • th:object="${item}" :  <form> 에서 사용할 객체를 지정. 선택 변수 식( *{...} )을 적용 가능

  • th:field
    • *{itemName}           : th:object가 있을 경우 사용 가능
    • ${item.itemName}  : th:object가 없는 경우 사용

    • 기능
      • id, name, value  속성을 모두 자동으로 만들어 준다. - value에는 실제 값이 들어간다.
      • value=""인 이유: 해당 Item이 비어있는 객체이기 때문 

 

  • 렌더링 후 소스코드
source code
<form action="" method="post">
        <div>
            <label for="itemName">상품명</label>
            <input type="text" class="form-control" placeholder="이름을 입력하세요" id="itemName" name="itemName" value="">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" class="form-control" placeholder="가격을 입력하세요" id="price" name="price" value="">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" class="form-control" placeholder="수량을 입력하세요" id="quantity" name="quantity" value="">
        </div>

 

 

3. 요구사항 추가

 

요구사항 추가

 

3.1. 추가 방법

 

원리

  • Item이란 class가 도메인이다.
    = item의 정보를 가지고 서비스를 제공하는 것
    • 해당 요구사항은 Item의 정보를 추가하는 것이고 해당 title들을 item class에 담아야한다.
    • 그래야지 나중에 controller || db 에서 item 정보들을 사용할 수가 있다.
  • Item에 담을 정보들이 단순 자료형이면 Item class에서 변수 지정으로 설정만 하면 되지만 위의 요구사항처럼 하위 정보들 중에서 선택을 하는 것이면 선택지에 관련된 class, Enum 을 생성하거나 controller에 만들어서 넣는 경우가 있다.
    • class,ENUM 생성하는 경우: 분리를 시켜서 향후 수정이나 다른 곳에서도 사용이 가능
    • controller에 생성하는 경우: 굳이 분류를 할 필요없는 고정된 값이라고 판단되는 경우
  • ItemRepository
    • 현재 project는 규모가 작아서 repository에 직접 service까지 구현을 한다.

 

3.2. 도메인 선택지 종류


좌 상단부터 시계방향: Item, Enum, String


  • Enum, class, String으로 한 이유는 여러가지 방법을 알기 위함. 큰 이유 없음
  • Boolean인 판매여부는 자료형 이므로 따로 추가할 필요 X

 

 

4. 4가지 add -edit 방식

  1. 판매여부 - 체크박스 단일: 한가지 선택지 밖에 없고 선택해도 되고 안해도 된다.
  2. 등록지역 - 체크박스 멀티: 여러 선택지 중 다중 선택
  3. 상품 종류 - 라디오 버튼: 여러 선택지 중 1가지만 선택, 초기에 선택하지 않는 이상 무조건 선택된다.
  4. 배송방식 - 셀렉트 박스: 여러 선택지 중 1가지만 선택 선택해도 되고 안해도 된다.

 

  • item 도메인
package hello.itemservice.domain.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;
    }
}

 

  • item = 주 도메인
  • item이란 domain은 우리가 필요로 하는 컨트롤러, 서비스, 리포지토리에게 중심이 되는 객체이자 data 저장 객체
  • 판매 여부, 등록지역, 상품 종류, 배송 방식 의 iv는 생성자로 초기화 하는 것이 아니라 처음 domain에서 점차 추가됨 고려해서 분리한다.

 

4.1. 판매여부 - 체크박스 단일(자료형)

 

<hr class="my-4">

<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" th:field="*{open}" class="form-check-input">
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

 

  • item의 private Boolean open 에 해당 checkBox의 값이 들어가는 경로

    1. form 전송시, {key:value} -> {name: value} 형식으로 server에서 값을 받게 된다.
      check box 경우
      • checked 될시 value = on
      • checked 안될 시 value 자체가 넘어가지 않는다.
    2. server에서는 @ModelAttribute가 form으로 부터 받은 값을 item 객체에 넣는다.
      check box 경우
      • on 같은 경우 string으로 인지하지 않고 true로 저장한다.
      • check하지 않은 경우 open이란 field 자체가 넘어오지 않아서 null 이 된다.
      • Boolean은 객체 타입이다. 그래서 null이 가능하다.

 

 

  • th:field=*{open}
    • input, label,value 생성
    • check 박스 경우 value가 on일 시, checked attribute까지 자동으로 생성

 

  • 체크박스와 스프링
    •  
    • open=null은 수정 페이지에서 문제
      • 체크 된 것을 체크 해제 할때 request params의 반환 값이 null이다.
      • 결국 open의 value값이 넘어가지 않아서 기존 open=true 값 변경이 불가하다.

 

정리

  1. 체크하면
    • 서버로는 open=on 이 넘어간다.
    • on->true로 되서 item.open=true 가 된다.
    • on->true로 되서 html value 값이 지정되고 th:field = *{open}가 value=on 일 경우, checked=checked 까지 자동 생성

  2. 체크안하면
    • 서버로는 open=null 이 넘어간다. 사실상 넘어가는게 없다는 뜻이다.

 

해결 방안(트릭 사용)

  • type="hidden" name="_open" value="on"
  • 스프링 MVC는 히든 필드만 넘어올시, check 해제되었다고 판단한다.

  • hidden: 항상 server로 값을 보낸다는 뜻

  • 체크박스 체크: open=on&_open=on 값이 서버로 넘어가고 item.open=true 된다. _open=on 무시
  • 체크박스 체크X: _open=on 값이 서버로 넘어가고 item.open=false 된다
  • 결론적으로 null과 같이 아무 값도 넘기지 않고 true / false로만 전달이 가능하다.

 

  • 판매여부 - 체크박스 단일 치트키
    • th:field="*{open}"
      1. 자동으로 id, value, name, hidden, checked(checkbox value가 on일 경우) 생성
      2. input의 hidden tag 도 자동으로 추가한다.

 

// 치트키 사용
    <input type="checkbox" th:field="*{open}" class="form-check-input">

// 원래 노가다 방식 - item.open=true인 경우
    <input type="checkbox" id="open" name="open" value="on" checked="checked" class="form-check-input">
    <input type="hidden" name="_open" value="on"/>
    
    <label for="open" class="form-check-label">판매 오픈</label>

 

 

4.2. 등록지역 - 체크박스 멀티(controller에서 생성)

 

  • 주의: UI에서 선택할 목록이 Map 인 것이고 실제 선택한 것들은 Item에서 List로 저장을 한다.
// Item.class
private List<String> regions; // 등록지역

 

// FormItemController.class

@ModelAttribute("regions") // model.modelAttribute("name", xxx)의 "name" 부분이다.
// @ModelAttribute 는 별도의 method에 담기면 해당 Controller(FormItemController.class) 호출할 때마다 "regions"란 key로 region이 model에 들어감
public Map<String, String> regions(){
    Map<String, String> regions = new LinkedHashMap<>();
    regions.put("SEOUL", "서울");
    regions.put("BUSAN", "부산");
    regions.put("JEJU", "제주");
    return regions;
}

 

@ModelAttribute 기능

  1. request로 들어온 Data를 Item에 담고 Model에 넣을 때 사용. - params로 들어가 있었다.
  2. Method에 적용할 때 해당 컨트롤러를 요청시 method 반환한 값이 자동으로 model에 담기게 된다.
  3. 그냥 view 로 갈 때 regions 객체도 항상 보내진다고 보면된다.

 

<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-inline">
        <label th:for="${#ids.prev('regions')}"
                th:text="${region.value}" class="form-check-label">지역</label>
    </div>
</div>

 

thymeleaf 기능

  • ${region.key}: Map의 key 추출
  • ${region.value}: Map의 value 추출
  • ${#ids.prev('region')}: loop 형식의 id (동적인 id)를 넣기 위해서 thymeleaf가 지원하는 기능 regions1, regions2, regions3 .... 동적으로 확장 가능
  • *{regions}
    • item.regions와 연관 짓기 위한 이름이지 @model로 온 regions가 아니다.
    • 위의 checkbox 또한 _regions라는 hidden tag를 생성한다.
    • 앞서 설명한 hidden과 동일한 방식으로 작동한다.
  • 서울, 부산 check : regions=SEOUL&_regions=on&regions=BUSAN&_regions=on&_regions=on
  • check X              :  _regions=on&_regions=on&_regions=on
 

 

4.3. 상품 종류 - 라디오 버튼(Enum)

 

  • 하나 선택하는 순간 무르기 없다
  • 처음부터 선택안하거나 하나 무조건 선택하거나 결정해야함
  • 하나만 선택 가능하다.
  • 하나는 무조건 선택이 되므로 hidden field를 사용할 필요가 없다.
  • item.itemType=null 이 나와도 괜찮다. 수정시 아예 선택 안하거나 무조건 하나 선택하거나 이후에는 무조건 다른 하나를 선택해야만 하기 때문

 

package hello.itemservice.domain.item;

public enum ItemType {
    Book("도서"), FOOD("음식"), ETC("기타");

    private final String description;

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

    public String getDescription() {
        return description;
    }
}

 

@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
    return ItemType.values(); 
    // enum은 values() 사용시 배열로 반환한다.
}
  • ItemType.values() : enum의 values는 배열을 반환 : [BOOK, FOOD, ETC]

 

<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"></label>
</div>

 

 

thymeleaf 기능

  • ${type.name()}: enum의 field 명 : BOOK, FOOD, ETC
  • ${type.description}: enum field의 description : 도서, 음식, 기타
 

 

 

4.4. 배송방식 - 셀렉트 박스(class-객체 생성)

 

@Data
@AllArgsConstructor
public class DeliveryCode {
    private String code;
    private String displayName;
}
@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;
}

 

<div>배송 방식</div>
<select  th:field="*{deliveryCode}" class="form-select">
    <option value="">==배송 방식 선택==</option>
    <option th:each="deliveryCode : ${deliveryCodes}"
            th:value="${deliveryCode.code}" // fast, normal, slow
            th:text="${deliveryCode.displayName}"> // 빠른 배송, 일반 배송, 느린 배송
    </option>
</select>

 

  • select
    • option: select 할 옵션들
    • html 렌더링 후에는 selected property가 선택된 option에 생성된다.
    • 서버에 전달 되는 {key:value} -> {name: 빠른배송 || 일반 배송 || 느린 배송}
    • 선택된 옵션에 관해서 th:field는 selected를 자동 주입해준다.

 

 

이전 발행글 : 1. 타임리프 - 기본기능