[ React ] msw mock data & qraphql 사용하기

CJY00N·2023년 7월 10일
0

react

목록 보기
5/10
post-thumbnail

⚡️ msw mock data로 서버처럼 구현하기

mock이란?

: "Mock"은 테스트 또는 개발 중에 실제 데이터 또는 외부 서비스를 대신하여 사용되는 가짜 또는 모의 데이터 또는 객체를 의미
이는 실제 시스템과의 상호 작용을 시뮬레이션하거나 특정 상황을 재현하여 코드를 테스트하거나 개발하는 데 유용하다.
=> 백엔드에 데이터가 아직 없거나, 불러오지 못할 경우에 서버를 임시로 만드는 것은 어려우므로 mock 데이터를 사용한다.

msw 설치하기

https://mswjs.io/

yarn add msw --dev
yarn add graphql-tag
yarn add graphql-requset

graphql이란?

: GraphQL은 페이스북에서 개발된 데이터 질의 및 조작 언어이다.
RESTful API의 대안으로 등장한 GraphQL은 클라이언트가 필요한 데이터를 명시적으로 요청하고 받을 수 있는 유연하고 효율적인 방법을 제공한다.
=> 미리 쿼리 형식을 만들어 놓음으로써 백엔드와의 협업 시 '이렇게 동작하게끔 데이터를 넘겨주세요.'라는 식으로 요청할 수 있어서 의사소통이 더 효율적이게 된다.

graphql 쿼리문 작성

▼ src/graphql/products.ts

export type Product = {
  id: string;
  imageUrl: string;
  price: number;
  title: string;
  description: string;
  createdAt: string;
};

export type Products = {
  products: Product[];
};

const GET_PRODUCTS = gql`
  query GET_PRODUCTS {
    id
    imageUrl
    price
    title
    description
    createdAt
  }
`;

export default GET_PRODUCTS;

uuid 사용하기

yarn add --dev @types/uuid

v4를 사용하면 랜덤값으로 사용할 수 있다.

Mock handler 정의하기

▼ src/mocks/handler.ts

const mockProducts = Array.from({ length: 20 }).map((_, i) => ({
  id: uuid(),
  imageUrl: `https:// /200x150/${i}0000/${i}0000`,
  price: 50000,
  title: `임시상품${i + 1}`,
  description: `임시상세내용${i + 1}`,
  createdAt: new Date(1654567890123 + i * 1000 * 60 * 60 * 24).toString(),
}));

export const handlers = [
  graphql.query(GET_PRODUCTS, (req, res, ctx) => {
    return res(
      ctx.data({
        products: mockProducts,
      })
    );
  }),
];

✚ placeimg.com 사이트가 운영을 종료하여 마땅한 사이트를 찾다가 dummyimage.com라는 color와 size값을 url에 전달해 이미지를 띄우는 사이트를 찾아 사용했다.

public 디렉토리 생성

npx msw init public/ --save

browser.ts 생성하기

▼ src/mocks/browser.ts

import { setupWorker } from "msw";
import { handlers } from "./handlers";

// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers);

공식문서에 있는 것을 그대로 가져왔다.

_layout에서 worker 실행하기

▼ src/pages/_layout.tsx

import { worker } from "../mocks/browser";

...

  if (import.meta.env.DEV) {
    worker.start();
  }

두 문장을 추가한다.
콘솔에 [MSW] Mocking enabled.가 뜨면 성공.

graphqlfetcher 추가하기

기존의 fetcher는 restfetcher로 이름을 변경하고, 아래에 graphqlfetcher를 추가한다.

▼ src/queryClient.ts

export const graphqlFetcher = <T>(query: RequestDocument, variables = {}) =>
  request<T>(BASE_URL, query, variables);

그리고 BASE_URL을 "/"로 변경한다.

 const BASE_URL = "/";

상품목록 불러오기

graphqlFetcher로 products를 불러와서 화면에 띄운다.
▼ src/pages/products/index.tsx

const { data } = useQuery<Products>(QueryKeys.PRODUCTS, () =>
    graphqlFetcher<Products>(GET_PRODUCTS)
  );

  return (
    <div>
      <h2>상품목록</h2>
      <ul className="products">
        {data?.products?.map((product) => (
          <ProductItem {...product} key={product.id} />
        ))}
      </ul>
    </div>
  );


그러면 이런식으로 화면에 출력된다.

상품상세 불러오기

먼저 핸들러에서 GET_PRODUCT를 정의한다.
일단 id값을 받아오는 것은 나중에 하도록하고, 어떤 상품이던 첫번째 배열의 값을 띄우는 것으로 한다.
id값에 해당하는 product(found)를 찾아 상세 정보를 출력한다.
▼ src/mocks/handler.ts

export const handlers = [
  graphql.query(GET_PRODUCTS, (req, res, ctx) => {
    return res(
      ctx.data({
        products: mockProducts,
      })
    );
  }),
    graphql.query(GET_PRODUCT, (req, res, ctx) => {
    const found = mockProducts.find((item) => item.id === req?.variables.id);
    if (found) return res(ctx.data(found));
    return res(ctx.data(mockProducts[0]));
  }),
];

그러고나서 마찬가지로 graphqlFetcher를 호출하여 data 값을 받아오고 화면에 출력한다.
▼ src/pages/products/[id].tsx

  const { id } = useParams<"id">();

  const { data } = useQuery<Product>([QueryKeys.PRODUCTS, id], () =>
    graphqlFetcher<Product>(GET_PRODUCT, { id })
  );
  console.log(data);

  if (!data) return null;

  return (
    <div>
      <h2>상품상세</h2>
      <ProductDetail item={data} />
    </div>
  );

⚡️ recoil 사용하기

https://recoiljs.org/ko/docs/introduction/getting-started

recoil 다운로드

yarn add recoil

layout에서 RecoilRoot로 감싸는 부분을 추가한다.
▼ src/pages/_layout.tsx

  return (
    <QueryClientProvider client={queryClient}>
      <Suspense fallback={"loading..."}>
        <RecoilRoot>
          <Gnb />
          <Outlet />
        </RecoilRoot>
      </Suspense>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>

id값을 params로 받아서 상태관리를 하기 위해 selectorFamily를 사용한다.

  • get함수는 id값을 받아 해당하는 값을 반환한다.
  • set함수는 id값을 받아 newValue값으로 변경한다.
    ▼ src/recoils/cart.ts
import { atom, selectorFamily, useRecoilValue } from "recoil";

const cartState = atom<Map<string, number>>({
  key: "cartState",
  default: new Map(),
});

export const cartItemSelector = selectorFamily<number | undefined, string>({
  key: "cartItem",
  get:
    (id: string) =>
    ({ get }) => {
      const carts = get(cartState);
      return carts.get(id);
    },
  set:
    (id: string) =>
    ({ get, set }, newValue) => {
      if (typeof newValue === "number") {
        const newCart = new Map([...get(cartState)]);
        newCart.set(id, newValue);
        set(cartState, newCart);
      }
    },
});

item을 띄울 때 담기 버튼을 추가하고 해당 버튼을 클릭하면 옆 숫자가 1씩 증가하도록 한다.
▼ src/components/product/item.tsx

  const [cartAmount, setCartAmount] = useRecoilState(cartItemSelector(id));
  const addToCart = () => setCartAmount((prev) => (prev || 0) + 1);

...

      <button className="product-item_add-cart" onClick={addToCart}>
        담기
      </button>
      <span>{cartAmount || 0}</span>


위는 recoil을 이용한 방법이었다.
다음으로는 recoil을 사용하지 않고, graphql을 이용해서 장바구니 기능을 구현할 것이다.

⚡️ 장바구니 페이지

CART 타입 / 쿼리문 정의

먼저, CART 타입을 정의하고, GET_CART와 ADD_CART 쿼리문을 작성한다.
▼src/graphql/cart.ts

export type Cart = {
  id: string;
  imageUrl: string;
  price: number;
  title: string;
  amount: number;
};

export const ADD_CART = gql`
  mutation ADD_CART($id: string) {
    cart(id: $id) {
      id
      imageUrl
      price
      title
      amount
    }
  }
`;
export const GET_CART = gql`
  query GET_CART {
    cart {
      id
      imageUrl
      price
      title
      amount
    }
  }
`;

핸들러 정의

처음 cartData를 빈 배열로 생성한다.
▼ src/mocks/handler.ts

let cartData: { [key: string]: Cart } = {};

cart에 제품을 추가할 때는 mutation을 이용하며, 현재 cartData를 담고 있는 newData를 새로 정의하고 해당 id값에 해당하는 것이 이미 배열에 있으면 원래의 amount값에 1을 추가하고, 없으면, id값에 해당하는 제품을 배열에 넣고 amount값을 1로 설정한다.
cartData를 newData로 덮어씌운다.

  graphql.mutation(ADD_CART, (req, res, ctx) => {
    const newCartData = { ...cartData };
    const id = req.variables.id;
    const targetProduct = mockProducts.find(
      (item) => item.id === req?.variables.id
    );

    if (!targetProduct) {
      throw new Error("상품이 없습니다.");
    }

    const newItem = {
      ...targetProduct,
      amount: (newCartData[id]?.amount || 0) + 1,
    };
    newCartData[id] = newItem;
    cartData = newCartData;

    return res(ctx.data(newItem));
  }),

GET_CART는 cartData를 리턴해준다.

  graphql.query(GET_CART, (req, res, ctx) => {
    return res(ctx.data(cartData));
  }),

CartItem 컴포넌트 정의

▼ src/components/cart/item.tsx

const CartItem = ({ id, title, imageUrl, price, amount }: Cart) => (
  <li>
    {id}
    <img src={imageUrl} /> {title} {price} {amount}</li>
);

CartList 컴포넌트 정의

▼ src/components/cart/index.tsx

const CartList = ({ items }: { items: Cart[] }) => {
  return (
    <div>
      <ul>
        {items.map((item) => (
          <CartItem {...item} key={item.id} />
        ))}
      </ul>
    </div>
  );
};

장바구니 페이지에서 CartList 출력하기

GET_CART로 CART 데이터를 받아온다.
react query는 기본적으로 캐시된 데이터를 저장하고 일정 시간동안 사용하기 때문에, ADD_CART를 한 후에 다시 장바구니를 확인하면 업데이트되지 않는 경우가 있다.
따라서 cachetime과 staletime을 0으로 설정해주면 항상 최신 상태를 확인할 수 있다.
▼ src/pages/cart/index.tsx

  const { data } = useQuery(QueryKeys.CART, () => graphqlFetcher(GET_CART), {
    staleTime: 0,
    cacheTime: 0,
  });
  const cartItems = Object.values(data || {}) as Cart[];
  if (!cartItems.length) {
    return <div>장바구니가 비었어요.</div>;
  }
  return (
    <div>
      <h2>장바구니</h2>
      <CartList items={cartItems} />
    </div>
  );


위와 같이 장바구니를 간단하게 띄웠다 .!

profile
COMPUTER SCIENCE ENGINEERING / Web Front End

0개의 댓글