블랙커피 스터디1 문벅스 step3 서버와의 통신해 메뉴 관리하기 회고

짱유경·2021년 11월 7일
1

블랙커피 스터디

목록 보기
3/4
post-thumbnail

스터디 끝나고 쓰는 미션회고..
제출한 미션은 여기에서 확인할 수 있다.

🎯 step3 요구사항 - 서버와의 통신을 통해 메뉴 관리하기

  • 링크에 있는 웹 서버 저장소를 clone하여 로컬에서 웹 서버를 실행시킨다.
  • 웹 서버를 띄워서 실제 서버에 데이터의 변경을 저장하는 형태로 리팩터링한다.
    • localStorage에 저장하는 로직은 지운다.
    • fetch 비동기 api를 사용하는 부분을 async await을 사용하여 구현한다.
    • API 통신이 실패하는 경우에 대해 사용자가 알 수 있게 alert으로 예외처리를 진행한다.
  • 중복되는 메뉴는 추가할 수 없다.

이번 미션은 강의를 참고하지 않고 온전히 내가 구현해냈다. 😜
대신 전체적으로 기능은 잘 동작했는데 한가지 문제점을 고치지 못하고 미션을 제출했었다.

(1) 품절버튼을 처음 클릭하면 아무런 반응이 없고 다시 클릭해야 품절선이 표시됐다.
(2) 새로고침하면 품절 반응이 리셋됐었다. 

🏁 class 코드로 변경하기

전에 코드는 아래와 같은 방식으로 함수형으로 관리했는데, class형이 보기도 좀더 깔끔하고 만약 상속을 사용할 일이 있으면 class형이 좀 더 사용하기 쉬울 것 같아서 class 코드로 변경했다.

// 기존 코드
function App() {
    this.menu = [];
    const addMenu = () => {};
    const deleteMenu = () => {};
}
// 새로운 코드
class App {
  constructor() {
    this.currentCategory = "espresso";
    this.menuListCount = 0;

    this.$menuCount = $(".menu-count");
    this.$menuList = $("#menu-list");
    this.$menuNameInput = $("#menu-name");
    this.$menuForm = $("#menu-form");
    this.$nav = $("nav");
    this.$categoryTitle = $("#category-title");

    this.setEvent();
    this.render();
 }
  
 setEvent() {
   // code ...
 }
  
  render() {
    // code..
  }
  
  // code...
}

상태(현재 메뉴 카테고리, 현재 등록된 메뉴 갯수)와 자주 사용되는 DOM 태그들은 constructor안에서 관리해줬다. 처음에 총 메뉴의 갯수는 어떻게 관리해야될까 고민이었는데
(1). 그동안 사용해왔던 childeElementCount방식 사용하기 - 이 방식은 자꾸 값이 이상하게 출력됐었다 ...
(2). countMenu()함수에서 배열의 길이를 카운팅하기 - 파라미터로 배열을 넘겨주고 배열의 값을 카운팅하려 했는데 자꾸 뭔가 작동이 안돼서22.. 일단 등록된 메뉴를 가져오는 함수에서 카운팅해 전역적으로 관리해줬다. 그런데 실제로 이 값을 사용하는 곳은 countMenu() 한곳밖에 없는데 전역적으로 값을 관리하는게 뭔가 별로여서 그닥 마음에 들진 않았다.

🌌 api 통신하기

😎 fetch 함수 이용하기

fetch함수는 데이터를 한번 response.JSON()과 같은 방식으로 변경을 해줘야 하기도 하고, get/post/put/delete와 같이 REST API 메서드를 모두 사용하는 상황에서 각 호출마다 fetch 함수에서 config를 직접 수정하는건 비효울적일것 같아서 api.js라는 파일을 추가해서 관리하기로 했다.

// api.js
const SERVER_URL = "http://localhost:3001";

const api = {
  get: async (url) => {
    try {
      const res = await fetch(`${SERVER_URL}/api/category${url ? url : ""}`);

      if (!res) {
        throw new Error("서버 에러");
      }

      return await res.json();
    } catch (e) {
      throw new Error(e);
    }
  },
  post: async (url, data) => {
    try {
      const res = await fetch(`${SERVER_URL}/api/category${url ? url : ""}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json;charset=utf-8",
        },
        body: JSON.stringify(data),
      });

      return await res.json();
    } catch (e) {
      return e;
    }
  },
  put: async (url, data) => {
    // code..
  },
  delete: async (url) => {
    // code..
};

export default api;

이런식으로 객체 안에 각 메서드를 만들어주고, 사용할땐 api.get("/menu") api.post("/menu", { name: "카페라떼" });와 같은 방식으로 사용해주면 됐다.
그런다음 각 api를 호출하는 파일을 다시한번 만들어줬다.

// menuApi.js

import api from "./api.js";

const menuApi = {
  addMenu: async (category, $target) => {
    try {
      const res = await api.post(`/${category}/menu`, {
        name: $target.value,
      });

      if (res.message) {
        alert(res.message);
      }
      
      return res;
    } catch (e) {
      alert(e);
    }
  },
  getMenu: async (category) => {
    try {
      const res = await api.get(`/${category}/menu`);
      return res;
    } catch (e) {
      alert(e.message);
      return false;
    }
  },
  soldOutMenu: async (category, menuId) => {
    // code ...
  },
  editMenu: async (category, menuId, result) => {
    // code ...
  },
  deleteMenu: async (category, menuId) => {
    // code ...
};

export default menuApi;

사용할땐 아래와 같은 방식으로 사용해서, 각 메서드마다 try/catch와 같은 예외처리 구문으로 복잡해질 필요가 없어서 훨씬 더 보기 깔끔해졌다. 근데 직접적으로 target을 넘겨주는게 아니라 바로 target.value를 넘겨주는 방식이 조금 더 좋지 않았나 싶었다.

  async addMenu() {
    if (!this.$menuNameInput.value) {
      alert("값을 입력하세요.");
      return;
    }
    await menuApi.addMenu(this.currentCategory, this.$menuNameInput);
    this.render();
}

작성하면서 아쉬웠던게 에러 핸들링 부분..! 미션을 제출하고 나서야 알았던건데 catch단에서 throw new Error(e)를 했던게 미스였던 것 같고, catch단에서 error 메세지를 알려준게 아니라 try단에서 error message를 검증해주는게 별로였던 것 같다. ㅜㅜ

🤩 render()

 async render() {
    const data = await menuApi.getMenu(this.currentCategory);
    this.menuListCount = data.length;
    this.countMenu();

    const template = data
      ? data
          .map((item) => {
            return `
          <li data-menu-id="${item.id}" class="menu-list-item d-flex items-center py-2">
            <span class="w-100 pl-2 menu-name">
              ${item.name}
            </span>
            <button
              type="button"
              class="bg-gray-50 text-gray-500 text-sm mr-1 menu-sold-out-button"
            >
              품절
            </button>
            <button
              type="button"
              class="bg-gray-50 text-gray-500 text-sm mr-1 menu-edit-button"
            >
              수정
            </button>
            <button
              type="button"
              class="bg-gray-50 text-gray-500 text-sm menu-remove-button"
            >
              삭제
            </button>
          </li>
        `;
          })
          .join("")
      : "<h2>데이터를 불러오는데 실패했습니다.</h2>";

    this.$menuList.innerHTML = template;
 }

확실히 step1과 비교해서 직접적으로 DOM을 건드리는 부분이 줄어든 것 같다. step1/step2까지만 해도 메뉴의 등록, 삭제, 품절, 업데이트마다 직접 DOM을 조작했는데 이번 미션에서는 DOM의 변경이 있을때마다 this.render()메서드를 호출해줬다. 한가지 고민됐던건 과연 이런 방식이 성능에 문제가 없을까? 이 부분이였다. 그래서 React와 같이 상태를 기준으로, DOM을 직접 건들이지 않는 프레임워크에서 가상 DOM을 이용하는구나 싶었다. 상태가 변화될때마다 모든 DOM을 새로 그리지 않고 변경된 부분만 새로 리렌더링 한다는게 얼마나 혁신적인(!) 부분이었는지 새삼 느끼게 되었던 것 같다.

💠 step3 미션 후기

조금 더 "상태관리" 라는 키워드를 이해하게 된 것 같다.
데이터라는 상태를 api 호출로 상태를 관리하고, 그런 유동적인 상태를 서버라는 중앙 저장소(store)에서 관리하니까 확실히 상태를 관리하기 쉬워졌다. 만약 코드를 짜다가 문제가 생기면 어느 곳에서 문제가 있는지 기준을 찾고 관리하기 쉬워졌고, 전 미션에서는 직접 상태에다가 push나 slice로 불변성을 신경쓰지 않은 반면 이번에는 서버에다가 데이터를 넣어주면 관리해줘 내가 꺼내 쓰기만 하면 되니 직접적으로 상태를 건드리지 않고 전의 값을 복사해서 새로운 값을 덮어씌우는 방식이 왜 선호되는지 알게 되었다.

아쉬웠던 점은 에러 처리에 대한 부분..! try/catch에 대해 이해하고 있었다고 생각했는데 아니였던 것 같다. 특히 지금 방식에서는 api함수를 호출하는게 겹겹이 레이어가 씌워져 있는 방식이라 더더욱 헷갈렸다. 크게는 서버단의 상태가 이상하거나, 서버단에서는 아무런 문제가 없지만 프론트단에서 넘기는 데이터가 이상할때 생기는 에러 이런부분을 좀 더 구별해서 처리하고 싶었는데 그게 잘 안됐던 것 같다.

0개의 댓글