ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스프링 MVC 1] 7. 스프링 MVC - 웹 페이지 만들기
    INFLEARN/스프링 MVC 1편 2021. 11. 5. 17:45

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

     

    [강의 정리] 김영한님 :: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

     

     

    1. 프로젝트 생성

    # 프로젝트 생성

    • Packaging은 Jar로

    # Welcome 페이지

    • 스프링 부트에서 Jar를 사용하기 위해 /resources/static 위치에 index.html파일을 두었음
    • 스프링 부트가 Welcome 페이지로 처리해줌

     

    2. 요구사항 분석

    # 서비스 제공 흐름

    • 상품 목록, 상세, 등록, 수정

    출처: 김영한님 강의 자료

     

    3. 상품 도메인 개발

    # Item - 상품 객체

    • 핵심 도메인 같은 경우 @Data 사용하는거 위험할 수 있어서 @Getter, @Setter 등으로 분리하는게 좋음

     

    # ItemRepository - 상품 저장소

    • public Item save(Item item)
    • public Item findById(Long id)
    • public List<Item> findAll()
    • public void update(Long itemId, Item updateParam)

     

    # ItemRepositoryTest - 상품 저장소 테스트

    • test 끝날때마다 실행 (초기화)

    • junit 5부터는 public 필요없음
    • void save()
    • void findAll()
    • void updateItem()

     

    4. 상품 서비스 HTML

    # 부트스트랩 (css)

    • /resources/static/css/bootstrap.min.css 추가
    • /resources/static에 넣어 두었으므로, 스프링 부트가 정적 리소스를 제공
    • 단, 리소스가 공개되는 해당 폴더에 HTML을 넣어두면 실제 서비스에도 공개 되므로 주의 필요

    # HTML 파일

    • 상품 목록 : /resources/static/html/items.html
    • 상품 상세 : /resources/static/html/item.html
    • 상품 등록 폼 : /resources/static/html/addForm.html
    • 상품 수정 폼 : /resources/static/html/editForm.html

     

    5. 상품 목록 - 타임리프

    # BasicItemController

    • 컨트롤러 로직은 itemRepository에서 모든 상품을 조회한 다음에 모델에 담고 뷰 템플릿을 호출
    • @RequiredArgsConstructor
      • final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.
      • public BasicItemController(ItemRepository itemRepository) { this.itemRepository = itemRepository; }
      • 이렇게 생성자만 딱 1개만 있으면, 스프링이 해당 생성자에 @Autowired로 의존관계 주입
      • 따라서 final 키워드를 빼면 안됨 (의존관계 주입 안됨)
    • 테스트용 데이터 추가
      • @PostConstruct : 해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출

     

    # [타임리프] 사용선언

     

    #  [타임리프] 속성 변경 - th:href

    • hreft="value1" -> th:href="value2"의 값으로 변경
    • 타임리프 뷰 템플릿을 거치게 되면 원래 값을 th:xxx 값으로 변경
    • HTML을 그대로 보면 href 속성 사용, 뷰 템플릿 거치면 th:href의 값이 href로 대체되면서 동적으로 변경
    • <link href="../css/bootstrap.min.css"
      th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

     

    #  [타임리프] 핵심

    • th:xxx가 붙은 부분은 서버사이드에서 렌더링 되고 기존 것을 대체
    • th:xxx가 없으면 html의 xxx속성 그대로 사용
    • 그냥 html 파일 열었을 때와 뷰 템플릿일 때 다름

     

    # [타임리프] URL 링크 표현식 - @{...}

    • th:href="@{/css/bootstrap.min.css}
    • @{...} : 타임리프의 URL 링크 표현식

     

    [타임리프] 속성 변경 - th:onclick

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

     

    # [타임리프] 리터럴 대체 - |...|

    • 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더하기를 사용해야하는데, 리터럴 대체 문법을 사용하면 더하기 없이 사용 가능
    • 결과 : location.href='/basic/items/add'
    • 더하기 사용 : th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
    • 리터럴 대체 사용 : th:onclick="|location.href='@{/basic/items/add}'|"

     

    # [타임리프] 반복 출력 - th:each

    • 반복은 th:each를 사용하여 모델에 포함된 컬렉션 데이터(items)를 변수(item)에 하나씩 넣고 사용가능
    • 컬렉션 수 만큼 <tr> .. </tr>이 하위 태그를 포함하여 생성됨
    • <tr th:each="item : ${items}">

     

    [타임리프] 변수 표현식 - ${...}

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

     

    [타임리프] 내용 변경 - th:text

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

     

    [타임리프] URL 링크 표현식2 - @{...}

    • 상품 ID를 선택하는 링크
      • th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
    • 경로 변수 {itemId} 뿐만 아니라, 쿼리 파라미터도 생성가능
      • th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
      • 생성링크 : http://localhost:8080/basic/items/1?query=test

     

    [타임리프] URL 링크 간단히

    • 리터럴 대체 문법을 활용하여 URL 링크 걸 수 있음
    • th:href="@{|/basic/items/${item.id}|}"

     

    +) 참고

    • 타임리프는 네츄럴 템플릿(natural templates) 이기 때문에, 순수 HTML 파일을 웹 브라우저에서 열어서 확인하고 그대로 유지하면서, 뷰 템플릿도 사용할 수 있음

     

     

    6. 상품 상세

     

    # 상품 상세 컨트롤러

    • @PathVariable로 넘어온 상품 ID로 조회하고 모델에 담고, 뷰 템플릿 호출

     

    # 상품 상세 뷰

    • /resources/static/item.html => /resources/static/templates/basic/item.html
    • 속성 변경, 상품수정 링크 변경, 목록으로 링크 변경

     

    7. 상품 등록 폼

     

    # 상품 등록 폼 컨트롤러 (GET)

    • 단순 뷰 호출

     

    # 상품 등록 폼 뷰

    • /resources/static/addForm.html => /resources/static/templates/basic/addForm.html

     

    # 상품 등록 처리 컨트롤러 (POST)

    • 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고, HTTP 메서드로 두 기능을 구분
      • @GetMapping("/add")
      • @PostMapping("/add")

     

    # 조합

    • 중복 제거 가능 (클래스 레벨에 공통적인 부분 뺌)

     

    8. 상품 등록 처리 - @ModelAttribute

     

    # POST - HTML Form

    • content-type: application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파라미터 형식으로 전달 : itemName=itemA&price=10000&quantity=10

     

    # addItemV1 - @RequestParam

    • 요청 파라미터 형식을 처리해야 하므로 @RequestParam을 사용
    • itemName, price, quantity 요청 파라미터 데이터를 해당 변수들에 받는다
    • Item 객체를 생성하고 set을 한 후, itemRepository를 통해서 저장
    • 저장된 item을 모델에 담아서 뷰에 전달

     

    # addItemV2 - 상품 등록 처리 - @ModelAttribute

    • 기존의 @RequestParam으로 변수를 하나씩 받아서 Item 생성하는 것이 불편 @ModelAttribute 사용해서 한 번에 처리 가능
    1. @ModelAttribute - 요청 파라미터 처리
      • @ModelAttribute는 Item 객체를 생성하고, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)로 입력
    2. @ModelAttribute - Model 추가
      • Model에 @ModelAttribute로 지정한 객체를 자동으로 넣어줌
      • model.addAttribute(..) 부분을 생략해도 동작함

     

    # addItemV3 - 상품 등록 처리 - ModelAttribute 이름 생략

    • @ModelAttribute name 생략 가능
    • 생략시 model에 저장되는 name은 클래스명 첫 글자만 소문자로 변경해서 자동으로 등록 (ex. Item -> item)

     

    # addItemV4 - 상품 등록 처리 - ModelAttribute 전체 생략

    • @ModelAttribute 자체도 생략 가능 
    • 대상 객체는 모델에 자동 등록

     

     

    9. 상품 수정

     

    # 상품 수정 컨트롤러

    • 수정에 필요한 정보를 조회하고, 수정용 폼 뷰를 호출

     

    # 상품 수정 폼 뷰

    • /resources/static/editForm.html => /resources/static/templates/basic/editForm.html

     

    # 상품 수정 개발

    • 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고, HTTP 메서드로 두 기능을 구분
      • 상품 수정 폼 : GET /items/{itemId}/edit
      • 상품 수정 처리 : POST /items/{itemId}/edit

    • 리다이렉트
      • 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동
      • 스프링은 redirect:/... 를 지원
      • 컨트롤러에 매핑된 @PathVariable의 값은 redirect에서도 사용할 수 있음
      • 로케이션이 실제로 이동 되는 것이며, URL 변경 (컨트롤러부터 다시 호출)

     

    +) 참고

    • HTML Form 전송은 PUT, PATCH를 지원하지 않음
    • GET, POST만 사용할 수 있음
    • PUT, PATCH는 HTTP API 전송시 사용

     

    10. PRG Post/Redirect/Get

     

    # 상품 중복 등록

    • 지금까지 만들어놓은 상품 등록 처리 컨트롤러에서는 상품 등록을 완료하고 웹 브라우저의 새로그침 버튼을 클릭하면 상품이 계속해서 중복 등록 됨 (id값이 계속 올라가고, 같은 내용 상품 등록)
    • 상품 저장을 하고 나면 뷰 템플릿을 호출하게 되는데 이 때 마지막으로 남은 URL을 계속 호출함

    출처 : 김영한님 강의 자료

     

    # POST 등록 후 새로고침

    • 웹 브라우제의 새로 고침은 마지막에 서버에 전송한 데이터 재전송
    • 상품 등록 폼에서 데이터를 입력하고 저장을 선택하면 POST /add + 상품 데이터를 서버에 전송하게 되고, 이 상태에서 새로고침하면 또 똑같이 POST /add + 상품 데이터를 서버에 전송하게 됨

    출처 : 김영한님 강의 자료

    • 새로고침 : POST /add

     

     

    # POST, Redirect GET

    • 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 리다이렉트 호출
    • 즉, POST 후 실제 상품 상세 화면으로 이동하기 때문에,
    • 새로고침을 하게 되면 가장 마지막에 호출했던 상품 상세 화면 GET /items/{id}가 호출됨

    출처 : 김영한님 강의 자료

    • 새로고침 : GET /items/{id}

    • 이와 같은 문제 해결 방식을 PRG Post/Redirect/Get 이라 함
    • 단, 아래와 같이 URL 변수를 더해서 사용하면 URL 인코딩이 되지 않으므로 RedirectAttributes를 사용해야 함

     

     

    11. RedirectAttributes

     

    # RedirectAttributes

    • 리다이렉트 할 때 간단히 status = true를 추가하여, 뷰 템플릿에서 값이 있으면 메시지 출력
    • RedirectAttributes를 사용하면 URL 인코딩, pathVariable, 쿼리 파라미터 처리 가능
      • pathVariable 바인딩 : {itemId}
      • 나머지는 쿼리 파라미터로 처리 : ?status=true
      • http://localhost:8080/basic/items/3?status=true

     

    # 저장 후 메시지 노출 (뷰 템플릿에 메시지 추가)

    • <h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
      • th:if : 해당 조건이 참이면 실행 (status가 true 일 때)
      • ${param.status} : param은 타임리프에서 쿼리 파라미터를 편리하게 조회할 수 있게 지원해주는 기능 (원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야함)

    "저장 완료" 메시지 출력

     

    댓글

Programming Diary