Front-end

zustand(with TypeScript) | 사이드 프로젝트에서 사용해보기

FuterNomad 2023. 1. 17. 22:14

앞서 zustand 개념에 대해 공식 문서를 통해 살펴 보았다. → 2023.01.16 - [Front-end] - 🐻zustand 이해하기

 

그렇다면, 스터디에서 공부한 오픈소스 쇼핑몰 분석 프로젝트에서 사용한 예시를 통해 zustand를 자세히 알아보자!

 

Create a store

/store/products/long-sleeve.ts

import create from 'zustand';
import { devtools, persist } from 'zustand/middleware';

interface IProducts {
  products: PhotoType[];
  setProducts: (value: PhotoType[], title: string) => void;
  reset: () => void;
}
type PhotoType = {
  alt: string;
  avg_color: string;
  height: number;
  id: number;
  liked: boolean;
  photographer: string;
  photographer_id: number;
  photographer_url: string;
  src: PhotoSrcType;
  type: string;
  url: string;
  width: number;
};

const useLongProducts = create<IProducts>()(
  devtools(
    persist(
      (set) => ({
        products: [],
        setProducts: (productList: PhotoType[], title: string) => {
          productList.map((product) => {
            product.type = title.slice(12);
            set((state: IProducts) => ({
              products: [...state.products, product],
            }));
          });
        },
        reset: () => {
          set(() => ({ products: [] }));
        },
      }),
      { name: 'longProduct', getStorage: () => sessionStorage }
    )
  )
);

export default useLongProducts;

먼저, zustand/middleware에서 {devtools와 persist}를 import 한 것을 확인할 수 있다.

  • devtools : redux devtools
  • persist
      • persist middleware는 zustand state를 storage(localStorage, AsyncStorage, etc.) 에 저장할 수 있게 해주는 기능으로 데이터를 지속할 수 있게 해준다.
      • 사용 방법은 option을 따로 지정해 주는 것이다. (아래 코드로 확인 ↓ )
    ,{ name: 'longProduct', getStorage: () => sessionStorage }
    • name은 storage에 저장될 때 설정되는 key값으로 반드시 유일한 값이어야 한다.
    • storage를 지정하는 것은 선택사항으로 사용하고자 하는 storage를 return 하는 함수에 전달해주면 된다.
      (default는 localStorage이고, 위 예시에서 storage는 getStorage로 작성되어져 있다.)
    • storage 기능의 장점은 사용자가 새로 고침을 하더라도 data가 유지된다는 점이다.
  • data가 setProducts 함수를 통해 store에 set되는 과정에서 외부 API에서 받아온 data를 실제 사용할 때 필요한 data 형태로 가공하여 저장하는 로직을 중간에 추가할 수 있다.(아래 코드로 확인 ↓ )
setProducts: (productList: PhotoType[], title: string) => {
  productList.map((product) => {
    product.type = title.slice(12);
    set((state: IProducts) => ({
      products: [...state.products, product],
    }));
  });
},

 

Use a store

/hooks/useMain.ts

import useShortProducts from "store/products/short-sleeve";
import useLongProducts from "store/products/long-sleeve";
import useProduct from "store/products/index";
import axios from "axios";
import { useEffect, useState } from "react";
import { CollectionType, IdListType, ImgListType } from "types/products";

const url = "https://api.pexels.com/v1";
const API_KEYS = "-";

const headersProps = {
  headers: {
    authorization: API_KEYS,
  },
};

export default function useMain() {
  const [introImgList, setIntroImgList] = useState<ImgListType[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const LongProducts = useLongProducts((state) => state.products);
  const setAllProducts = useProduct((state) => state.setAllProducts);
  const setShortProducts = useShortProducts((state) => state.setProducts);
  const setLongProducts = useLongProducts((state) => state.setProducts);

  const getCollectionIdList = () =>
    axios(`${url}/collections?per_page=10`, headersProps)
      .then((res) => {
        console.log(res);
        const idList = <IdListType[]>[];
        if (res.data.collections.length > 0) {
          res.data.collections.map((collection: CollectionType) =>
            idList.push({
              id: collection.id,
              title: collection.title,
            })
          );
        }
        if (idList.length > 0) {
          getImgList(idList);
        }
      })
      .catch(function (error) {
        console.log("getCollections---error", error);
      });

  const getImgList = (idList: IdListType[]) => {
    let introImgArr = <ImgListType[]>[];
    idList.map((collectionId) => {
      axios(`${url}/collections/${collectionId.id}`, headersProps)
        .then((res) => {
          if (res.data.media.length > 0) {axios(`${url}/collections/${collectionId.id}`, headersProps)
        .then((res) => {
          const randomNum = Math.floor(Math.random() * 5);
          if (res.data.media.length > 0) {
            setAllProducts(res.data.media, collectionId.title);
            if (collectionId.title.includes("short-sleeve")) {
              setShortProducts(res.data.media, collectionId.title);
            } else if (collectionId.title.includes("long-sleeve")) {
              setLongProducts(res.data.media, collectionId.title);
            }
            introImgArr.push({
              url: res.data.media[randomNum].src.landscape,
              title: collectionId.title,
            });
          }
        })
        .catch(function (error) {
          console.log("getTShirtList---error", error);
        });
    });
    setIntroImgList(introImgArr);
    setIsLoading(false);
  };

  useEffect(() => {
    if (LongProducts.length < 1) {
      setIsLoading(true);
      getCollectionIdList();
    }
  }, []);

  return { isLoading, introImgList };
}

먼저, useMain funcion을 간단히 소개하자면
useMain()은 외부 API에서 response를 받는 과정에서 data를 store에 set하는 custom hook이다.

위 코드 예시에서 살펴볼 점은,

 1. API 트래픽 조절을 위해 zustand store에 data가 없는 경우에만 API요청을 하도록 코드를 작성하였다. (아래 코드로 확인 ↓ )

import useLongProducts from "store/products/long-sleeve";
...
export default function useMain() {
	const LongProducts = useLongProducts((state) => state.products);
    ...
    useEffect(() => {
        if (LongProducts.length < 1) {
          setIsLoading(true);
          getCollectionIdList();
        }
    }, []);
    ...

 2. useMain()이 호출되는 시점에서 data를 사용하는 경우에는 store에서 data를 import 하지 않고, useState()를 이용하여 API response를 받을 때, 직접 data를 set하고 return하여 return된 state를 component에 prop으로 전달하여 사용하였다.

export default function useMain() {
...
	const [introImgList, setIntroImgList] = useState<ImgListType[]>([])
	const [isLoading, setIsLoading] = useState(false);
	...
	const getImgList = (idList: IdListType[]) => {
    let introImgArr = <ImgListType[]>[];
    idList.map((collectionId) => {
      axios(`${url}/collections/${collectionId.id}`, headersProps)
        .then((res) => {
          if (res.data.media.length > 0) {axios(`${url}/collections/${collectionId.id}`, headersProps)
        .then((res) => {
          const randomNum = Math.floor(Math.random() * 5);
          if (res.data.media.length > 0) {
			      ...
            introImgArr.push({
              url: res.data.media[randomNum].src.landscape,
              title: collectionId.title,
            });
          }
        })
        ...
    });
    setIntroImgList(introImgArr);
    setIsLoading(false);
  };
...
return { isLoading, introImgList };

return된 isLoading과 introImgList를 ↑ / prop으로 전달 ↓

...
import useMain from "hooks/useMain";

const IndexPage = () => {
  const { isLoading, introImgList } = useMain();

  return (
    <Layout>
      {/* <PageIntro /> */}
      {!isLoading && <TestImgs photoList={introImgList} />}
      ...
  );
};

export default IndexPage;

 3. useMain()이 호출된 시점이 아닌 다른 시점에서 data를 사용해야 하는 경우를 위해
     response를 store에 저장하는 시점에 사용하기 좋은 형태로 set한 후, 사용하고자 하는 페이지나 component에서 store를 import         하여 바로 사용 혹은 가공하여 사용하였다. (아래 코드로 확인 ↓ *위와 다른 코드 예제 )

...
import useCart from "store/cart";

const CheckoutPage = () => {
  const priceTotal = useCart((state) => {
    const cartItems = state.cartItems;
    let totalPrice = 0;
    if (cartItems.length > 0) {
      cartItems.map((item) => (totalPrice += item.price * item.count));
    }

    return totalPrice;
  });

  return (
    <Layout>
     ...
		  <div className="checkout-total">
	      <p>Total cost</p>
        <h3>${priceTotal}</h3>
	    </div>
     ...
    </Layout>
  );
};

export default CheckoutPage;

 


Reference

https://github.com/reactstudy2022/ecommerce (예시 코드 전문)
https://ui.toast.com/posts/ko_20210812
https://blacklobster.tistory.com/3
[react] 상!태!관!리! Zustand 를 아십니까? (velog.io)

'Front-end' 카테고리의 다른 글

Next.js 13 <Link /> with Middleware  (0) 2023.09.12
Next.js 13 Paradigm  (0) 2023.09.05
왜 정규 변수가 아닌 useState를 사용해야 할까?  (0) 2023.02.18
🐻zustand 이해하기  (0) 2023.01.16