: "Mock"은 테스트 또는 개발 중에 실제 데이터 또는 외부 서비스를 대신하여 사용되는 가짜 또는 모의 데이터 또는 객체를 의미
이는 실제 시스템과의 상호 작용을 시뮬레이션하거나 특정 상황을 재현하여 코드를 테스트하거나 개발하는 데 유용하다.
=> 백엔드에 데이터가 아직 없거나, 불러오지 못할 경우에 서버를 임시로 만드는 것은 어려우므로 mock 데이터를 사용한다.
yarn add msw --dev
yarn add graphql-tag
yarn add graphql-requset
: GraphQL은 페이스북에서 개발된 데이터 질의 및 조작 언어이다.
RESTful API의 대안으로 등장한 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;
yarn add --dev @types/uuid
v4를 사용하면 랜덤값으로 사용할 수 있다.
▼ 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에 전달해 이미지를 띄우는 사이트를 찾아 사용했다.
npx msw init public/ --save
▼ 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);
공식문서에 있는 것을 그대로 가져왔다.
▼ src/pages/_layout.tsx
import { worker } from "../mocks/browser";
...
if (import.meta.env.DEV) {
worker.start();
}
두 문장을 추가한다.
콘솔에 [MSW] Mocking enabled.가 뜨면 성공.
기존의 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>
);
https://recoiljs.org/ko/docs/introduction/getting-started
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를 사용한다.
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 타입을 정의하고, 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));
}),
▼ src/components/cart/item.tsx
const CartItem = ({ id, title, imageUrl, price, amount }: Cart) => (
<li>
{id}
<img src={imageUrl} /> {title} {price} {amount}개
</li>
);
▼ src/components/cart/index.tsx
const CartList = ({ items }: { items: Cart[] }) => {
return (
<div>
<ul>
{items.map((item) => (
<CartItem {...item} key={item.id} />
))}
</ul>
</div>
);
};
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>
);
위와 같이 장바구니를 간단하게 띄웠다 .!