본문 바로가기

📢 들어가며

이번 포스팅은 지난 포스팅에서 이어진다.

지난 포스팅에선 클릭이벤트로 주소를 입력 받거나 출력하는 기능을 구현하였다.

이번 포스팅에선 본격적으로 백엔드 기능을 구현해볼 것이다.
Spring Boot + MyBatis + MySQL을 연동하고
주소, 후기 및 별점을 저장하거나 불러오는 API를 만들어볼 것이다.

사진 파일 업로드까지는 이 포스팅에서 다루지 않는다.
파일을 다루는 방식이나 (ex. blob) Vue 의 slot 기능을 구체적으로 알아보기 위해 다음 포스팅에서 자세히 다뤄볼 예정이다.

모든 소스코드는 깃헙에서 확인할 수 있다.

🍜 Spring Boot, Mybatis, MySQL 연동

이 프로젝트의 첫번째 포스팅에서 언급했던 것처럼 MySQL 이 설치된 상태로 가정하고 진행한다.

MySQL 설치가 된 상태라면, 데이터베이스를 만들어주자.
명령 프롬프트 등을 이용해 MySQL 에 로그인 한 뒤 create database 명령어를 사용하면 된다.
다른 DB 툴을 사용해도 상관 없다.

DB 관리 도구로는 DBeaver를 강추한다.
UI 가 알아보기 쉽고 PRO 버전이 아니라면 무료로 사용 가능한데, 웬만한 기능은 무료로 사용 가능하다.
그리고 무엇보다 비버 아이콘이 아주 귀엽다🥰

데이터 베이스 이름은 자유롭게 하면 된다.
나는 good_restaurant 라고 해줬다.

💡 mysql 로그인

> mysql -u [유저 아이디] -p

설치 시 별다를 설정을 해주지 않았다면 id는 root고 비번은 mysql 일 것이다.
-u 는 유저 아이디를 의미하고 -p는 패스워드를 의미하는데,
패스워드는 위 명령어 입력 후 엔터를 치면 타이핑할 수 있다.

💡 데이터베이스 생성

mysql > create good_restaurant;

Spring Boot, Mybatis, MySQL 연동 방법은 다른 포스팅에서 다룬 적이 있다.

Spring Boot + MyBatis + MySQL 연동 방법
☝️☝️☝️
이 글을 보고 Srpring Boot, MyBatis, MySQL 을 연동하자.
테스트코드까지 있어서 어렵지 않게 연동할 수 있을 것이다.

연동 후 디렉토리 상태

포스팅을 따라 잘 따라했다면,
디렉토리 상태가 위와 같을 것이다.

이제 본격적으로 리뷰와 별점을 저장하는 API를 구현해보자.

🍜 Axios 설치

저장 API를 구현해보기에 앞서...
다시 보니 저장버튼을 빠뜨렸다;;
사이드바 하단에 저장 버튼을 추가해주자.

// template

<div class="side-bar">
    // ...
    <div class="bottom-btn-area">
        <BButton class="save-btn">
            저장
        </BButton>
    </div>
</div>

// style
> .bottom-btn-area {
    text-align: right;
    padding-right: 10px;

    > .save-btn {
        color: #fff;
        font-weight: bold;
          background-color: #ee9e06;
    }
}

/frontend/src/components/SideBar.vue

저장 버튼 추가!

 

우리가 저장해야할 항목은 맛집 이름, 위치정보, 별점, 후기가 있다. (추후 이미지 추가)
inputv-model 을 달아주자
나는 각각 title, address, grade, review로 달아줬다.

<template>
  <div class="side-bar-wrapper">
    <VueResizable
        class="resizable-side-bar"
        :width="500"
        :min-width="500"
        :max-width="Infinity"
        :active="['r']"
        v-if="isVisibleSideBar"
    >
      <div class="side-bar">
        <div class="title-area">
          <BInput v-model="title" placeholder="맛집 이름을 입력해주세요."/>
        </div>
        <div class="image-area">
          <div class="iw-file-input">
            사진을 업로드 해주세요
          </div>
        </div>
        <div class="location-info-area">
          <FontAwesomeIcon icon="location-dot" />
          <BInput
              placeholder="위치 정보 직접 입력하기"
              v-model="address"
          />
        </div>
        <div class="rate-area">
          <BFormRating v-model="grade" />
        </div>
        <div class="review-area">
          <BFormTextarea
              ref="textarea"
              placeholder="후기를 입력해주세요."
              v-model="review"
          />
        </div>
        <div class="bottom-btn-area">
          <BButton class="save-btn">
            저장
          </BButton>
        </div>
      </div>
    </VueResizable>
    <BButton
        class="side-bar-active-btn"
        size="sm"
        @click="showSideBar"
    >
      <FontAwesomeIcon :icon="isVisibleSideBar ? 'angle-left' : 'angle-right'" />
    </BButton>
  </div>
</template>

<script>
import VueResizable from 'vue-resizable';

export default {
  name: 'SideBar',
  components: {
    VueResizable
  },
  data() {
    return {
      isVisibleSideBar: true,
      title: undefined,
      address: undefined,
      grade: undefined,
      review: undefined
    }
  }
}
</script>

frontend/src/components/SideBar.vue

이제 input 에 내용을 입력하면 v-model 로 set해준 data에 담긴다.
우리는 이것을 API 에 params로 전달해 줄 것이다.

저장 버튼을 누르면 API가 실행되도록 구현해보자.
API 구현엔 Axios 를 이용할 것이다.
공식 문서에 따르면,
Axios는 브라우저, Node.js 를 위한 Promise API를 활용하는 HTTP 비동기 통신 라이브러리라고 설명되어 있다.
쉽게 말하자면 백엔드와 프론트엔드의 통신을 쉽게하는 라이브러리이다.
이미 자바스크립트에는 fetch api가 있지만 프레임워크에서 ajax를 구현할 땐 axios를 쓰는 편이라고 보면된다.

axios fetch
third party 라이브러리(제 3자가 만든 라이브러리)로, 따로 설치가 필요하다. 현대 브라우저에 빌트인이라 설치 필요 없음
XSRF(사이트 간 request 위조) 보호를 해준다. 별도 보호 없음
data 속성을 사용 body 속성을 사용
data는 object를 포함한다. body는 문자열화 되어있다.
status가 200이고 statusText가 OK이면 성공이다 응답 객체가 ok 속성을 포함하면 성공이다
자동으로 JSON데이터 형식으로 변환된다. .json() 메소드를 사용해야한다.
요청을 취소할 수 있고 타임아웃을 걸 수 있다. 지원 X
HTTP 요청을 가로챌 수 있음 지원 X
download 진행에 대해 기본적인 지원을 함 지원하지 않음
좀더 많은 브라우저에 지원됨 한정적인 브라우저에 지원됨

여러모로 Axios가 fetch보다 더 좋은 것 같다...

나는 npm으로 axios 를 설치해줬다.

npm install axios

설치 후 packagke.json에 axios 가 설치된 걸 확인할 수 있었다.

  "dependencies": {
    // ...
    "axios": "^0.26.1",
    // ...
  },

우리가 params에 담을 데이터들(맛집 제목, 리뷰 등)은 대부분 문자열이라 HTTP의 GET 메소드를 써도 될 것 같지만,
숫자형인 별점과 엄청난 맛집이어서 후기가 아주 길어질 경우를 생각해 POST 메소드를 활용할 것이다.

<template>
    // ...
    <BButton
        class="save-btn"
        @click="saveReview"
    >
        저장
    </BButton>
    // ...
</template>

<script>
import axios from 'axios';

methods: {
// ...
    saveReview() {
        axios.post('/api/review/saveReview', {
            title: this.title,
            address: this.address,
            grade: this.grade,
            review: this.review
    }
}
// ...
</script>

/frontend/src/components/SideBar.vue

아까 만들어줬던 저장 버튼에 saveReview 라는 method를 만들어줬다.
API의 url은 /api/review/saveReview 로 지정해줬고,
params 데이터로는 title, address, grade, review를 넣어줬다.

🍜 리뷰 저장(추가) API 구현

/api/review/saveReview라고 url을 정해줬으니 이에 대한 API를 구현해보자.

🍥 DTO 구현

먼저, DTO를 만들 것이다.
DTO는 Data Transfer Object의 약자로, 계층 간 데이터 교환을 하기 위해 사용하는 객체이다.

💡 참고
DTO vs VO

VO는 Value Object의 약자로, DTO처럼 그냥 객체를 의미한다.
DTO와 VO 둘 다 단순 객체라는 점은 같지만,VO는 read-only이고 DTO는 setter를 가지고 있어 값이 변할 수 있다.

 

/src/main/java/패키지/dto 경로에 ReviewDTO.java를 생성한다.
패키지 명은 프로젝트에 맞춰 자유롭게 하면된다.
나는 com.map.restaurant.good 라고 해줬다. (정확히는 dto까지 패키지임)
(패키지는 보통 도메인을 거꾸로 뒤집은 형태로 짓는다고 한다.)

package com.map.restaurant.good.dto;

public class ReviewDTO {
    String id;
    String title;
    String address;
    Integer grade;
    String review;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Integer getGrade() {
        return grade;
    }

    public void setGrade(Integer grade) {
        this.grade = grade;
    }

    public String getReview() {
        return review;
    }

    public void setReview(String review) {
        this.review = review;
    }
}

/src/main/java/com/map/restaurant/good/dto/ReviewDTO.java

📌 Intellij 꿀팁 (Windows 기준)

String title;
String address;
Integer grade;
String review;

class 안에 위 코드 처럼 변수만 선언해주고 alt + Insert 를 누르면 getter, setter를 자동 생성할 수 있다!
(MacOS에선 코맨드키 + N 라고 하는데, 안해봐서 되는지는 모르겠다.)

전체 선택 후 OK!

🍥 테이블 생성

DTO에 맞춰 넘겨받을 파라미터들을 저장할 테이블을 아래 SQL로 생성해줬다.

create table tbl_review (
    id varchar(36) not null primary key,  
    title varchar(20) not null,
    review varchar(500) not null,
    address varchar(50) not null,
    grade int
);

id 로는 정수형이 아닌 UUID를 사용할 것이다.

UUID는 128비트의 숫자이며, 32자리의 16진수로 표현된다.
여기에 8-4-4-4-12 글자마다 하이픈을 집어넣어 5개의 그룹으로 구분한다.

예)

550e8400-e29b-41d4-a716-446655440000

즉, 나는 id에 32자리의 16진수와 4개의 하이픈이 더해진 varchar(36) 을 할당해줬다.

📌 int형 대신 UUID를 사용하는 이유

  • 현재 근무 중인 직장에서 대부분의 프로젝트에 UUID를 사용하고 있기 때문에 내게 친근하다 ㅎㅎ
  • UUID는 데이터에 대한 정보를 노출하지 않기 때문에 보안상 안전하다.
  • int 값은 그 테이블에서만 고유하지만, UUID는 여러 데이터베이스에서도 고유한 값이다.
  • int 값은 insert를 하기 전 데이터베이스를 조회해 전 pk값을 알아와야하지만, uuid는 stateless하기 때문에 함수를 사용하여 키를 생성할 수 있어 그럴 필요 없다.

UUID의 단점으로는 int 값보다 더 많은 양의 저장 장소가 필요하다는 것인데,
이 프로젝트에선 딱히 저장 장소에 대해 신경 쓰지 않아도 될 것 같아 UUID를 사용하게 됐다.

🍥 DAO 구현

DAO 는 Data Access Object의 약자로, DB에 접근하기 위한 객체(인터페이스)이다.
(추상적인 이 인터페이스의 알맹이는 Mybatis로 선언된 sql라고 생각하면 된다.)

package com.map.restaurant.good.dao;

import com.map.restaurant.good.dto.ReviewDTO;

public interface ReviewDAO {
    void saveReview(ReviewDTO reviewDTO);
}

src/main/java/com/map/restaurant/good/dao/ReviewDAO.java

저장만 할 것이기 때문에 아무것도 반환하지 않는(void) saveReview 메소드를 선언해준다.
요청으로 받아온 데이터를 DB에 넘겨야하기 때문에 매개변수엔 DTO를 넣어준다.

🍥 Mapper 구현

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.map.restaurant.good.dao.ReviewDAO">
    <insert id="saveReview">
        insert into tbl_review (id, title, address, review, grade)
        values (#{id}, #{title}, #{address}, #{review}, #{grade})
    </insert>
</mapper>

🍥 Controller 구현

Controller 는 MVC(Model View Controller)패턴에서의 그 C가 맞다!
Controller 는 사용자가 요청이 진입하는 지점이며, 요청에 따라 어떤 처리를 할지 결정해준다.
우리의 경우, controller에 /api/review/saveReview 요청에 대한 기능이 구현되면 된다.

@RestController
@RequestMapping("/api/review")
public class ReviewCtrl {
    @Autowired
    private ReviewDAO reviewDAO;

    @PostMapping("/saveReview")
    public void saveReview(@RequestBody ReviewDTO reviewDTO) {
        String id = UUID.randomUUID().toString();
        reviewDTO.setId(id);
        reviewDAO.saveReview(reviewDTO);
    }
}

src/main/java/com/map/restaurant/good/ReviewCtrl.java

파라미터로 ReviewDTO를 받고 saveReview 기능이 동작하는 API를 짜줬다.

자, 이제 리뷰가 저장이 되는 지 확인해보자!
최근에 먹은 맛집에 대한 리뷰를 작성해보았다.

확신의 등촌샤브칼국수
tbl_review

DB에 정상적으로 저장되는 걸 확인할 수 있었다!🎉

🍜 Spinner 생성

API 동작 시 주의해야할 점은, 유저가 API가 돌고 있을 동안 다른 동작을 하지 못하도록 막는 것이다.
뭔가 저장, 수정, 삭제하는 API가 돌고 있을 때 유저가 같은 데이터에 대한 다른 API 기능을 해버렸을 때,
혼란을 야기할 수 있다.

때문에 Spinner를 추가해주기로 했다.
이 역시 Bootstrap을 활용한다.

frontend/src/components/ 경로에 ProgressSpinner.vue를 생성하고 아래와 같이 입력한다.

<template functional>
    <div class="progress-spinner">
        <BSpinner class="progress-spinner-icon" />
    </div>
</template>

<script>
export default {
    name: 'ProgressSpinner',
    functional: true
}
</script>

<style lang="scss" scoped>
.progress-spinner {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    cursor: progress;
    z-index: 777;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: rgba(0, 0, 0, 0.3);
}
</style>

frontend/src/components/ProgressSpinner.vue

단순히 Spinner가 화면 가운데서 돌아가는 컴포넌트이다.
UI 확인을 위해 SideBar.vue 컴포넌트에 import 해와서 띄워보겠다.

<template>
    <div class="side-bar-wrapper">
    // ...
        <ProgressSpinner />
    </div>
</template>

<script>
    // ...
    import ProgressSpinner from '@/components/ProgressSpinner.vue'

    export default {
        name: 'SideBar',
        components: {
            ProgressSpinner,
            VueResizable
        },
        // ...
    }
</script>

frontend/src/components/SideBar.vue

빙글빙글 가운데서 잘 도는 것을 확인했다!🎉
z-index를 크게 잡아 SideBarr.vue 화면보다 스피너 화면이 위에 위치하고 있어 마우스 클릭도 되지 않는 걸 확인했다.

잠시 스피너 구현 코드에 대해 살펴보자.
template 부분과 data 부분에 functional이란 걸 선언해줬다.
이는 Vue2 의 함수형 컴포넌트를 의미한다.

🍥 함수형 컴포넌트

함수형 컴포넌트란,
상태(data)와 인스턴스가 존재하지 않는(this 컨텍스트가 없음) 컴포넌트를 말한다.
ProgressSpinner.vue 코드에도 data가 없고, this 컨텍스트가 사용되지 않았다.

🍥 함수형 컴포넌트는 왜 사용하는가?

일반 컴포넌트에 비해 가볍고 성능이 좋아 렌더링이 빨리된다.
딱히 메서드나 데이터가 필요 없는 컴포넌트는 함수형 컴포넌트로 선언해 성능을 높일 수 있다.
ProgressSpinner.vue 역시 메소드나 데이터가 딱히 필요없는 컴포넌트였기 때문에 함수형으로 선언해줬다.

🍥 함수형 컴포넌트 구현방법

ProgressSpinner.vue 처럼
templatefunctional을 붙이고,
functional: true 속성을 사용하면 된다.

💡참고
Vue 3의 함수형 컴포넌트는 Vue2의 선언법과 다르다.
Vue 3에선 함수형 컴포넌트를 일반 함수로만 만들 수 있다.
자세한 건 공식 문서 참고

 

이제, API가 동작할 때만 ProgreeSpinner가 돌도록 구현해보자.

<template>
    // ...
    <ProgressSpinner v-if="processingCount > 0" />
    // ...
</template>
<script>
// ...
    data() {
        return {
            // ...
            processingCount: 0
        }
    },
    methods: {
        // ...
        async saveReview () {
            this.processingCount++;
            try {
                await axios.post('/api/review/saveReview', {
                    title: this.title,
                    address: this.address,
                    grade: this.grade,
                    review: this.review
                });
                await this.$bvModal.msgBoxOk('저장 완료되었습니다.', {
                    hideHeader: true,
                    okTitle: '확인',
                    noFade: false,
                    size: 'sm',
                    buttonSize: 'sm',
                    okVariant: 'success',
                    headerClass: 'p-2 border-bottom-0',
                    footerClass: 'p-2 border-top-0',
                });
            } catch (e) {
                console.log(e.message);
                await this.$bvModal.msgBoxOk(e.message, {
                    hideHeader: true,
                    okTitle: '확인',
                    noFade: false,
                    size: 'sm',
                    buttonSize: 'sm',
                    okVariant: 'danger',
                    headerClass: 'p-2 border-bottom-0',
                    footerClass: 'p-2 border-top-0',
                });
                return await Promise.reject(e);
            } finally {
                this.processingCount--;
            }
        }
    }
</script>

frontend/src/components/SideBar.vue

data()processingCount를 선언하고, api 가 돌기 전에 수를 증가시키고, 완료되고 난 후엔 감소시켰다.
그리고, v-ifprocessingCount 가 0이상일 떄 ProgreeeSpinner가 화면에 보이게 하여
API가 동작할 떄만 스피너가 돌도록 했다.

추가로, API가 완료되었을 때 저장이 완료되었다는 Bootstrap 확인 팝업과 에러 상황일 때 에러를 UI에서 인지할 수 있도록 에러 팝업을 추가해줬다.

적절하게 스피너와 팝업이 뜨는 것을 확인할 수 있었다! 🎉

🍜 리뷰 가져오기 API 구현

저장 API 와 같은 방법으로 리뷰를 가져오는 API를 구현볼 것이다.
아직 가져온 데이터를 어떤식으로 UI 에 출력해야할지 몰라서, 단순히 API 만 구현해본다.

🍥 Controller

@RestController
@RequestMapping("/api/review")
public class ReviewCtrl {

    // ...

    @GetMapping("/getReviews")
    public List<ReviewDTO> getReviews() { return reviewDAO.getReviews(); }
}

src/main/java/com/map/restaurant/good/ReviewCtrl.java

🍥 DAO

public interface ReviewDAO {
    // ...
    List<ReviewDTO> getReviews();
}

src/main/java/com/map/restaurant/good/dao/ReviewDAO.java

🍥 Mapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.map.restaurant.good.dao.ReviewDAO">

    <!-- ... -->

    <select id="getReviews" resultType="hashMap">
        select
            *
        from
             tbl_review
    </select>
</mapper>

src/main/resources/mappers/ReviewMapper.xml

간단히 설명하자면 tbl_review 테이블 내 모든 데이터를 가져오게하는 것이다.
resultTypehasMap으로 설정하면 list에 여러 hashMap이 담겨 오게 된다.
GET Method로 가져오게 했다.

UI가 구현되어 있지 않아 PostMan으로 확인해봤다.

/api/review/getReviews

모든 리뷰를 잘 가져오는 걸 확인할 수 있었다! 🎉

🍜 리뷰 수정 API 구현

리뷰 추가 시에 addReviewinsertReview 라고 메소드를 짓지 않고,
saveReview로 지어준 이유는 리뷰 수정을 위해서 였다.

나는 따로 리뷰수정 API를 구현하지 않고,
saveReview API를 수정해서 리뷰 수정을 할 수 있도록 해줬다.
(이유는 on duplicate key를 써보고 싶었고, 수정 API를 구현하기 귀찮았기 때문이다ㅎㅎ;)

🍥 Controller

    @PostMapping("/saveReview")
    public void saveReview(@RequestBody ReviewDTO reviewDTO) {
        String id = reviewDTO.getId();
        if (id == null) {
            String uuidStr = UUID.randomUUID().toString();
            reviewDTO.setId(uuidStr);
        }
        reviewDAO.saveReview(reviewDTO);
    }

src/main/java/com/map/restaurant/good/ReviewCtrl.java

saveReview를 위와 같이 수정했다.
넘겨 받은 값에 id가 없으면 생성해주고, id가 있으면 그대로 DAO로 넘긴다.

🍥 Mapper

    <insert id="saveReview">
        insert into tbl_review (id, title, address, review, grade)
        values (#{id}, #{title}, #{address}, #{review}, #{grade})
        on duplicate key
        update
            title = #{title},
            address = #{address},
            review = #{review},
            grade = #{grade}
    </insert>

src/main/resources/mappers/ReviewMapper.xml

saveReview Mapper 역시 수정한다.
중복되는 key가 존재하면, 해당 키의 row 값을 담아온 값으로 수정한다.

수정 기능 역시 UI가 구현되어 있지 않아 Postman으로 확인해보았다.
동일한 Id에 값만 다르게 해서 돌려보았다.

수정 전 tbl_review
DB에 저장된 id 값과 함께 /api/review/saveReview
수정 후 tbl_review

값이 추가되거나 하지 않고 올바르게 수정된 걸 확인할 수 있었다!🎉

🍜 리뷰 삭제 API 구현

🍥 Controller

    @DeleteMapping("/deleteReview")
    public void deleteReview(@RequestParam(value = "id", required = true) UUID id) {
        reviewDAO.deleteReview(id);
    }

src/main/java/com/map/restaurant/good/ReviewCtrl.java

HTTP DELETE METHOD(@DeleteMapping)으로 구현해줬다.
id값을 받고 넘기면 삭제 기능을 하도록 했다.

잠깐 TMI 를 풀어보자면,
내가 일하고 있는 팀에선 HTTP DELETE METHOD를 사용하지 않는다.
팀장님의 지침 때문이다.

왜 사용하지 않냐고 여쭤봤었는데, 두 번 생각하는게 불편하다고 답변해주셨다.
GETPOST 만으로도 충분히 원하는 로직을 구현할 수 있고,
API 명으로도 충분히 어떤 기능을 할 수 있는지 나타낼 수 있기 떄문에 굳이 쓰지 않고 싶다고 하셨다.

스택 오버 플로우에 관련된 토론장이 펼쳐져 흥미롭게 읽었다.
팀장님처럼 요즘 DELETEPUT이 유용하지 않다는 사람도 있었고,
RESTFUL 서비스를 위해선 DELETEPUT을 사용하는 게 좋다고 하는 사람도 있다.

팀 영향으로 내 API 코드들이 완전히 RESTFUL 하진 않은 같지만,
자주 써보지 않은 걸 구현해보고 싶어서 나는 DELETE를 쓰기로 했다 ㅎㅎ

🍥 DAO

public interface ReviewDAO {
    // ...
    void deleteReview(@Param("id") String id);
}

src/main/java/com/map/restaurant/good/dao/ReviewDAO.java

🍥 Mapper

    <delete id="deleteReview">
        delete
        from tbl_review
        where id = #{id}
    </delete>

src/main/resources/mappers/ReviewMapper.xml

이 역시 PostMan으로 확인해봤더니,
id 값에 해당하는 데이터가 삭제된 것을 확인할 수 있었다!


이번 포스팅에선 CRUD API를 구현해보았다.

다음 포스팅에선 불러온 값들을 UI에 그려보도록하겠다!

댓글, 하트, 피드백은 언제나 환영입니다🥰

Seize the day!

Spring MVC | Spring Boot | Spring Security | Mysql | Oracle | PostgreSQL | Vue.js | Nuxt.js | React.js | TypeScript | JSP | Frontend | Backend | Full Stack | 자기계발 | 미라클 모닝 | 일상