일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 아이
- XACT_STATE
- XWPF
- transferTo
- 요청매핑
- TRANCOUNT
- 프론트컨트롤러
- 배치
- 튜닝
- SQLSTATE=42705
- MSSQL
- renameTo
- HWPF
- 홈스쿨링
- 재귀멤버
- SQL
- 앵커멤버
- 요약
- 진경혜
- 스프링
- 함수
- getChannel()
- dm_exec_requests
- ERROR_MESSAGE
- java
- 교육법
- 디스패처서블릿
- git
- spring
- 자바
- Today
- Total
필기노트
타임리프 간단히 알아보기 본문
상품 목록
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "basic/items";
}
/**
* 테스트용 데이터 추가
*/
@PostConstruct
public void init() {
itemRepository.save(new Item("testA", 10000, 10));
itemRepository.save(new Item("testB", 20000, 20));
}
}
컨트롤러 로직은 itemRepository에서 모든 상품을 조회한 다음에 모델에 담는다. 그리고 뷰 템플릿을 호출한다.
@RequiredArgsConstructor 는 final 이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.
@PostConstruct : 해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출된다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css"
th: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/${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
th:href="@{/css/bootstrap.min.css}"
href="value1" 을 th:href="value2" 의 값으로 변경한다.
타임리프 뷰 템플릿을 거치게 되면 원래 값을 th:xxx 값으로 변경한다. 만약 값이 없다면 새로 생성한다.
HTML을 그대로 볼 때는 href 속성이 사용되고, 뷰 템플릿을 거치면 th:href 의 값이 href 로 대체되면서 동적으로 변경할 수 있다.
대부분의 HTML 속성을 th:xxx 로 변경할 수 있다.
타임리프 핵심
핵심은 th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체한다.
th:xxx 이 없으면 기존 html의 xxx 속성이 그대로 사용된다.
HTML을 파일로 직접 열었을 때, th:xxx 가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시한다.
따라서 HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.
URL 링크 표현식 - @{...},
th:href="@{/css/bootstrap.min.css}"
@{...} : 타임리프는 URL 링크를 사용하는 경우 @{...} 를 사용한다. 이것을 URL 링크 표현식이라 한다.
URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.
상품 등록 폼으로 이동
속성 변경 - th:onclick
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
여기에는 다음에 설명하는 리터럴 대체 문법이 사용되었다. 자세히 알아보자.
리터럴 대체 - |...|
|...| :이렇게 사용한다.
타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
다음과 같이 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.
<span th:text="|Welcome to our application, ${user.name}!|">
결과를 다음과 같이 만들어야 하는데
location.href='/basic/items/add'
그냥 사용하면 문자와 표현식을 각각 따로 더해서 사용해야 하므로 다음과 같이 복잡해진다.
th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
리터럴 대체 문법을 사용하면 다음과 같이 편리하게 사용할 수 있다.
th:onclick="|location.href='@{/basic/items/add}'|"
반복 출력 - th:each
<tr th:each="item : ${items}">
반복은 th:each 를 사용한다. 이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고, 반복문 안에서 item 변수를 사용할 수 있다.
컬렉션의 수 만큼 <tr>..</tr> 이 하위 테그를 포함해서 생성된다.
변수 표현식 - ${...}
<td th:text="${item.price}">10000</td>
모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.
프로퍼티 접근법을 사용한다. ( item.getPrice() )
내용 변경 - th:text
<td th:text="${item.price}">10000</td>
내용의 값을 th:text 의 값으로 변경한다.
여기서는 10000을 ${item.price} 의 값으로 변경한다.
1) 쿼리 파라미터
@{/hello(param1=${param1}, param2=${param2})}
/hello?param1=data1¶m2=data2
() 에 있는 부분은 쿼리 파라미터로 처리된다.
2) 경로 변수
@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}
/hello/data1/data2
URL 경로상에 변수가 있으면 () 부분은 경로 변수로 처리된다.
3) 경로 변수 + 쿼리 파라미터
@{/hello/{param1}(param1=${param1}, param2=${param2})}
/hello/data1?param2=data2
경로 변수와 쿼리 파라미터를 함께 사용할 수 있다.
4) URL 링크 간단히
th:href="@{|/basic/items/${item.id}|}"
상품 이름을 선택하는 링크를 확인해보자.
리터럴 대체 문법을 활용해서 간단히 사용할 수도 있다.
참고
타임리프는 순수 HTML 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다. JSP를 생각해보면, JSP 파일은 웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 뒤죽박죽 되어서 정상적인 확인이 불가능하다. 오직 서버를 통해서 JSP를 열어야 한다.
이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿 (natural templates)이라 한다.
상품 등록
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 등록 폼</h2>
</div>
<h4 class="mb-3">상품 입력</h4>
<form action="item.html" th:action method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control" placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" placeholder="수량을 입력하세요">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">상품등록</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" onclick="location.href='items.html'" th:onclick="|location.href='@{/basic/items}'|" type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
속성 변경 - th:action
th:action
HTML form에서 action 에 값이 없으면 현재 URL에 데이터를 전송한다.
상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을 구분한다.
상품 등록 폼: GET /basic/items/add
상품 등록 처리: POST /basic/items/add
이렇게 하면 하나의 URL로 등록 폼과, 등록 처리를 깔끔하게 처리할 수 있다.
취소
취소시 상품 목록으로 이동한다.
th:onclick="|location.href='@{/basic/items}'|"
상품 등록 처리 - @ModelAttribute
POST - HTML Form
content-type: application/x-www-form-urlencoded
메시지 바디에 쿼리 파리미터 형식으로 전달 itemName=itemA&price=10000&quantity=10
예) 회원 가입, 상품 주문, HTML Form 사용
요청 파라미터 형식을 처리해야 하므로 @RequestParam 을 사용하자
@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer 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";
}
@RequestParam 으로 변수를 하나하나 받아서 Item 을 생성하는 과정은 불편했다.
이번에는 @ModelAttribute 를 사용해서 한번에 처리해보자.
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
itemRepository.save(item);
//model.addAttribute("item", item); //자동 추가, 생략 가능
return "basic/item";
}
@ModelAttribute - 요청 파라미터 처리
@ModelAttribute 는 Item 객체를 생성하고, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해다.
@ModelAttribute - Model 추가
@ModelAttribute 는 중요한 한가지 기능이 더 있는데, 바로 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다. 지금 코드를 보면 model.addAttribute("item", item) 가 주석처리 되어 있어도 잘 동작하는 것을 확인할 수 있다.
모델에 데이터를 담을 때는 이름이 필요하다. 이름은 @ModelAttribute 에 지정한 name(value) 속성을 사용한다. 만약 다음과 같이 @ModelAttribute 의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.
@ModelAttribute("hello") Item item -> 이름을 hello 로 지정
model.addAttribute("hello", item); -> 모델에 hello 이름으로 저장
/**
* @ModelAttribute 자체 생략 가능
* model.addAttribute(item) 자동 추가
*/
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
@ModelAttribute 자체도 생략가능하다. 대상 객체는 모델에 자동 등록된다.
상품수정
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
상품 수정은 상품 등록과 전체 프로세스가 유사하다.
GET /items/{itemId}/edit:상품수정폼
POST /items/{itemId}/edit : 상품 수정 처리
리다이렉트
상품 수정은 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트를 호출한다.
스프링은 redirect:/... 으로 편리하게 리다이렉트를 지원한다.
redirect:/basic/items/{itemId}
컨트롤러에 매핑된 @PathVariable 의 값은 redirect 에도 사용 할 수 있다.
redirect:/basic/items/{itemId} -> {itemId} 는 @PathVariable Long itemId 의 값을 그대로 사용한다.
RedirectAttributes
redirect의 경우, 지정된 주소로 새로운 HTTP GET 요청이 시작되기 때문에 실행 이전에 수행된 모델 데이터가 소멸한다.
따라서 redirect를 할 때는 Request 객체나 Model 객체에 데이터를 담아도 전달되지 않는다.
그러나 redirect 방법으로도 데이터를 전달하는 방법이 있다. GET 방식인 URL 주소의 쿼리 스트링으로 Model 객체의 속성을 추가해서 전달한다.
/**
* RedirectAttributes
*/
@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}";
}
리다이렉트 할 때 간단히 status=true 를 추가해보자. 그리고 뷰 템플릿에서 이 값이 있으면,
저장되었습니다. 라는 메시지를 출력해보자.
실행해보면 다음과 같은 리다이렉트 결과가 나온다.
http://localhost:8080/basic/items/3?status=true
RedirectAttributes
RedirectAttributes 를 사용하면 URL 인코딩도 해주고, pathVarible , 쿼리 파라미터까지 처리해준다.
redirect:/basic/items/{itemId}
pathVariable 바인딩: {itemId}
나머지는 쿼리 파라미터로 처리: ?status=true
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<!-- 추가 -->
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능
'김영한 강의 요약' 카테고리의 다른 글
타임리프 스프링 통합과 폼 (0) | 2023.11.03 |
---|---|
타임리프 기본 기능 (0) | 2023.10.23 |
스프링 요청매핑, HTTP 요청, HTTP 응답 (0) | 2023.10.11 |
스프링 로깅 (0) | 2023.10.04 |
Spring DispatcherServlet(FrontController 패턴) 코드로 이해하기 (0) | 2023.08.09 |