웹 페이지 만들기
요구사항 분석
타임리프(Thymleaf)
- 타임리프 동작 방식은 기존 코드를 th 태그가 붙은 코드로 대체하는 방식으로 동적으로 동작한다.
<!-- 타임리프 예제 -->
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>상품 목록</h2>
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품 등록
</button>
</div>
</div>
<hr class="my-4">
<div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>상품명</th>
<th>가격</th>
<th>수량</th>
</tr>
</thead>
<tbody>
<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/{itemId}(itemId=${item.id})}"
th:text="${item.itemName}">상품명</a></td>
<td th:text="${item.price}">10000</td>
<td th:text="${item.quantity}">10</td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
- 타임리프 사용 선언 : <html xmlns:th="http://www.thymeleaf.org">
- th 태그 사용 시
- 타임리프 뷰 템플릿을 거치게 되면 원래 값인 href="value1"을 th:href="value2"의 값으로 변경한다.
- HTML을 그대로 볼 때는 href 속성이 사용되고 뷰 템플릿을 거치면 href의 값이 th:href의 값으로 대체되면서 동적으로 변경된다.
- 대부분의 HTML 속성을 th:xxx로 변경할 수 있다.
- 타임리프 핵심
- th:xxx가 붙은 부분은 서버 사이드에서 렌더링되고 기존 것(xxx)를 대체한다.
- th:xxx가 없으면 기존 HTML의 xxx가 그대로 사용된다.
- HTML 파일을 직접 여는 경우 th:xxx 속성이 있어도 웹 브라우저는 th:xxx 속성을 알지 못하므로 무시한다.
- th:xxx가 붙은 부분은 서버 사이드에서 렌더링되고 기존 것(xxx)를 대체한다.
- URL 링크 표현식 : 타임리프에서 URL 링크를 사용하는 경우 @{...}의 형태로 사용한다.
- ex) th:href="@{/css/bootstrap~}"
- URL 링크 표현식을 사용하는 경우 서블릿 컨텍스트를 자동으로 포함한다.
- ex) th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
- PathVariable 스타일처럼 사용할 수 있다.
- 이 경우 item.id 값이 있으면 {}안의 itemId 값으로 치환된다.
- 경로 변수(PathVariable)뿐만 아니라 쿼리 파라미터도 생성할 수 있다.
- ex) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
=> http://localhost:8080/basic/items/1?query=test
- ex) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
- 간단한 URL 링크의 경우 th:href="@{|/basic/items/${item.id}|}" 이렇게 표현할 수 있다.
- 리터럴 대체 문법 - |...|
- 타임리프에서는 문자와 표현식은 분리되어 있기에 더해서 사용해야한다.
ex) <span th:text="'Welcome to our application, ' + ${user.name} + '!'"> - 리터럴 대체 문법 사용 시 더하기 없이 편리하게 사용할 수 있다.
ex) <span th:text="|Welcome to our application, ${user.name}!|">
- 타임리프에서는 문자와 표현식은 분리되어 있기에 더해서 사용해야한다.
- 반복 출력 - th:each
- ex) <tr th:each="item : ${items}">
- Model에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고 반복문 안에서 item 변수를 사용할 수 있다.
- ex) <tr th:each="item : ${items}">
- 변수 표현식 - ${...}
- ex) <td th:text="${item.price}">10000</td>
- Model에 포함된 값이나 타임리프 변수로 선언한 값을 조회할 수 있다.
- 프로퍼티 접근법을 사용한다. ex) item.getPrice()
- ex) <td th:text="${item.price}">10000</td>
- 내용 변경 - th:text
- ex) <td th:text="${item.price}">10000</td>
- 기존 값(10000)을 th:text의 값으로 변경한다.
- ex) <td th:text="${item.price}">10000</td>
- HTML form에서 action에 값이 없으면 현재 URL에 데이터를 전송한다.
- ex) 등록 폼(GET)과 등록 처리(POST) URL을 동일하게 맞추고 HTTP 메소드로 구분하는 경우
- 타임리프 장점
- JSP의 경우 서버를 통해서만 열어야 했는데 순수 HTML 파일을 웹 브라우저에서 열어서 내용 확인 가능
- 순수 HTML을 그대로 유지하면서 뷰 템플릿을 사용할 수 있는 네츄럴 템플릿이다.
@ModelAttribute
//@ModelAttribute 사용하지 않는 경우
@PostMapping("/add")
public String addItemV1(@RequestParam("itemName") String itemName,
@RequestParam("price") int price,
@RequestParam("quantity") int quantity,
Model model) {
Item item = new Item();
item.setItemName(itemName);
item.setPrice(price);
item.setQuantity(quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
- @ModelAttribute를 사용하지 않으면 @RequestParam으로 요청 파라미터 데이터 하나하나를 해당 변수에 받아서 객체를 만들어줘야 한다.
- 뷰로 데이터를 넘겨줘야 하는 경우 Model 객체에 데이터를 담아서 넘겨야 한다.
//@ModelAttribute 사용
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item) {
itemRepository.save(item);
// model.addAttribute("item", item); //자동 추가, 생략 가능
return "basic/item";
}
- @ModelAttribute를 사용하는 경우 객체를 만들고 변수를 하나하나 받을 필요 없다.
- 뷰로 데이터를 넘겨줘야 하는 경우에도 @ModelAttribute에 name 속성을 주면 해당 값으로 Model에 값이 담겨서 뷰로 넘어간다.
- ex) @ModelAttribute("hello") Item item = model.addAttribute("hello", item)
- @ModelAttribute name 속성의 기본 값은 클래스 명에서 첫 글자만 소문자로 바꾼 것이다.
- ex) @ModelAttribute HelloData helloData = @ModelAttribute("helloData") HelloData helloData
- 단순 타입이 아닌 경우 @ModelAttribute 자체를 생략할 수 있다.
- @ModelAttribute 역할
- 요청 파라미터 처리 - 객체 생성, 요청 파라미터 값을 프로퍼티 접근법으로 입력
- Model 추가 - Model에 @ModelAttribute로 지정한 객체를 자동으로 넣어준다.
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable("itemId") Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
- 리다이렉트 : 뷰 템플릿을 호출하는 대신 URL로 해당 컨트롤러를 호출한다.
- 리다이렉트하는 방법
- ex) redirect:/...
- 기존에 컨트롤러에 매핑된 PathVariable 값이 있으면 redirect에서도 {}로 PathVariable 방식을 사용할 수 있다.
- HTML Form 전송은 PUT, PATCH 방식을 지원하지 않고 GET, POST만 사용할 수 있다.
- PUT, PATCH는 HTTP API(Rest API) 전송 시에 사용
- 히든 필드로 HTML Form 전송 시 PUT, PATCH 매핑을 사용하는 방법이 존재한다.
- HTTP 요청상 POST 요청이지만 스프링에서 PUT, PATCH로 처리해주는 방식
PRG 패턴 Post / Redirect / Get
- 기존 예제의 문제점
- GET 방식으로 상품 등록 폼을 보여준다.
- 상품 등록 폼에서 POST 방식으로 상품을 등록/저장한다.
- 이때 새로고침을 하면 마지막에 서버에 전송한 데이터(POST 방식의 상품 저장)를 다시 전송한다.
- 새로 고침 = 마지막에 했던 행위를 다시하는 것
- 예제의 경우 상품을 저장하고 새로고침을 계속 시도하면 상품 데이터를 서버로 계속 재전송하게 되고 상품 데이터가 계속 쌓이게 된다.
- 기존 예제의 문제점 (새로 고침 문제) 해결 방법
- 상품 저장 후 뷰 템플릿으로 이동하는 것이 아닌 리다이렉트를 호출한다. (PRG 방식 사용)
- ex) 상품 저장 후 리다이렉트로 상품 상세 화면으로 이동하면 새로 고침을 하더라도 상품 저장을 하는 것이 아닌 상세 화면을 보여주는 행위를 하게 된다.
- 상품 저장 후 뷰 템플릿으로 이동하는 것이 아닌 리다이렉트를 호출한다. (PRG 방식 사용)
//새로 고침 시 문제 발생
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
//PRG 방식 사용
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
- 예제의 V4 버전은 새로 고침 시 상품 데이터가 계속 생겨나는 문제가 발생한다.
- V5 버전에서는 PRG 방식을 사용해서 상품 상세 화면으로 리다이렉트를 함으로 문제를 해결한다.
"redirect:/basic/items/" + item.getId();
- 예제와 같이 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기에 위험하다.
- RedirectAttributes를 사용해야 한다.
RedirectAttributes
- ex) 상품 저장 후 리다이렉트 시 상품 저장이 잘되었는지 고객 입장에서는 확신이 들지 않음.
=> 저장이 잘되었다면 리다이렉트 된 화면에 저장 완료 라는 메시지를 보여줘야 한다면?
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
- RedirectAttributes 사용 시
- URL 인코딩을 해준다.
- PathVariable과 쿼리 파라미터를 처리해준다.
- ex) redirect:/basic/items/{itemId}
- {itemId}는 addAttribute("itemId")와 일치하므로 pathVariable 바인딩
- pathVariable로 바인딩 되지 않은 나머지는 쿼리 파라미터로 처리 => ?status=true
<!-- 상품 상세 화면 추가 내용 -->
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
- 상품 상세 화면에 예제와 같이 추가하는 경우
- th:if => 해당 조건이 참이면 실행
- ${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능
- 원래는 컨트롤러에서 Model에 직접 값을 담고 꺼내야하지만 쿼리 파라미터는 자주 사용되기 때문에 타임리프에서 지원해준다.
출처 : [인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의 | 김영한 - 인프런
김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습
www.inflearn.com
'Spring > [인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술]' 카테고리의 다른 글
[인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 스프링 MVC - 기본 기능 (4) | 2024.10.07 |
---|---|
[인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 스프링 MVC - 구조 이해 (2) | 2024.10.04 |
[인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] MVC 프레임워크 만들기 (2) | 2024.10.03 |
[인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 서블릿, JSP, MVC 패턴 (3) | 2024.10.02 |
[인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 서블릿 (3) | 2024.09.30 |