LIGHTLOG
article thumbnail
1. 미션 소개
2. 미션에서 기억에 남는점
  2-1. 백엔드와의 협업 경험
  2-2. msw
  2-3. 나의 api 변천사
3. 리뷰리뷰스터디

 


1. 미션 소개

🛒 장바구니 배포 페이지 🛒

이번 미션은 장바구니 기능이 중점적인 간단한 커머스 사이트를 구현하는 것이었다.

 

ver. Mobile

 

ver. Web

 


2. 기억에 남는 점 

 

2-1. 백엔드와의 첫 협업,, 🎤

📌 API 명세서

백엔드에서 일방적으로 명세를 보내주지 않고, 만나서 명세를 어떻게 주고받으면 좋을지 얘기해서 굉장히 유익했다. 또한, 자잘한 요청들을 쉽게 할 수 있었고 백엔드에 대한 이해도를 높일 수 있었다. 만나서 소통하니까 일하는 느낌을 받아서, 함께 만들어나간다는 느낌도 강하게 받을 수 있었다.

 

초반에 서로 얼굴을 보지 못한 채로 테스트 할 때, CORS를 포함한 갖가지 에러들이 백엔드 문제인지 프론트 문제인지 몰라서 답답했다. 그래서, 레벨3에서는 최대한 많이 만나고 소통을 해야겠다고 다짐했다.

 

2-2. MSW 😇  

이번 미션에서 가장 아쉬움이 남는 부분이다.

 

백엔드의 구현이 완료되기까지 그냥 리팩토링이나 진행하자며 msw를 구현하지 않았는데, 방학이 되자 백엔드 서버가 죽고 미션 웹을 들어가볼 수 없다는 것은 꽤나 슬펐다. 테스트를 하지 못하니 리팩토링 또한 진행할 수가 없었었다. (결국, 뒤늦게 나마 방학때 msw를 구현하게 되었다... 소잃고 외양간 고치기...)

 

백엔드가 완료되고 프론트가 시작되는 이상적인 스케쥴링이 지켜지 않을 가능성이 현업에서도 높을 것이라 생각한다. 이번 미션에서는 너무 백엔드에게 의존적으로 있었다. 앞으로는 msw를 무조건 구현하도록 해야겠다.

 

msw는 선택이 아닌 필수!!

 

2-3. 나의 api 코드 변천사 ⭐️  

장바구니 미션을 오랫동안 진행하면서 다양한 api 요청들을 어떻게 하면 잘 관리할 수 있을지에 대해 지속적으로 고민해왔다. 초반 프로젝트 규모가 정말 작을때에는 api 요청이 하나씩 늘어갈수록 그냥 함수를 하나씩 늘려가기만 했었다. 그러다보니 반복되는 로직이 자연스럽게 생겼고, 이와 관련해서 추상화를 해보고 싶었는데 시간이 부족했다. "로직을 짜면서 이게 최선인가?"라는 생각이 계속해서 들었다. 

 

그래서 방학때 api 관련 리팩토링을 주로 진행했었다. 그 초,중,후반의 변천사는 이러하다.

 

1) 초반: 무지성으로 필요한 api 요청 함수 다 만들자.

import {
  DEFAULT_VALUE_SERVER_OWNER,
  KEY_LOCALSTORAGE_SERVER_OWNER,
  SERVERS,
} from "../constants";
import { getLocalStorage } from "../utils";

// Base64로 인코딩
const base64 = btoa(
  process.env.REACT_APP_USERNAME + ":" + process.env.REACT_APP_PASSWORD
);

export const fetchProducts = () =>
  fetch(
    `${
      SERVERS[
        getLocalStorage(
          KEY_LOCALSTORAGE_SERVER_OWNER,
          DEFAULT_VALUE_SERVER_OWNER
        )
      ]
    }/products`
  );

export const fetchCartItems = async () =>
  fetch(
    `${
      SERVERS[
        getLocalStorage(
          KEY_LOCALSTORAGE_SERVER_OWNER,
          DEFAULT_VALUE_SERVER_OWNER
        )
      ]
    }/cart-items`,
    {
      headers: {
        Authorization: `Basic ${base64}`,
      },
    }
  );

export const changeQuantity = async (cartItemId: number, newQuantity: number) =>
  fetch(
    `${
      SERVERS[
        getLocalStorage(
          KEY_LOCALSTORAGE_SERVER_OWNER,
          DEFAULT_VALUE_SERVER_OWNER
        )
      ]
    }/cart-items/${cartItemId}`,
    {
      headers: {
        Authorization: `Basic ${base64}`,
        "Content-Type": "application/json",
      },
      method: "PATCH",
      body: JSON.stringify({ quantity: newQuantity }),
    }
  );

export const addCartItem = async (productId: number) =>
  fetch(
    `${
      SERVERS[
        getLocalStorage(
          KEY_LOCALSTORAGE_SERVER_OWNER,
          DEFAULT_VALUE_SERVER_OWNER
        )
      ]
    }/cart-items`,
    {
      headers: {
        Authorization: `Basic ${base64}`,
        "Content-Type": "application/json",
      },
      method: "POST",
      body: JSON.stringify({ productId: productId }),
    }
  );
  
  
  // ...이하 중략

 

2) 중반: 반복되는 로직을 추상화해보자.

초반의 코드에서 서버의 상태가 달라져야하고 유저에 따른 authorization에 필요한 token값이 달라져야하기 때문에 매번 localStorage를 접근해서 값을 넣어주는 로직이 맘에 들지 않았다. 또한, 함수명이 일반 도메인함수명과 크게 다르지 않아서 사용처에서 이것이 api 요청에 대한 함수인지 구분이 가지 않는 문제가 있었다. 

import { getLocalStorage } from "../utils";
import {
  DEFAULT_VALUE_LOGIN_TOKEN,
  DEFAULT_VALUE_SERVER_OWNER,
  KEY_LOCALSTORAGE_LOGIN_TOKEN,
  KEY_LOCALSTORAGE_SERVER_OWNER,
  SERVERS,
} from "../constants";

const request = async (path: string, init?: RequestInit) => {
  const baseServerUrl =
    SERVERS[
      getLocalStorage(KEY_LOCALSTORAGE_SERVER_OWNER, DEFAULT_VALUE_SERVER_OWNER)
    ];
  const response = await fetch([baseServerUrl, path].join(""), {
    ...init,
    headers: {
      Authorization: `Basic ${getLocalStorage(
        KEY_LOCALSTORAGE_LOGIN_TOKEN,
        DEFAULT_VALUE_LOGIN_TOKEN
      )}`,
      "Content-Type": "application/json",
      ...init?.headers,
    },
  });

  if (!response.ok) throw new Error(response.status.toString());
  return response;
};

export const api = {
  get: (path: string) => request(path).then((response) => response.json()),

  patch: (path: string, payload?: unknown) =>
    request(path, {
      method: "PATCH",
      body: JSON.stringify(payload),
    }),

  post: (path: string, payload?: unknown) =>
    request(path, {
      method: "POST",
      body: JSON.stringify(payload),
    }),

  delete: (path: string) =>
    request(path, {
      method: "DELETE",
    }),
};

 

 

3. 후반: 사용처에서 endpoint를 알지 못하도록 api Layer를 두자.

중반의 코드로 변경하니 반복되는 로직은 request함수로 깔끔하게 줄일 수 있었고, 사용처에서 api.delete(`/cart-items/${product.cartItemId}`) 이런식으로 api 요청 함수를 호출하며 코드 가독성 또한 높일 수 있었다. 

 

하지만, 사용처에서 api endpoint를 알게 되는 관심사의 분리 측면에서의 문제점이 존재했고, 이를 해결하기 위해 api endpoint별로 api Layer를 하나 더 두어서 관리하는 방법으로 리팩토링하게 되었다. 

 

이렇게 하면, endpoint 변경에 대해 편리하게 대응할 수 있고, 본래 사용처에서 존재하던 데이터 전처리 과정을 제거하여 관심사의 분리도 훨씬 수월해졌다. 

관련 블로그 

// apis/cart.ts
import { api } from "./api";

const ENDPOINT = "/cart-items";

export const cartApi = {
  getCartItems: () => api.get(ENDPOINT),

  addCartItem: (productId: number) =>
    api.post(ENDPOINT, { productId: productId }),

  deleteCartItem: (cartItemId: number) =>
    api.delete(`${ENDPOINT}/${cartItemId}`),

  updateQuantity: (cartItemId: number, quantity: number) =>
    api.patch(`${ENDPOINT}/${cartItemId}`, { quantity: quantity }),
};

// apis/auth.ts
import { api } from "./api";

const ENDPOINT = "/auth";

export const authApi = {
  login: () => api.post(`${ENDPOINT}/login`),
};

// apis/orders.ts
import { api } from "./api";
import { LocalProductType } from "../types/domain";

const ENDPOINT = "/orders";

export const orderApi = {
  getOrders: () => api.get(ENDPOINT),

  getOrderDetail: (orderId: string) => api.get(`${ENDPOINT}/${orderId}`),

  getMyCoupons: (cartItems: LocalProductType[]) => {
    const cartItemIdsQuery = cartItems
      .map((product) => "cartItemId=" + product.cartItemId.toString())
      .join("&");

    return api.get(`${ENDPOINT}/coupons/${cartItemIdsQuery}`);
  },

  order: (selectedProducts: LocalProductType[], couponId: number | null) => {
    const products: Omit<LocalProductType, "id">[] = selectedProducts.map(
      (product) => {
        const newProduct = structuredClone(product);
        delete newProduct.id;
        return newProduct;
      }
    );

    return api.post(ENDPOINT, {
      products,
      couponId,
    });
  },
};

 2-4 페이지 구조도


3. 리뷰리뷰스터디

1) 'forEach' 내부에서 비동기 로직을 실행하면 어떤 문제점들이 있을까?

forEach 내부에는 await 키워드가 있어도 순차적으로 처리가 안된다. 비동기 처리가 완료되는 것을 기다리지 않는다. 순차처리가 중요하다면 for..of, 병렬처리는 map/Promise.all활용하자.

await Promise.all(checkedItemIdList.map((cartId) => fetchDeleteCart(server, cartId, memberAuth)));

 

2) z-index 상수화의 기준을 잘 세워보자!

ex) 네비게이션 바, 헤더(4xx) > 콘텐츠(3xx) > 모달, 팝업(2xx) > 푸터(1xx)

 

3) default Value 활용하기

// 옵셔널 체이닝
{cartList?.length > 0 && (

const { cartList } = useCartList();

// 옵셔널 체이닝을 제거하고 default value를 활용해보면 어떨까요?
const { cartList = [] } = useCartList();

cartList.length > 0 && (

 

4) localStorage get, set 로직을 recoil effect로 처리해주는 방법

const localForageEffect = key => ({setSelf, onSet}) => {
  setSelf(localForage.getItem(key).then(savedValue =>
    savedValue != null
      ? JSON.parse(savedValue)
      : new DefaultValue() // Abort initialization if no value was stored
  ));

  // Subscribe to state changes and persist them to localForage
  onSet((newValue, _, isReset) => {
    isReset
      ? localForage.removeItem(key)
      : localForage.setItem(key, JSON.stringify(newValue));
  });
};

const currentUserIDState = atom({
  key: 'CurrentUserID',
  default: 1,
  effects: [
    localForageEffect('current_user'),
  ]
});

 

5) SVG sprite 란?

첫 화면이 로딩될때 개발자 모드에서 svg아이콘들이 주르륵 불러와지는 경험이 있으신가요?

그럴땐, svg sprite를 적용해보자!

 

6) Barrel Pattern의 문제점

React import시에 Tree-shaking에 주의해야하는 점에 대해서 알아보자!

 

7) unknown과 any의 차이

type을 정의할때 unknown과 any의 차이점은 무엇인가요?

 

 

 

 



 



 

 

 

profile

LIGHTLOG

@lightOnCoding

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!