스프링 MVC - 웹페이지 만들기

유동우·2023년 5월 10일
0
post-thumbnail

상품 서비스 HTML

/resources/static : 스프링 부트가 정적 리소스를 제공

실제 서비스에서도 공개되어 공개할 필요없는 HTML을 두는 것은 주의해야한다.

/resources/template : 스프링 부트가 동적 리소스를 제공


상품 목록 - 타임리프

@RequiredArgsConstructor

  • final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.

//생성자가 하나만 있으면 해당 생성자에 자동으로 @Autowired 의존관계 주입
public BasicItemController(ItemRepository itemRepository) {
    this.itemRepository = itemRepository;
}

타임리프 사용 선언

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

속성변경

th:href="@{/css/bootstrap.min.css}"

HTMl을 그대로 볼 때는 href 속성이 사용되고, 뷰 템플릿을 거치면 th:href의 값이 href로 대체되면서 동적으로 변경할 수 있다.


타임리프 핵심

  • 핵심은 th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체한다. th:xxx 이 없으면 기존 html의 xxx 속성이 그대로 사용된다.
  • HTML을 파일로 직접 열었을 때, th:xxx 가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시한다.
  • 따라서 HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.

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

  • ex) th:href="@{/css/bootstrap.min.css}"
  • 서블릿 컨텍스트 자동으로 포함

상품 등록 폼으로 이동, 속성 변경 - th:onclick

  • onclick="location.href='addForm.html'"
  • th:onclick="|location.href='@{/basic/items/add}'|"

리터럴 대체 - |...|

  • 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 하는데,
  • 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용 가능.

반복 출력 - th:each

<tr th:each="item : ${items}">
  • 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함, 반복문 안에서 item 변수를 사용 할 수 있음

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

<td th:text="${item.price}">10000</td>
  • 프로퍼티 접근법을 사용하여, 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.

내용 변경 - th:text

<td th:text="${item.price}">10000</td>
  • 10000을 ${item.price}의 값으로 변경

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

th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
  • 상품 ID를 선택하는 링크
  • 경로를 템플릿처럼 편리하게 사용 가능
  • 경로 변수뿐만 아니라 쿼리 파라미터도 생성한다.

URL 링크 간단히

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

JSP vs Thymeleaf

JSP 파일은 웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 뒤죽박죽 되어서 정상적 확인 불가능

Thymeleaf는 순수 HTML 파일을 웹 브라우저에서 확인 가능하고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과 또한 확인이 가능하다.
=> 네츄럴 템플릿 (=natural templates)

상품 상세

BasicItemController에 추가 

@GetMapping("/{itemId}")
    public String item(@PathVariable Long itemId, Model model) {
		Item item = itemRepository.findById(itemId); //PathVariable로 넘어온 상품 ID로 상품을 조회
     	model.addAttribute("item", item); //모델에 담음
     	return "basic/item"; //뷰 템플릿 호출
}

상품 등록 폼

  @GetMapping("/add")
  public String addForm() {
      return "basic/addForm";
  }
<form action="item.html" th:action method="post">
  • th:action
  • HTMl form에서 action에 값이 없으면 현재 URL에 데이터를 전송
  • 상품 등록 폼: GET /basic/items/add
  • 상품 등록 처리: POST /basic/items/add
  • 위 와 같이 폼과 처리 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을 구분

상품 등록 처리 - @ModelAttribute

 //요청 파라미터 형식을 처리해야 하므로 @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"; //상품 상세에 사용한 item.html 뷰 템플릿을 그대로 재활용
 }
 
 /**
* @ModelAttribute("item") Item item
* model.addAttribute("item", item); 자동 추가 */

  @PostMapping("/add")
  public String addItemV2(@ModelAttribute("item") Item item, Model model)   { 
itemRepository.save(item); //model.addAttribute("item", item); //자동 추가, 생략 가능
      return "basic/item";
  }
  
  //클래스의 첫 글자만 소문자로 바꿔서 등록
   @PostMapping("/add")
  public String addItemV3(@ModelAttribute Item item) {
      itemRepository.save(item);
      return "basic/item";
  }
  
  //ModelAttribute 전체 생략
   @PostMapping("/add")
   public String addItemV4(Item item) {
      itemRepository.save(item);
      return "basic/item";
   }

ModelAttribute 2가지 역할
1. 요청 파라미터 처리 : Item 객체를 생성하고, 요청파라미터의 값을 프로퍼티 접근법(setXXX) 입력
2. Model 추가 : @ModelAttribute에 지정한 name(value) 속성을 사용.

만약 이름과 객체 이름이 다르다면


상품 수정

//BasicItemController에 추가
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
      Item item = itemRepository.findById(itemId);
      model.addAttribute("item", item);
      return "basic/editForm";
}

@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:/... 로 편하게 사용 가능
  • ex) redirect:/basic/items/{itemId}
  • 컨트롤러에 매핑된 @PathVariable 값은 redirect에도 사용 가능
  • redirect:/basic/items/{itemId} => {itemId} 는 @PathVariable Long itemId 의 값을 그대로 사용한다.

PRG Post/Redirect/Get

상품 등록을 한 후 새로고침을 하면 같은 폼이 계속 중복돼서 등록된다
-> 웹 브라우저의 새로고침은 마지막에 서버에 전송한 데이터를 다시 전송
-> 즉, 마지막이 POST /add 이므로 POST 방식을 다시 전송하게 된다.

새로고침문제를 해결하기 위해 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 리다이렉트 호출
-> 따라서 마지막에 호출한 내용이 상품 상세 화면인 GET /items/{id} 가 된다.

//BasicItemController에 추가
 /**
  * PRG - Post/Redirect/Get
  */
   
@PostMapping("/add")
public String addItemV5(Item item) {
      itemRepository.save(item);
      return "redirect:/basic/items/" + item.getId(); // URL 인코딩이 안되어 위험 -> RedirectAttributes 사용하자
}

RedirectAttributes

고객에게 "저장되었습니다" 라는 메세지를 띄워보자.

/**
 * 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}";
}
  1. 뷰 템플릿에 status=true가 있으면 저장되었습니다 출력
  2. http://localhost:8080/basic/items/3?status=true -> 실제 리다이렉트 결과 주소

RedirectAttributes

  • URL 인코딩도 해주고, pathVariable, 쿼리 파라미터까지 처리 가능
  • redirect: /basic/items/{itemId}
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
  • th:if 해당 조건이 참이면 실행
  • ${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능
  • 원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 한다. 그런데 쿼리 파라미터는 자주 사용해서 타임리프에서 직접 지원한다.

정리


Reference
김영한 님 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
효율적이고 꾸준하게

0개의 댓글