ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 MVC - 웹 페이지 만들기
    Web/Spring 2023. 10. 15. 16:15

     

    1. 프로젝트 생성 

     

    스프링 부트 2.7, packaging Jar, dependencies: Spring Web, Thymeleaf,Lombok

     

    1.1 /resources/static/index.html -> 웰컴페이지 추가함 

     

     

    2. 요구사항 분석 

     

    상품을 관리할 수 있는 서비스 만들기 

     

    상품 도메인 : 상품 ID, 상품명, 가격, 수량

    상품 관리 기능 : 상품목록, 상품상세, 상품등록,상품 수정 

    출처: 스프링MVC(김영한) - 인프런

     

    3. 상품 도메인 개발 

    @Data
    public class Item {
    
     private Long id;
     private String itemName;
     private Integer price;
     private Integer quantity;
     public Item() {
     
     }
     	//..
     }

     > price와 quantity는 Integer로 선언하여, 0이거나 null일 때도 값을 처리할 수 있도록 함 

     

    - 상품저장소 

    @repository
    public class ItemRepository{
    
    	private static final Map<Long, Item< store = new HashMap<>();
        private static final sequence = 0L;
        
        public Item save(Item item){
        	item.setId(++sequence);
            stroe.put(item.getId(), item);
            return item;
        }
        
        //..
        
        public void update(Long itemId, Item updateParam){
        	Item findItem = findById(itemId);
            findItem.setItemName(updateParam.getItemName());
            //..
        }
    }

    - update 부분에서 item 파라미터 DTO같은 것 따로 만들어서 넣는 게 좋다. 

      그냥 item으로 받으면, 수정할 정보 외에도 너무 많은 걸 담고 있다. (명확성이 떨어짐) 

     

    * 실무에서는 hashmap 쓰면 안된다. (싱글톤 + 멀티스레드)

    * 싱글톤으로 관리되기 때문에 사실 static 안써도 상관 없긴함! 

      다만 따로 생성해서 테스트 하거나 할때 확인하려면, static으로 해야함 

    * Long도 그냥 쓰면 안되고 아토믹 롱? 같은 것 써야함 

     

     

    4. 상품서비스 HTML 

      

    -> HTML파일 따로 다운받음 (CSS 포함) 

    -> 정적 파일은 직접 열어도 잘 열림 

     

    5. 상품목록 - 타임리프 

     

    BasicItemController 

    @Controller 
    @RequestMapping("/basic/items")
    @RequiredArgsConstructor // final붙은애들 사용해서 생성자 만들어준다.
    public class BasicItemController {
    
    	private final ItemRepository itemRepository; //생성자가 하나면, Autowired생략해도 됨
        
        
        @GetMapping
        public String item(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));
     }
    }

    - 컨트롤러 로직은 모든 상품 조회해서 뷰 템플릿에 담음 

    - items.html 정적 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>
    
    <!-- -->
    <button class="btn btn-primary float-end" onclick="location.href='addForm.html'" 
    th:onclick="|location.href='@{/basic/items/add}'|"
    <!-- ||는 문자열을 이어주는 타임리프 문법 -->
    type="button">상품 등록</button>
    
    <!-- -->
    <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>

     

    - 타임리프의 장점은 html태그에 코드를 때려박아도, 기존 html을 최대한 유지한다는 것

       추후에 마크업 작업을 해도 html코드가 깨지지 않는다.

     

    > 타임리프 사용 선언 

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

    > 속성 변경 - th:href 등 

      타임리프 뷰 템플릿을 거치게 되면, 원래 값을 변경함 (값이 없다면 새로 생성)

      HTML을 그대로 볼 때는 href속성이 사용되고, 뷰 템플릿을 거치면, th:href의 값으로 치환

      대부분의 HTML 속성을 th:xxx로 변경할 수 있음 

     

    > 타임리프 핵심 

       핵심은 th:xxx가 붙은 부분은 서버사이드 랜더링, 나머지는 기존 코드 사용 

       HTML 파일을 직접 열면 th:xxx가 있어도 속성을 알지 못하므로 무시함 

     

    > URL 링크 표현식 @(..)

       타임리프의 URL링크 표현식이다. (서블릿 컨텍스트를 자동으로 포함한다)

     

    > 리터럴 대체 |...|

       타임리프는 문자와 표현식을 나누어 관리하므로, 더해서 사용해야한다.

    <span th:text="'Welcome to our application, ' + ${user.name} + '!'">

       이때 리터럴 대체문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.

     

    > 반복 출력: -th:each

    <tr th:each="item : ${items}">

    items의 데이터가 item 변수에 담기고, 반복문 안에서 사용할 수 있다.

     

    > 변수 표현식 ${} 

        프러퍼티 접근법을 사용하여, 변수에 접근가능 

        ${} -> 변수.getxxx와 동일 

     

    > 내용 변경 - th:text

       HTML 태그 사이 내용을 대체한다.

     

    > 링크 표현식 2 

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

    URL 링크 표현식으로 경로를 템플릿 처럼 편리하게 사용가능 

    경로변수 + 쿼리 파라미터도 생성 가능하다.

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

     

     

    *정적페이지에서 css경로를 절대경로로 바꾸고 파일을 열면 

      /css > C:css로 먹음 !! 상대경로로 잘 쓰자.. 웹 프로젝트의 경우 

      root가 보통 프로젝트 최상위 (webapp / 혹은 resources 임)

    [웹 프로젝트는 웹 페이지를 보여주는게 목적!]

     

    6. 상품 상세 

    @GetMapping("/{itemId}")
    public String item(@PathVariable Long itemId, Model mode){
    	Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        
    	return "basic/item";

    - PathVariable로 넘어온 상품 ID 조회하고 모델에 담은 후 뷰 템플릿 호출

     

    6. 상품 등록

    @GetMapping("/add")
    public String addForm(){
    	return "basic/addForm";
       }

    - 단순히 From으로 이동 

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

    - 상품 등록 처리 : POST / basic/items/add

     

    7. 상품 등록 처리 - @ModelAttribute

     

    - 이제 상품 등록 폼에서 전달된 데이터를 실제 상품 등록을 처리해보자 

    - 상품 등록 폼은 다음 방식으로 서버에 데이터를 전달함 

     

    POST - HTML Form 

                content-type : application/x-www-form-urlencoded

                메시지 바디에 쿼리 파라미터 형식으로 전달 

                요청 파라미터를 처리해야하므로, @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";
    }
    
    
    @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(Item item) {
     
     	itemRepository.save(item);
     	//model.addAttribute("item", item);
     
     return "basic/item";
    }

    - V1 요청 파라미터로 하나씩 다 받은 후에 데이터를 토대로 Item객체를 생성 

    - V2 @ModelAttribute로 요청 파라미터 처리 (프로퍼티 접근법 사용 setXXX로 생성해줌)

             Model추가 : 모델에 @ModelAttribute로 지정한 객체 자동으로 넣어줌 

             모델에 데이터를 담을 때는 이름이 필요함, 이름을 지정하지 않으면 클래스명 사용

    - V3 ModelAttribute생략 (요청 파라미터의 경우 기본 자료형 아니면, 자동으로 붙여줌)

     

    *어차피 Model은 컨트롤러 밖에서 생성되어 주입되는 것!! (어뎁터 + Argument Resolver)

     

    8. 상품 수정 

     

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

     

    > 상품 수정 폼 뷰 (등록폼과 유사) 

    > Get /item/{itemId}/edit : 상품 수정 폼 

    > Post /item/{itemId}/edit : 상품 수정 처리 

     

    - 리다이렉트 

       상품 수정은 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트 호출한다.

    스프링은 redirect:/..로 지원 

    > 컨트롤러에 매핑된 @PathVariable은 redirect에도 사용할 수 있다. 

       (redirect: /basic/items/{itemId} -> @PathVariable Long itemId 그대로 사용

     

    9 PRG: Post/Redirect/Get

     

     - POST 등록 후 새로고침 

    출처: 스프링MVC (인프런) - 김영한

    웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송

    상품 등록 폼에서 데이터 입력하고 저장하면, POST/add + 상품데이터 서버 전송

    새로 고침하면, 또 마지막에 전송한 POST/add가 전송되게 됨 

     

    출처: 스프링MVC (인프런) - 김영한

     

    - 새로 고침 문제를 해결하기 위해 상품 저장 후 뷰 템플릿으로 이동하는 것이 아니라

      상품 상세 화면으로 리다이렉트 호출! (컨트롤러 호출 -> 뷰템플릿은 포워딩처리해서 url변화 없음)

      리다이렉트는 GET 방식으로 컨트롤러 호출함 물리적 URL이 변화 -> 새로고침해도 상세화면만 나온다.

     

    @PostMapping("/add")
    public String addItemV5(Item item){
    	itemRepository.save(item);
        return "redirect:/basic/item/"+ item.getId(); //URL 인코딩 문제 있음 사용x
        
    }

     

    10 RedirectAttributes

     

    리다이렉트 후 -> 고객입장에서 저장이 잘 된건지 확인받고 싶음 

    저장되었다는 메시지 보여달라는 요구사항이 들어옴 

     

    @PostMapping("/add")
    public String addItemV6(Item item, RedirectAttributes rd){
    	Item savedItem = itemRepository.save(item);
        rd.addAttribute("itemId", savedItem.getId());
        rd.addAttribute("status",true);
        return "redirect:/basic/items/{itemId}";
    }

    - RedirectAttriute를 사용하면, URL인코딩, pathVariable,쿼리 파라미터 처리 다해준다.

    - 위 객체에 값을 add시 

    - redirect를 확인해서 {} < 바인딩 가능한건 꺼내서 바인딩하고, 나머지는 전부 쿼리 스트링 처리 

     

    <div class="container">
     <div class="py-5 text-center">
     <h2>상품 상세</h2>
     </div>
     <!-- 추가 -->
     <h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>

    - ${param.status}: 타임리프 쿼리 파라미터 간편 조회 기능 

                                  원래는 모델에 담고 값을 꺼내야하는데, 쿼리파라미터는 자주 사용해서 타임리프에서 지원해줌 

Designed by Tistory.