[리팩토링] Next.js 쇼핑몰 프로젝트 - 데이터 초기화 컴포넌트

YouGyoung·2024년 3월 29일
0

기존 코드

DataInitializer로 이름 붙여진 데이터 초기화 컴포넌트는 말 그대로 데이터를 초기화해 주는 역할을 하는 컴포넌트입니다.

루트 레이아웃에서 children을 감싸고 있는 구조인데, 부모 컴포넌트인 AuthProvider에서 세션 정보를 받은 다음 장바구니와 위시리스트 상태를 관리하는 스토어의 데이터를 로컬 스토리지에서 채울지, FireStore에서 채울지를 정합니다.

세션이 있는 경우 FireStore에서 데이터를 가져와 채우고, 세션이 없는 경우 로컬 스토리지에서 데이터를 가져와 채웁니다.

주석 처리가 되어 있는 부분은 다른 컴포넌트의 리팩토링 도중 배포 오류가 발생한 부분입니다.

여기에서 로딩을 처리하고 있는 부분이 있는데요.
지금은 API를 통해 상품 리스트를 모두 가져온 후에만 렌더가 되도록 했습니다만,
생각 이상으로 로딩 화면이 길게 표시되는 문제가 있어 이 부분은 제거하려고 합니다.

src/app/DataInitializer.tsx

'use client';
import React, { useContext, useEffect, useState } from 'react';
import { getProductList } from './api/product';
import { CartItems, Product } from '@/types/globalTypes';
import { AppDispatch } from '@/types/reduxTypes';
import { useAppDispatch } from '@/hooks/useAppDispatch';

export default function DataInitializer({
  children,
}: {
  children: React.ReactNode;
}) {
  const dispatch: AppDispatch = useAppDispatch();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const productList: Product[] = await getProductList();
        //dispatch(setProductList(productList));
      } catch (error) {
        console.error('상품 리스트를 가져오는 것을 실패했습니다.:', error);
      }

      // try {
      //   if (!currentUser) {
      //     const cartItems: CartItems = getCartItemsLocalStorage();
      //     dispatch(setCartItems(cartItems));
      //   }
      // } catch (error) {
      //   console.error(
      //     '로컬 스토리지에서 장바구니 아이템을 가져오는 작업을 실패했습니다.:',
      //     error
      //   );
      // }
      setLoading(false);
    };

    fetchData();
  }, [dispatch]);

  if (loading) {
    return (
      <div className="flex items-center justify-center h-full">
        <span className="loading loading-ring loading-lg"></span>
      </div>
    );
  }

  return <>{children}</>;
}

리팩토링 후 코드

DataInitializer 컴포넌트에서 모듈로 분리되어, 다음과 같은 파일 구조로 변경 되었습니다.

src/
├── _utils/
│   ├── getAllProductsFakeStore.ts          # 상품 목록을 불러오는 유틸리티 함수
│   ├── getCartItemsLocalStorage.ts         # 로컬 스토리지에서 장바구니 아이템을 불러오는 함수
│   ├── getUserCartItems.ts                 # 사용자의 장바구니 아이템을 불러오는 함수
│   └── getUserWishlistItems.ts             # 사용자의 위시리스트 아이템을 불러오는 함수
├── app/
│   ├── DataInitializer.tsx                 # 초기 데이터를 설정하는 컴포넌트
│   └── components/
│       └── LoadingSpinner.tsx              # 로딩 스피너 컴포넌트
└── hooks/
    ├── useProduct.ts                       # 상품 관련 상태를 관리하는 커스텀 훅
    └── useStore.ts                         # 스토어 관련 상태를 관리하는 커스텀 훅

getUserCartItems.ts, getUserWishlistItems.ts은 회원가입 리팩토링할 때 만든 모듈인데 여기서도 사용되니, 잘 만들어 놓은 모듈은 재사용성이 높다는 게 몸소 느껴지네요.

최근에는 변수명이나 파일명을 지을 때, 줄임말 대신 적당한 길이 내에서 자세한 표현을 사용하려고 합니다. 줄임말로 해놓으면 유지보수를 하기 힘들다는 걸 알았거든요..

회원가입 기능을 리팩토링하면서부터 캡슐화를 최대한 고려하고 있습니다.
로직 코드를 뷰 구성 부분에서 최대한 제거하는 게 가독성이 확실히 좋더라구요.

로딩 렌더링 코드는 여기에서 뿐만 아니라 다른 곳에서도 사용이 되는 중이라 LoadingSpinner 컴포넌트로 생성해서 import해 오기로 했습니다.

src/app/DataInitializer.tsx

'use client';
import useProduct from '@/hooks/useProduct';
import useStore from '@/hooks/useStore';
import { useSession } from 'next-auth/react';
import React, { useEffect } from 'react';
import LoadingSpinner from './components/LoadingSpinner';

interface DataInitializerProps {
  children: React.ReactNode;
}

const DataInitializer: React.FC<DataInitializerProps> = ({ children }) => {
  const { data: session, status } = useSession();
  const { initializeUserStore, initializeStore } = useStore();
  const { setProductsStore } = useProduct();
  useEffect(() => {
    if (status === 'authenticated') {
      initializeUserStore(session.user.email);
    } else {
      initializeStore();
    }
  }, [status]);

  useEffect(() => {
    setProductsStore();
  }, []);

  return status === 'loading' ? <LoadingSpinner /> : <>{children}</>;
};

export default DataInitializer;

src/hooks/useStore.ts

import { resetCartItems, setCartItems } from '@/slices/cartSlice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { CartItems, WishlistItems } from '@/types/globalTypes';
import { resetWishlistItems, setWishlistItems } from '@/slices/wishListSlice';
import getUserCartItems from '@/_utils/getUserCartItems';
import getUserWishlistItems from '@/_utils/getUserWishlistItems';
import getCartItemsLocalStorage from '@/_utils/getCartItemsLocalStorage';

const useStore = () => {
  const dispatch = useAppDispatch();

  const resetStore = () => {
    dispatch(resetCartItems());
    dispatch(resetWishlistItems());
  };

  const initializeUserStore = async (email: string) => {
    const cartItems: CartItems = await getUserCartItems(email);
    const wishlistItems: WishlistItems = await getUserWishlistItems(email);
    dispatch(setCartItems(cartItems));
    dispatch(setWishlistItems(wishlistItems));
  };

  const initializeStore = async () => {
    const cartItems: CartItems = getCartItemsLocalStorage();
    dispatch(setCartItems(cartItems));
  };

  return { resetStore, initializeUserStore, initializeStore };
};

export default useStore;

src/_utils/getCartItemsLocalStorage.ts

import { CARTITEMS_KEY } from '@/constants/localStorageKeys';
import { CartItems } from '@/types/globalTypes';

const getCartItemsLocalStorage = (): CartItems => {
  const jsonData = localStorage.getItem(CARTITEMS_KEY);
  if (!jsonData) return {};

  try {
    const cartItems: CartItems = JSON.parse(jsonData);
    return cartItems;
  } catch (error) {
    console.error('Parsing error in getCartItemsLocalStorage:', error);
    return {};
  }
};

export default getCartItemsLocalStorage;

src/hooks/useProduct.ts

import getAllProductsFakeStore from '@/_utils/getAllProductsFakeStore';
import { useAppDispatch } from './useAppDispatch';
import { Product } from '@/types/globalTypes';
import { setProducts } from '@/slices/productSlice';

const useProduct = () => {
  const dispatch = useAppDispatch();
  const setProductsStore = async () => {
    const products: Product[] = await getAllProductsFakeStore();
    dispatch(setProducts(products));
  };
  return { setProductsStore };
};

export default useProduct;

src/_utils/getAllProductsFakeStore.ts

import { Product } from '@/types/globalTypes';

export const getAllProductsFakeStore = async (): Promise<Product[]> => {
  try {
    const res = await fetch('https://fakestoreapi.com/products');

    if (!res.ok) {
      throw new Error(
        `FakeStoreAPI Server responded with ${res.status}: ${res.statusText}`
      );
    }

    const products = await res.json();
    return products;
  } catch (error) {
    console.error('Failed to fetch product list:', error);
    return [];
  }
};

export default getAllProductsFakeStore;

src/app/components/LoadingSpinner.tsx

const LoadingSpinner: React.FC = () => (
  <div className="flex items-center justify-center h-full">
    <span className="loading loading-ring loading-lg"></span>
  </div>
);

export default LoadingSpinner;

마치며

큰일 났습니다. 생각보다 리팩토링이 재밌습니다🤭

리팩토링을 하면 할수록 깔끔해 지는 코드를 보는 게 즐거운 거 같아요.
오늘은 일찍 자려고 했는데, 지저분한 코드들이 자꾸 생각나서 결국 리팩토링 한 개를 또 하고 자러 가게 되었습니다.

다음 주부터는 개인 프로젝트를 한 개 더, 기획부터 준비해야 하니 주말 동안 최대한 끝내고 싶네요 🥹

profile
프론트엔드 개발자

0개의 댓글

관련 채용 정보