Thymeleaf#2 Spring๊ณผ HTML Form ์ฒ๋ฆฌ
by JiwonDevํ์๋ฆฌํ๋ ์คํ๋ง์ด ์์ด๋ ๋์ํ๋ค. ๋ค๋ง ์คํ๋ง๊ณผ ํจ๊ป ์ฌ์ฉํ๊ธฐ ์ ๋ง ํธํ๊ฒ ๋ง๋ค์ด์ ธ์๋ค.
- ๊ธฐ๋ณธ ๋ฉ๋ด์ผ: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
- ์คํ๋ง ํตํฉ ๋ฉ๋ด์ผ: https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

# ์คํ๋ง์์ ํ์๋ฆฌํ ์ฌ์ฉํ๊ธฐ
์๋๋ @Bean์ผ๋ก ํ์๋ฆฌํ ์์ง๊ณผ ํ์๋ฆฌํ ๋ทฐ๋ฆฌ์กธ๋ฒ๋ฅผ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋กํด์ผ ํ์ผ๋, ์คํ๋ง ๋ถํธ์์๋ ํ์ค๋ง ์ถ๊ฐํ๋ฉด ๋๋ค.
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

# HTML Form ๋ง๋ค๊ธฐ
- ํผ ํ๊ทธ์ th:object="${item}" ์ ์ด์ฉํด ์ ํ๋ณ์ *{name} ํด๋น ๊ฐ์ฒด์ ํ๋๋ฅผ ํธํ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
- ์ธํ ํ๊ทธ์ th:filed="..." ๋ฅผ ์ง์ ํด์ฃผ๋ฉด ํ๊ทธ ์์ฑ id, name, value๊ฐ ์๋ ์์ฑ๋๋ค.

- th:action, th:value๋ ๊ธฐ์กด ํ๊ทธ์ ๊ฐ์ผ๋ ํ์๋ฆฌํ ๋ฌธ๋ฒ์ ์ ์ฉํ๊ธฐ ํธํ๊ฒ ๋ ๋๋ง ํด์ค๋ค.
+ ๋ฐํ์์ ๋ฐ์ํ HTML ์ค๋ฅ๋ฅผ IDE ๋๋ ๋ ๋๋ง์์ ์ฐพ์ ์ ์๊ฒ ๋ง๋ค์ด์ฃผ๋ ํจ๊ณผ๋ ์๋ค.
@GetMapping("/add") public String addForm(Model model) { model.addAttribute("item", new Item()); return "form/addForm"; }
<form action="item.html" method="post" th:action th:object="${item}"> <div> <label for="itemName">์ํ๋ช
</label> <input class="form-control" placeholder="์ด๋ฆ์ ์
๋ ฅํ์ธ์" th:field="*{itemName}" type="text"> </div> <div> <label for="price">๊ฐ๊ฒฉ</label> <input class="form-control" placeholder="๊ฐ๊ฒฉ์ ์
๋ ฅํ์ธ์" th:field="*{price}" type="text"> </div> <div> <label for="quantity">์๋</label> <input class="form-control" placeholder="์๋์ ์
๋ ฅํ์ธ์" th:field="*{quantity}" type="text"> </div>

# ๋ณต์กํ Form ๋ง๋ค๊ธฐ
๋ณต์กํ Form์ ๋ง๋ค์ด๋ณด๋ฉฐ ํ์๋ฆฌํ๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ป๊ฒ HTML์ ์ฒ๋ฆฌํ๋์ง ์์๋ณด์.

์์ ์์ ์ฌ์ฉ๋๋ ๋๋ฉ์ธ ๊ฐ์ฒด์ Model ์ฝ๋ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
๋๋ฉ์ธ ๊ฐ์ฒด
@Data public class Item { private Long id; private String itemName; private Integer price; private Integer quantity; private Boolean open; //ํ๋งค ์ฌ๋ถ (true,false) private List<String> regions; //๋ฑ๋ก ์ง์ญ (List) private ItemType itemType; //์ํ ์ข
๋ฅ (Enum) private String deliveryCode; //๋ฐฐ์ก ๋ฐฉ์ (String) public Item() { } public Item(String itemName, Integer price, Integer quantity) { this.itemName = itemName; this.price = price; this.quantity = quantity; } }
@Data @AllArgsConstructor public class DeliveryCode { /* FAST: "๋น ๋ฅธ ๋ฐฐ์ก" NORMAL: "์ผ๋ฐ ๋ฐฐ์ก" SLOW: "๋๋ฆฐ ๋ฐฐ์ก" */ private String code; private String displayName; }
public enum ItemType { BOOK("๋์"), FOOD("์์"), ETC("๊ธฐํ"); private final String description; ItemType(String description) { this.description = description; } public String getDescription() { return description; } }
Model
@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("itemTypes") public ItemType[] itemTypes() { return ItemType.values(); } @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; }
@ ๋จ์ผ ์ฒดํฌ ๋ฐ์ค - ๊ธฐ์กด HTML
์๋์ ๊ฐ์ด ๋ง๋ค์์ ๋, open = on ์ ์ก๋๋ฉฐ ์ด๋ ์คํ๋ง ํ์ ์ปจ๋ฒํฐ์์ true, false๋ก ๋ฐ๊ฟ์ค๋ค.
- ํ์ง๋ง ์ค์ ๋์ํด๋ณด๋ฉด true๋ ์ ๋์๋ false๋ ๋์ค์ง ์๋๋ค. null์ด ๋ฐํ๋๋ค.

<!-- 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์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, ์๋์ ๊ฐ์ด ํ๋ ํ๋๋ฅผ ์ถ๊ฐํ๋ ๊ผผ์๋ฅผ ์ด์ฉํ๋ค. ๊ทธ๋ฌ๋ฉด open ๊ณผ _open์ ํจ๊ป ์ ์กํ๊ณ , ์๋ฒ์์ ์ฒดํฌํ์ฌ null์ด ๋ฐํ๋์ด ์ฒดํฌ๋ฐ์ค ๊ฐ์ด ์์ ๋์ง์๋ ์ค๋ฅ๋ฅผ ๋ง์ ์ ์๋ค.
<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>
@ ๋จ์ผ ์ฒดํฌ ๋ฐ์ค - ํ์๋ฆฌํ th:filed
๊ทธ๋ฅ th:filed ๋ฅผ ์ฌ์ฉํ๋ฉด id, name, value์ ํจ๊ป ์์์ ํ๋ ํ๋๊น์ง ๋ง๋ค์ด์ค๋ค. ๋๋ฌด๋ ํธํ๋ค.
- ๋ํ th:filed๋ open ๊ฐ์ true, false์ ๋ฐ๋ผ checked="checked" ์์ฑ์ ์์์ ์ถ๊ฐ/์ญ์ ํด์ค๋ค.
<!-- single checkbox --> <div>ํ๋งค ์ฌ๋ถ</div> <div> <div class="form-check"> <input class="form-check-input" id="open" th:field="*{open}" type="checkbox"> <label class="form-check-label" for="open">ํ๋งค ์คํ</label> </div> </div>

@ ๋ฉํฐ ์ฒดํฌ ๋ฐ์ค - ํ์๋ฆฌํ th:each
@ModelAttribute("myregions") // ํด๋น ํด๋์ค์ ๋ชจ๋ Controller์ myregions ๋ชจ๋ธ ์ถ๊ฐ public Map<String, String> regions() { Map<String, String> regions = new LinkedHashMap<>(); regions.put("SEOUL", "์์ธ"); regions.put("BUSAN", "๋ถ์ฐ"); regions.put("JEJU", "์ ์ฃผ"); return regions; }
- th:each๋ฅผ ์ด์ฉํ์ฌ ๊ฐ๊ฐ์ <input>๊ณผ <label> ์ ๋ฐ๋ณตํด์ ๋ง๋ ๋ค.
- ๋ฐ๋ณต๋ฌธ์์ ์ฌ์ฉ๋๋ id๋ th:filed์์ [ region1, region2, region3... ]์ผ๋ก ์๋์ผ๋ก ์์ฑํ๋ค.
๋ค๋ง ์ด ๊ฒฝ์ฐ <label>์์ <input>๊ณผ ๋์ผํ id๋ฅผ ์ ์ด์ฃผ๊ธฐ๊ฐ ์ด๋ ต๋ค. - ๊ทธ๋์ ํ์๋ฆฌํ ์ ํธ์์ ${#ids} ๋ผ๋ ๊ฒ์ ์ ๊ณตํ๋ค.
ids.prev{'regions'}๋ ์ด์ ์ ์๋ ์์ฑ๋ *{regions} id ๊ฐ์ ์ฌ์ฉํ๋ผ๋ ์๋ฏธ์ด๋ค.
<!-- multi checkbox --> <form action="item.html" method="post" th:action th:object="${item}"> <div>๋ฑ๋ก ์ง์ญ</div> <div class="form-check form-check-inline" th:each="rg : ${myregions}"> <input class="form-check-input" th:field="*{regions}" th:value="${rg.key}" type="checkbox"> <!-- th:filed="${item.regions}" --> <label class="form-check-label" th:for="${#ids.prev('regions')}" th:text="${rg.value}">์์ธ</label> </div> </div> </form>


์ด HTML Form ์๋์ ๊ฐ์ ํ๋ผ๋ฉํ๋ฅผ ๋ง๋ ๋ค. ์ด๋ ์คํ๋ง ํ์ ์ปจ๋ฒํฐ์์ ์ ์ ํ๊ฒ ๋ฐ๊ฟ์ค๋ค.
- ์์์ ์ค๋ช ํ ๊ฒ ์ฒ๋ผ, null์ ๋ฐฉ์งํ๊ธฐ ์ํด _regions๋ผ๋ ํ๋ ํ๋๋ฅผ ์์์ ์ถ๊ฐํด์ค๋ค.

# ๋ผ๋์ค ๋ฒํผ - ํ์๋ฆฌํ
๋ผ๋์ค ๋ฒํผ๋ ๊ฐ์ฒด๋ฅผ ์ด์ฉํด๋ ์๊ด์์ง๋ง, Enum์ ์ด์ฉํด์ ๋ง๋ค์ด๋ณด์.
- Enum.name()์ ํด๋น ์ด๋ ๊ฐ์ฒด์ toString() ์ ๋ฐํํ๋ค. (BOOK, FOOD, ETC)
- ์ฒดํฌ๋ฐ์ค์ ๊ฒฝ์ฐ null์ด ๋ฐํ๋์ด๋ ์๊ด์์ด์ ๋ฐ๋ก ํ๋ ํ๋๋ฅผ ๋ง๋ค์ง๋ ์๋๋ค.
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(); }
<!-- radio button --> <form action="item.html" method="post" th:action th:object="${item}"> <div> <div>์ํ ์ข
๋ฅ</div> <div class="form-check form-check-inline" th:each="type : ${itemTypes}"> <input class="form-check-input" th:field="*{itemType}" th:value="${type.name()}" type="radio"> <!-- th:filed = "${item.itemType}" --> <label class="form-check-label" th:for="${#ids.prev('itemType')}" th:text="${type.description}"> </label> </div> </div> </form>
์ฐธ๊ณ ๋ก ์คํ๋ง-ํ์๋ฆฌํ ์ฐ๋๊ธฐ๋ฅ์ผ๋ก Enum์ Model์ ๊ฑฐ์น์ง ์๊ณ ์คํ๋ง ๋๋ฉ์ธ ๊ฐ์ฒด์ ์ง์ ์ ๊ทผํด์ ์ฌ์ฉํ ์ ์๋ค. ๋ค๋ง ํจํค์ง ๊ฒฝ๋ก๋ฅผ ๋ค ์ ๋ ฅํด์ค์ผํด์ ๊ถ์ฅํ๋ ๋ฐฉ๋ฒ์ ์๋๋ค.
- ${ T(ํจํค์ง ๊ฒฝ๋ก).values() } ๋ก ENUM์ ๋ชจ๋ ์ ๋ณด๋ฅผ ๋ฐ์์ฌ ์ ์๋ค. ์ด๋ฅผ ์คํ๋งEL ๋ฌธ๋ฒ์ด๋ผ๊ณ ํ๋ค.
- ${ @mybean.doSomthing() } ์ผ๋ก ์ปจํ ์ด๋์ ๋ฑ๋ก๋ ์คํ๋ง ๋น์ ์ง์ ์ฌ์ฉํ ์๋ ์๋ค.
<!-- model์ ๋ด์์ ์ ์ฒด Enum์ ์ ๋ฌํ๋ ๋ฐฉ๋ฒ --> <div th:each="type : ${itemTypes}"> <!-- ์คํ๋ง ๊ฐ์ฒด๋ฅผ ์ฝ์ด ์ ์ฒด Enum์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ --> <div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
# Select ๋ฐ์ค - ํ์๋ฆฌํ
์์์ ํ๋ ๊ฒ๊ณผ ํน๋ณํ๊ฒ ๋ค๋ฅธ๊ฑด ์๋ค. ๊ทธ๋ฅ ์ฌ์ฉํ๋ฉด ๋๋ค.
@Data @AllArgsConstructor public class DeliveryCode { /* FAST: "๋น ๋ฅธ ๋ฐฐ์ก" NORMAL: "์ผ๋ฐ ๋ฐฐ์ก" SLOW: "๋๋ฆฐ ๋ฐฐ์ก" */ 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; }
<!-- SELECT --> <div> <div>๋ฐฐ์ก ๋ฐฉ์</div> <select class="form-select" th:field="*{deliveryCode}"> <option value="">==๋ฐฐ์ก ๋ฐฉ์ ์ ํ==</option> <option th:each="deliveryCode : ${deliveryCodes}" th:text="${deliveryCode.displayName}" th:value="${deliveryCode.code}"> </option> </select> </div>

# ์คํ๋ง์์ ์ค๋ฅ, ์์ธ์ฒ๋ฆฌ(BindingResult)
https://jiwondev.tistory.com/169#head5
Spring Vaildation#1 ๊ฒ์ฆ
๊ฐ์ฒด๋ฅผ ๋งคํํ๋ ์ปจํธ๋กค๋ฌ์ ์ค์ํ ์ญํ ์ค ํ๋๋ HTTP ์์ฒญ์ด ์ ์์ธ์ง ๊ฒ์ฆํ๋ ๊ฒ์ด๋ค. ์ด์ฉ๋ฉด ์ ์ ๋ก์ง๋ณด๋ค ์ด๋ฌํ ์์ธ, ๊ฒ์ฆ ๋ก์ง์ ์ค๊ณํ๋ ๊ฒ์ด ํจ์ฌ ๋ ์ด๋ ค์ธ ์ ์๋ค. ํด๋น๊ธ์ Bea
jiwondev.tistory.com
์คํ๋ง์ bindingErrors๋ฅผ ์ฌ์ฉํ๋ฉด ํ์๋ฆฌํ์์๋ ๊ฐ๊ฒฐํ ๋ฌธ๋ฒ์ ์ด์ฉํ ์ ์๋ค.

<form action="item.html" method="post" th:action th:object="${item}"> <div th:if="${#fields.hasGlobalErrors()}"> <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">๊ธ๋ก๋ฒ ์ค๋ฅ ๋ฉ์์ง</p> </div> <div> <label for="itemName" th:text="#{label.item.itemName}">์ํ๋ช
</label> <input class="form-control" id="itemName" placeholder="์ด๋ฆ์ ์
๋ ฅํ์ธ์" th:errorclass="field-error" th:field="*{itemName}" type="text"> <div class="field-error" th:errors="*{itemName}"> ์ํ๋ช
์ค๋ฅ </div> </div> ... </form>
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev