프로그래밍 공방

[Spring] 7. 스프링 MVC - Thymeleaf, @ModelAttribute, PRG 본문

개발/스프링

[Spring] 7. 스프링 MVC - Thymeleaf, @ModelAttribute, PRG

hyosupsong 2021. 11. 4. 01:40

Thymeleaf

타임리프 사용 선언

<html xmlns:th="http://www.thymeleaf.org">

속성 변경

<link href="../css/bootstrap.min.css"
   th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

타임리프를 사용하여 기존 속성의 값을 변경할 때는 위와 같이 th:를 붙인 해당 속성을 하나 더 생성해주면 된다
* HTML을 그대로 볼 때는 기존 속성이 사용되고(th: 는 HTML이 모르는 문법이므로 무시된다), 뷰 템플릿을 거치면 th: 의 값이 기존 속성을 대체하면서 동적으로 변경할 수 있게 된다
* 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿 (natural templates)이라 한다
* 뷰 템플릿 영역 : /resources/templates 

변수 표현식 - ${ ... }

<td th:text="${item.price}">10000</td>

Model에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다(프로퍼티 접근법을 사용 / item.getPrice())

URL 링크 표현식 - @{ ... }

<td>
    <a href="item.html"
       th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
       th:text="${item.id}">회원id</a>
</td>
<td>
    <a href="item.html"
       th:href="@{|/basic/items/${item.id}|}"
       th:text="${item.itemName}">상품명</a>
</td>

타임리프는 URL 링크를 사용하는 경우 @{ ... } 를 사용한다
* URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.

th:href="@{/basic/items/{itemId}(itemId=${item.id})}"

- URL 링크 표현식을 사용하면 경로를 템플릿처럼 편리하게 사용할 수 있다.
* 경로 변수( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성한다.
예) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
    생성 링크: http://localhost:8080/basic/items/1?query=test

th:href="@{|/basic/items/${item.id}|}"

- 아래에서 설명할 리터럴 대체 문법을 활용해서 위와 같이 간단히 사용할 수도 있다

리터럴 대체 - | ... |

타임리프에서는 아래와 같이 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.

<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"

 

리터럴 대체 문법을 사용하면 편리하게 사용할 수 있다

<span th:text="|Welcome to our application, ${user.name}!|">
th:onclick="|location.href='@{/basic/items/add}'|"

반복 출력 - th:each

<tr th:each="item : ${items}">
    <td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
    <td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>
    <td th:text="${item.price}">10000</td>
    <td th:text="${item.quantity}">10</td>
</tr>

th:each를 사용해서 반복을 표현할 수 있다

속성 변경 - th:action

HTML form에서 action에 값이 없으면 현재 URL에 데이터를 전송

상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을 구분하면 하나의 URL로 등록 폼과, 등록 처리를 깔끔하게 처리할 수 있다
- 상품 등록 폼: GET /basic/items/add
- 상품 등록 처리: POST /basic/items/add

@ModelAttribute

단계별 변화를 통해 @ModelAttribute에 대해 알아보자

V1

@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
                        @RequestParam int price,
                        @RequestParam Integer quantity,
                        Model model) {
    /* ... */
    return "basic/item";
}

- @RequestParam을 통해 요청 파라미터 데이터를 변수로 받고 함수 내에서 필요한 객체를 생성하고 로직을 수행한다

V2

@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
    /* ... */
 return "basic/item";
}

- @ModelAttribute는 Item 객체를 생성하고 요청 파라미터 값을 프로퍼티 입력값을 사용해서 입력해준다

@ModelAttribute - Model 추가

- @ModelAttribute는 모델(Model)에 @ModelAttribute로 지정한 객체를 자동으로 넣어준다
- Model에 담는 데이터의 이름은 @ModelAttribute에 지정한 name 속성을 사용한다
   - @ModelAttribute("hello") Item item 이름을 hello 로 지정
   - model.addAttribute("hello", item); 모델에 hello 이름으로 저장

V3

@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
    /* ... */
    return "basic/item";
}

- @ModelAttribute의 이름을 생략한다면 Model에 저장할 때 클래스 명의 첫글자만 소문자로 변경해서 등록한다
   - @ModelAttribute Item item -> item 으로 등록

V4

@PostMapping("/add")
public String addItemV4(Item item) {
    /* ... */
    return "basic/item";
}

- @ModelAttribute 자체도 생략가능하다 (모델에도 동일하게 자동 등록된다)

Redirect / 리다이렉트

스프링은 redirect:/... 으로 리다이렉트를 지원한다.

@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
    /* ... */
    return "redirect:/basic/items/{itemId}";
}

* 컨트롤러에 매핑된 @PathVariable 의 값은 redirect 에도 사용 할 수 있다

PRG Post/Redirect/Get

Post 관련 처리를 할 때 주의해야 하는 문제가 있다

POST 등록 후 새로고침

* 웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다
상품 등록 폼에서 데이터를 입력하고 저장을 선택하면 POST /add + 상품 데이터를 서버로 전송한다
이 상태에서 새로 고침을 선택하면 마지막에 전송한 POST /add + 상품 데이터를 서버로 다시 전송하게
된다. 그래서 내용은 같고, ID만 다른 상품 데이터가 계속 쌓이게 된다

POST, Redirect GET

새로 고침 문제를 해결하려면 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라,
상품 상세 화면으로 리다이렉트를 호출해주면 된다
웹 브라우저는 리다이렉트의 영향으로 상품 저장 후에 실제 상품 상세 화면으로 다시 이동한다
따라서 마지막에 호출한 내용이 상품 상세 화면인 GET /items/{id} 가 되는 것이다
이후 새로고침을 해도 상품 상세 화면으로 이동하게 되므로 새로 고침 문제를 해결할 수 있다
* 이런 문제 해결 방식을 PRG Post/Redirect/Get 라 한다

RedirectAttributes

@PostMapping("/add")
public String addItem(Item item, RedirectAttributes redirectAttributes) {
    /* ... */
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/basic/items/{itemId}";
}

RedirectAttributes 를 사용하면 URL 인코딩도 해주고, pathVarible, 쿼리 파라미터까지 처리해준다
- redirect:/basic/items/{itemId}
   - pathVariable 바인딩: {itemId}
   - 나머지는 쿼리 파라미터로 처리: ?status=true

이런 RedirectAttributes는 아래와 같이 사용할 수 있다

<div class="container">
    <h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
</div>

- th:if : 해당 조건이 참이면 실행
- ${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능
   - 타임리프에서 지원하는 쿼리 파라미터 사용 방식