1️⃣ msw로 Mock API 만들기
1. msw란?
Mock Service Worker는 서비스 워커를 이용하여 API를 모킹하는 라이브러리입니다.
MSW는 Service Worker API를 활용해 네트워크 요청을 가로채고 특정한 동작을 수행하도록 지정할 수 있습니다.
https://mswjs.io/docs/getting-started
Getting started
Three steps to get started with Mock Service Worker.
mswjs.io
공식 문서에 나와있는대로 설치해주기
yarn add msw --save-dev
npx msw init public/ --save
2. src/mocks/handlers.ts 파일 만들어서 추가
import { graphql } from "msw";
import { v4 as uuid } from "uuid";
import GET_PRODUCTS from "../graphql/products";
const mock_products = Array.from({ length: 20 }).map((_, i) => ({
id: uuid(),
imageUrl: `https://placeimg.com/640/480/${i + 1}`,
price: 50000,
title: `임시상품${i + 1}`,
description: `임시상세내용${i + 1}`,
createdAt: new Date(1645735501883 + i * 1000 * 60 * 60 * 10).toString(),
}));
export const handlers = [
graphql.query(GET_PRODUCTS, (req, res, ctx) => {
return res(
ctx.data({
products: mock_products,
})
);
}),
];
- 여기에서 계속 '(req: any, res: any, ctx: any) => any' 형식의 인수는 'GraphQLResponseResolver<GraphQLQuery, GraphQLVariables>' 형식의 매개 변수에 할당될 수 없습니다. 오류가 떠서 구글링을 해봐도 안 나오고... chat GPT한테 물어도 이상한 소리나 하고 있어서 강사님의 커뮤니티에 들어갔더니 버전 오류,, 버전 오류가 제일 짱난다
이렇다고 하셔서 버전 다운그레이드!!
다운그레이드 하니까 너무 잘된다,,
3. graphql로 mock api 만들기
yarn add graphql-tags
yarn add graphql-request
src/graphql/poducts.ts 파일 추가
import { gql } from "graphql-tag";
export type PRODUCTS = {
id: string;
imageUrl: string;
price: number;
title: string;
description: string;
createdAt: string;
};
const GET_PRODUCTS = gql`
query GET_PRODUCTS {
id
imageUrl
price
title
description
createdAt
}
`;
export default GET_PRODUCTS;
4. src/mocks/browser.ts 파일 추가
import { setupWorker } from "msw";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
4-1. _layout.ts 파일에 env 추가
import React, { Suspense } from "react";
import { Outlet } from "react-router-dom";
import { QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import { getClient } from "../queryClient";
import "../scss/index.scss";
import Gnb from "../assets/components/gnb";
import { worker } from "../mocks/browser";
if (import.meta.env.DEV) {
worker.start();
}
const Layout: React.FC = () => {
const queryClient = getClient();
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={"loading..."}>
<Gnb />
<Outlet />
</Suspense>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
export default Layout;
- 이렇게까지 하고 콘솔 봤더니 오류,,
- 터미널에 npx msw init public 치고 package.json에
"msw": {
"workerDirectory": "public"
},
이거 추가했더니 오류 해결...
하.. 힘들다
2️⃣ Rest >> Graphql로 변경하기
- 여기서도 강의시점과 바뀐게 꽤 있어서 오류 해결하느라 진땀뺐다,, 휴,,
1. 우선 queryClient.ts에서 fetcher 수정하기
// resetFetcher async로 요청
export const restFetcher = async ({
...
};
// graphqlFetcher 타입 설정을 이렇게 해주면 된다
export const graphqlFetcher = <T>(query: RequestDocument, variables = {}) =>
request<T>(BASE_URL, query, variables);
- 원래 있던 fetcher를 restFetcher로 이름을 바꿔주고, 하단에 graphqlFetcher를 새로 생성하기
- 여기에서 오류가 발생했는데,,
No overload matches this call.
Overload 1 of 3, '(queryKey: QueryKey, options?: Omit<UseQueryOptions<Product[], unknown, Product[], QueryKey>, "queryKey"> | undefined): UseQueryResult<...>', gave the following error.
Type '() => Promise<unknown>' has no properties in common with type 'Omit<UseQueryOptions<Product[], unknown, Product[], QueryKey>, "queryKey">'.
Overload 2 of 3, '(queryKey: QueryKey, queryFn: QueryFunction<Product[], QueryKey>, options?: Omit<UseQueryOptions<Product[], unknown, Product[], QueryKey>, "queryKey" | "queryFn"> | undefined): UseQueryResult<...>', gave the following error.
Type 'Promise<unknown>' is not assignable to type 'Product[] | Promise<Product[]>'.
Type 'Promise<unknown>' is not assignable to type 'Promise<Product[]>'.
Type 'unknown' is not assignable to type 'Product[]'.ts(2769)
types.d.ts(9, 89): The expected type comes from the return type of this signature.
- 이와 같은 오류가 계속 뜨는 것이었다,, 알고봤더니 typescript에서 타입 선언방식이 바뀌어서 그런 것,,
- 바꾸니까 해결 완!
2. src/graphql/products.ts 수정
import { gql } from "graphql-tag";
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;
export const GET_PRODUCT = gql`
query GET_PRODUCT($id: string) {
id
imageUrl
price
title
description
createdAt
}
`;
- 모든 목록을 불러오는 GET_PRODUCTS와 상세 데이터를 불러오는 GET_PRODUCT 만들어주기
3. pages/products/index.tsx 수정
import { useQuery } from "react-query";
import { QueryKeys, graphqlFetcher } from "../../queryClient";
import ProductItem from "../../assets/components/product/item";
import GET_PRODUCTS, { Products } from "../../graphql/products";
const ProductsList = () => {
// 데이터 가져오기, type 정의
const { data } = useQuery<Products>(QueryKeys.PRODUCTS, () =>
graphqlFetcher<Products>(GET_PRODUCTS)
);
return (
<div>
<h2>상품목록</h2>
<ul className="products">
{data?.products?.map((product: any) => (
<ProductItem {...product} key={product.id} />
))}
</ul>
</div>
);
};
export default ProductsList;
- 데이터 가져오는 부분과 컴포넌트로 만든 부분을 수정했다
- 위에서 발생한 오류가 여기와 연관이 되었었다
4. pages/poducts/[id].tsx 수정
import { useQuery } from "react-query";
import { useParams } from "react-router-dom";
import { graphqlFetcher, QueryKeys } from "../../queryClient";
import ProductDetail from "../../assets/components/product/detail";
import { GET_PRODUCT, Product } from "../../graphql/products";
const ProductsDetail = () => {
const { id } = useParams();
// 데이터 가져오기, type 정의
const { data } = useQuery<Product>([QueryKeys.PRODUCTS, id], () =>
graphqlFetcher<Product>(GET_PRODUCT, { id })
);
if (!data) return null;
return (
<div>
<h2>상품상세</h2>
<ProductDetail item={data} />
</div>
);
};
export default ProductsDetail;
- 여기에서도 데이터 가져오는 부분과 컴포넌트 부분 수정
3️⃣ 장바구니 mock API - Recoil 버전
1. item 컴포넌트에 장바구니 담기 버튼 추가
<button className="product-item__add-cart">장바구니 담기</button>
2. recoil로 상태관리
yarn add recoil
3. src/recoils/cart.ts 파일 생성
import { atom, selectorFamily } 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);
}
},
});
4. item 컴포넌트에 state 추가
import { Link } from "react-router-dom";
import { Product } from "../../../graphql/products";
import { cartItemSelector } from "../../../recoils/cart";
import { useRecoilState, useRecoilValue } from "recoil";
const ProductItem = ({ imageUrl, price, title, id }: Product) => {
const [cartAmount, setCartAmount] = useRecoilState(cartItemSelector(id));
const addToCart = () => setCartAmount((prev) => (prev || 0) + 1);
return (
<li className="products-item">
<Link to={`/products/${id}`}>
<p className="products-item__title">{title}</p>
<img className="products-item__image" src={imageUrl} />
<span className="products-item__price">${price}</span>
</Link>
<button className="product-item__add-cart" onClick={addToCart}>
장바구니 담기
</button>
<span>{cartAmount || 0}</span>
</li>
);
};
export default ProductItem;
- 전역상태관리 라이브러리는 처음 써보는데 이렇게 관리하니까 훨씬 좋은 거 같다,, 👍🏻
4️⃣ 장바구니 API - graphql 버전
- 이 부분에서 알 수 없는 404에러를 만나서 한 3-4시간?? 정도 오류해결하느라 너무 힘들었다.. ㅠㅠ 알고보니 너무 간단한 문제였지만 해결해서 저어어어말 다행이었다,,
1. product/item.tsx 수정
import { Link } from "react-router-dom";
import { Product } from "../../../graphql/products";
import { useMutation } from "react-query";
import { graphqlFetcher } from "../../../queryClient";
import { ADD_CART, Cart } from "../../../graphql/cart";
const ProductItem = ({ imageUrl, price, title, id }: Product) => {
// recoil 대신 mutation을 이용해 장바구니에 넣어주기
const { mutate: addCart } = useMutation((id: string) =>
graphqlFetcher(ADD_CART, { id })
);
return (
<li className="products-item">
<Link to={`/products/${id}`}>
<p className="products-item__title">{title}</p>
<img className="products-item__image" src={imageUrl} />
<span className="products-item__price">${price}</span>
</Link>
<button className="product-item__add-cart" onClick={() => addCart(id)}>
장바구니 담기
</button>
</li>
);
};
export default ProductItem;
2. mocks/handlers.ts 수정
// 장바구니에 넣는 데이터
graphql.mutation(ADD_CART, (req, res, ctx) => {
const newCarData = { ...cartData };
const id = req.variables.id;
const targetProduct = mock_products.find(
(item) => item.id === req.variables.id
);
if (!targetProduct) {
throw new Error("상품이 없습니다");
}
const newItem = {
...targetProduct,
amount: (newCarData[id]?.amount || 0) + 1,
};
newCarData[id] = newItem;
cartData = newCarData;
return res(ctx.data(newItem));
}),
3. queryClient.ts 수정
// graphqlFetcher
export const graphqlFetcher = (query: RequestDocument, variables = {}) =>
request(`${BASE_URL}`, query, variables, {
"Content-Type": "application/json",
// "Access-Control-Allow-Origin": BASE_URL,
});
// 쿼리 키 만들기
export const QueryKeys = {
PRODUCTS: "PRODUCTS",
CART: "CART",
};
- 처음에 404에러가 이 부분에서 난 줄 알고 열심히 찾고 찾고 찾았는데.. 아니어서 다행인지 뭔지.. 하하
4. graphql/cart.ts 수정
import { gql } from "graphql-tag";
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
}
}
`;
// 장바구니에 담긴 데이터 가져오기
const GET_CART = gql`
query GET_CART {
cart {
id
imageUrl
price
title
}
}
`;
export default GET_CART;
- 에러의 원인이었던 파일!!!!
// 이 부분을
import { gql } from "graphql-request";
// 이렇게 바꿨더니 오류 해결
import { gql } from "graphql-tag";
- 정말 생각지도 못했는데.. 인프런 질문 페이지를 보다가 설마설마 하면서 바꿨더니 오류가 해결되어버렸다,, 흑흑 정말 너무 간단한 거여서 날린 시간이 아까웠지만 그래도 해결해서 얼마나 기쁘던지 ㅠㅠ