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

링딩·2022년 8월 3일
0

스프링 MVC

목록 보기
7/18

이 글은 김영한 강사님의 강의를 참고하여 작성하였습니다.


Chap 7. 웹 페이지 만들기

들어가기 앞서 gradle과 lombok 확인 잊지말자

+) 지나가다 각자의 역할들

요구사항이 정리되고 디자이너, 웹 퍼블리셔, 백엔드 개발자가 업무를 나누어 진행한다.

  • 디자이너: 요구사항에 맞도록 디자인하고, 디자인 결과물을 웹 퍼블리셔에게 넘겨준다.
  • 웹 퍼블리셔: 다자이너에서 받은 디자인을 기반으로 HTML, CSS를 만들어 개발자에게 제공한다.
  • 백엔드 개발자: 디자이너, 웹 퍼블리셔를 통해서 HTML 화면이 나오기 전까지 시스템을 설계하고, 핵심
    비즈니스 모델을 개발한다. 이후 HTML이 나오면 이 HTML을 뷰 템플릿으로 변환해서 동적으로 화면을
    그리고, 또 웹 화면의 흐름을 제어한다.

참고

  • React, Vue.js 같은 웹 클라이언트 기술을 사용하고, 웹 프론트엔드 개발자가 별도로 있으면, 웹 프론트엔드 개발자가 웹 퍼블리셔 역할까지 포함해서 하는 경우도 있다.
  • 웹 클라이언트 기술을 사용하면, 웹 프론트엔드 개발자가 HTML을 동적으로 만드는 역할과 웹 화면의
    흐름을 담당한다.
    -> 이 경우 백엔드 개발자는 HTML 뷰 템플릿을 직접 만지는 대신에, HTTP API를 통해 웹
    클라이언트가 필요로 하는 데이터와 기능을 제공하면 된다.



요구사항 분석

이 웹 페이지 만들기에서의 목표: 상품을 관리할 수 있는 서비스

  • 상품 도메인 모델
    상품 ID
    상품명
    가격
    수량

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

상품 서비스

부트스트랩

부트스트랩 링크

* 부트스트랩(Bootstrap)이란?

웹사이트를 쉽게 만들 수 있게 도와주는 HTML, CSS, JS 프레임워크이다.
하나의 CSS로 휴대폰, 태블릿, 데스크탑까지 다양한 기기에서 작동한다. 다양한 기능을 제공하여 사용자가 쉽게 웹사이트를 제작, 유지, 보수할 수 있도록 도와준다

정적 파일이기 때문에 위치 경로는
resources/static/ 아래에 css 폴더를 따로 만들어서 그 곳에 넣어주자.

  • 주의 🧨
    이렇게 정적 리소스가 공개되는 resources/static/ 폴더에 HTML을 넣어두면, 실제 서비스에서도 공개된다.
    => 곧 서비스를 운영한다면 지금처럼 공개할 필요없는 HTML을 두는 것은 주의



상품 목록

@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));
    }
}

@RequiredArgsConstructor

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

+) @Autowired

  • 이렇게 생성자가 딱 1개만 있으면, @Autowired 생략이 가능
    -> 스프링이 해당 생성자에 @Autowired 로 의존관계를 주입해준다

테스트용 데이터

  • why? 테스트용 데이터가 없으면 회원 목록 기능이 정상 동작하는지 확인하기 어렵다.
  • @PostConstruct : 해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출된다.
    -> 여기서는 간단히 테스트용 테이터를 넣기 위해서 사용했다.


타임리프 _(feat 상품목록)

먼저 뷰 템플릿이기 때문에 items.html 정적 HTML을 -> 뷰 템플릿(templates) 영역으로 복사하자

/resources/templates/basic/items.html



<!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="items.html" th:href="@{/basic/items/{itemId}
(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
                <td><a href="items.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 링크 표현식이라 한다.

속성 변경 - th:onclick

속성 변경 - 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}!|">

ex)
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 변수를 사용할 수 있다.
  • 컬렉션의 수 만큼 .. 이 하위 테그를 포함해서 생성된다.

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

<td th:text="${item.price}">10000</td>

  • 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.
  • 프로퍼티 접근법을 사용한다. ( item.getPrice() )

내용 변경 - th:text

<td th:text="${item.price}">10000</td>

  • 기능: 내용의 값을 th:text 의 값으로 변경한다.
    여기서는 10000을 ${item.price} 의 값으로 변경한다.

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

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

  • URL 링크 표현식을 사용하면 경로를 템플릿처럼 편리하게 사용할 수 있다.
  • 경로 변수( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성한다.
    예) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
    -> 생성 링크: http://localhost:8080/basic/items/1?query=test

URL 링크 간단히

th:href="@{|/basic/items/${item.id}|}"
상품 이름을 선택하는 링크를 확인해보자.
리터럴 대체 문법을 활용해서 간단히 사용할 수도 있


아~ 이렇게 '순수 HTML'을 그대로 유지하면서, '뷰 템플릿'도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿 이라 하는 구나~ 😮


    /**
     * 특정 헤더로 추가 매핑
     * headers="mode",
     * headers="!mode"
     * headers="mode=debug"
     * headers="mode!=debug" (! = )
     */
    @GetMapping(value = "/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }




@ModelAttribute (feat. 상품 등록 처리)

상품들을 등록하려면 정말,,, 정말 많은 과정을 거쳤다. 한 번 다 봐보자

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";
    }  return "ok";
    }
  • Item 객체를 생성하고 itemRepository 를 통해서 저장한다.
  • 저장된 item 을 모델에 담아서 뷰에 전달
profile
초짜 백엔드 개린이

0개의 댓글