[Spring MVC] [1] 7. 스프링 MVC - 웹 페이지 만들기_1

윤경·2021년 9월 13일
0

Spring MVC

목록 보기
11/26
post-thumbnail

[1] 프로젝트 생성

압축 풀어서 build.gradle로 열기 !!

➡️ 조금 더 빠른 실행을 위함

✔️ index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body> <ul>
    <li>상품 관리 <ul>
        <li><a href="/basic/items">상품 관리 - 기본</a></li> </ul>
    </li> </ul>
</body>
</html>


[2] 요구사항 분석

📌 요구사항

상품 도메인 모델

  • 상품 ID / 상품명 / 가격 / 수량

상품 관리 기능

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

서비스 제공 흐름

기본적으로 백엔드 개발자는 아래의 흐름을 알고 있어야 함.
디자이너, 웹 퍼블리셔, 백엔드 개발자로 역할을 나눠 수행하기로 했을 때,

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


[3] 상품 도메인 개발

📌 파일 구조

✔️ item

package hello.itemservice.domain.item;


import lombok.Data;

@Data // 이걸 쓰면 getter, setter 이외에도 모두 생성해주기 때문에 !위험! 원랜 아래와 같이 분리해 쓰는 것을 추천
//@Getter @Setter
public class Item {

    private Long id;
    private String itemName;
    private Integer price;      // Integer로 선언하는 이유는 값이 안 들어갈 때를 대비 (null로 들어갈 수 있음)
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

✔️ ItemRepository

📌 import는 항상 java.util!!

그리고 지금이야 작은 프로젝트니까 이렇게 해도 규모가 조금 커진다고 했을 때 중복명확성을 따지자면 중복 >>>> 명확성 !

package hello.itemservice.domain.item;

import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
public class ItemRepository {

    // static 사용했다는 것에 주의
    private static final Map<Long, Item> store = new HashMap<>();
    private static long sequence = 0L;

    // item을 저장하는 기능
    public Item save(Item item) {
        item.setId(++sequence);
        store.put(item.getId(), item);
        return item;
    }

    public Item findById(Long id) {
        return store.get(id);
    }

    // List import 할 때 java.util로
    public List<Item> findAll() {
        return new ArrayList<>(store.values());
    }

    public void update(Long itemId, Item updateParam) {
        Item findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    public void clearStore() {
        store.clear();
    }
}

✔️ ItemRepositoryTest

이렇게 생성됨.

package hello.itemservice.domain.item;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;


class ItemRepositoryTest {

    ItemRepository itemRepository = new ItemRepository();

    // 하나 실행될 때마다 호출돼 데이터를 clear 시킴
    @AfterEach
    void afterEach() {
        itemRepository.clearStore();
    }

    @Test
    void save() {
        // given
        Item item = new Item("itemA", 10000, 10);

        // when
        Item savedItem = itemRepository.save(item);

        // then
        Item findItem = itemRepository.findById(item.getId());
        assertThat(findItem).isEqualTo(savedItem);
    }

    @Test
    void findAll() {
        // given
        Item item1 = new Item("item1", 10000, 10);
        Item item2 = new Item("item2", 20000, 20);

        itemRepository.save(item1);
        itemRepository.save(item2);

        // when
        List<Item> result = itemRepository.findAll();

        // then
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(item1, item2);
    }

    @Test
    void updateItem() {
        // given
        Item item = new Item("item1", 10000, 10);

        Item savedItem = itemRepository.save(item);
        Long itemId = savedItem.getId();

        // when
        Item updateParam = new Item("item2", 20000, 30);
        itemRepository.update(itemId, updateParam);

        Item findItem = itemRepository.findById(itemId);

        // then
        assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
        assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
        assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
    }
}


[4] 상품 서비스 HTML

bootstrap

여기에서 Compiled CSS and JS 다운로드 후 다음 폴더에 복사 붙여넣기

그리고 되는지 먼저 테스트 해보자.
이렇게 안 열리면 out 이라는 폴더를 지우고 서버 다시 키고 다시 해보기 (http://localhost:8080/css/bootstrap.min.css)

원래 이렇게 뜨는 것이 정상

/resources/static 경로에 넣어두어서 스프링 부트가 정적 리소스를 제공 가능. 정적 리소스기 때문에 해당 파일을 탐색기에서 직접 열어도 동작하는 것을 확인 가능.

‼️ 정적 리소스가 공개되는 /resources/static 폴더에 HTML을 넣어두면 실제 서비스에서도 공개되기 때문에 서비스를 운영할 땐 공개할 필요가 없는 HTML은 여기에 두지 않는 것이 좋다.

📌 html 파일 구조

✔️ items

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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'" 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>
                <td><a href="item.html">1</a></td>
                <td><a href="item.html">테스트 상품1</a></td>
                <td>10000</td>
                <td>10</td>
            </tr>
            <tr>
                <td><a href="item.html">2</a></td>
                <td><a href="item.html">테스트 상품2</a></td>
                <td>20000</td>
                <td>20</td>
            </tr>
            </tbody>
        </table>
    </div>

</div> <!-- /container -->

</body>
</html>

✔️ item

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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>

    <div>
        <label for="itemId">상품 ID</label>
        <input type="text" id="itemId" name="itemId" class="form-control" value="1" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" name="itemName" class="form-control" value="상품A" readonly>
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control" value="10000" readonly>
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control" value="10" readonly>
    </div>

    <hr class="my-4">

    <div class="row">
        <div class="col">
            <button class="w-100 btn btn-primary btn-lg" onclick="location.href='editForm.html'" type="button">상품 수정</button>
        </div>
        <div class="col">
            <button class="w-100 btn btn-secondary btn-lg" onclick="location.href='items.html'" type="button">목록으로</button>
        </div>
    </div>

</div> <!-- /container -->
</body>
</html>

✔️ addForm

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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" 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'" type="button">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

✔️ editForm

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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>

    <form action="item.html" method="post">
        <div>
            <label for="id">상품 ID</label>
            <input type="text" id="id" name="id" class="form-control" value="1" readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="form-control" value="상품A">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control" value="10000">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="form-control" value="10">
        </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='item.html'" type="button">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

html이 잘 되는지 확인! 이렇게 전체 경로를 복사해 경로 붙여넣기를 하면 잘 되는지 바로 확인 가능한

다 잘 되는지 확인 했음 ! 따로 잘 나오는 html 결과는 넣지 않겠음


[5] 상품 목록 - 타임리프

컨트롤러와 뷰 템플릿 개발하기

✔️ BasicItemController

package hello.itemservice.web;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.PostConstruct;
import java.util.List;

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

    /*
    test용 데이터를 추가
     */
    @PostConstruct
    public void init() {
        itemRepository.save(new Item("itemA", 10000, 10));
        itemRepository.save(new Item("itemB", 20000, 20));
    }
}

컨트롤러 로직은 itemRepository에서 모든 상품을 조회한 다음 모델에 담기 → 그리고 뷰 템플릿 호출

📌 @RequiredArgsContructor: final이 붙은 멤버 변수만 사용해 생성자를 자동 생성
➡️ 따라서 final 키워드 절대 빼지 않기(빼면 ItemRepository 의존 관계 주입 X)

그리고 생성자가 딱 하나 @Autowired(의존관계 주입) 생략 가능

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

✔️ items.html

(이미 있던 items.html 을 복붙해서 코드 수정하기)

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          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=th:href=

타임리프 뷰 템플릿을 거치면 원래 값을 th:xxx값으로 변경하고 만약 값이 없다면 생성한다.
HTML을 그대로 볼 땐 href 속성이 사용되고 뷰 템플릿을 거치면 th:href 값이 href로 대체되면서 동적으로 변경 가능해진다.
대부분의 HTML 속성을 th:xxx로 변경할 수 있다.

✔️ 타임리프 핵심

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

✔️ URL 링크 표현식 - @{...}

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

@{...}: 타임리프는 url 링크를 사용하는 경우 @{...}를 사용한다. ➡️ URL 링크 표현식
이걸 사용하면 서블릿 컨텍스트를 자동으로 포함한다. (현재는 서블릿 컨텍스트가 필요 없어짐. 링크 앞에 applicationA 막 이런식으로 붙였던,,)

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

반복을 위해 사용하며 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고 반복문 안에서 item 변수를 사용할 수 있다.
컬렉션의 수 만큼 <tr>..</tr> 하위 태그를 포함해 생성된다.

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

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

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

✔️ 내용 변경 - th:text

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

내용의 값을 th:text 값으로 변경한다.

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

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

(상품 ID를 선택하는 링크를 확인)
url링크 표현식을 사용하면 경로를 템플릿처럼 편리하게 사용할 수 있다.
경로 변수 ({itemId}) 뿐만 아니라 쿼리 파라미터도 생성

Ex.
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도 그대로 유지하고, 뷰 템플릿도 사용할 수 있는 타임 리프의 특징네츄럴 템플릿(natural templates)이라고 한다.


내용이 길어서 자르고 2편으로

profile
개발 바보 이사 중

0개의 댓글