필기노트

타임리프 간단히 알아보기 본문

김영한 강의 요약

타임리프 간단히 알아보기

우퐁코기 2023. 10. 20. 08:50
반응형

상품 목록

@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&param2=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} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능

 


 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 원

www.inflearn.com

반응형
Comments