ํ†ตํ•ฉํ…Œ์ŠคํŠธ

์ž‘์„ฑ์ผ : 10/17/2025
๊ฐ•์˜์ž : ์ฝ”๋“œ์กฐ์ปค, ์˜คํ”„
์ œ๊ณต : ์ธํ”„๋Ÿฐ

1. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ž€?

๋‘ ๊ฐœ ์ด์ƒ์˜ ๋ชจ๋“ˆ์ด ์—ฐ๊ฒฐ๋œ ์ƒํƒœ๋ฅผ ๊ฒ€์ฆ.
๋ชจ๋“ˆ๋“ค์ด ์ƒํ˜ธ ์ž‘์šฉํ–์—ฌ ๋ฐœ์ƒํ•˜๋Š” ์ƒํƒœ๋ฅผ ๊ฒ€์ฆ.
๊ทธ๋ž˜์„œ ์‹ค์ œ ์•ฑ์ด ๋™์ž‘ํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๊ฐ€๊น๊ฒŒ ๊ธฐ๋Šฅ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ•ญ๋ชฉ

  • ํŠน์ • ์ƒํƒœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ์กฐํ•ฉ
  • API์™€ ํ•จ๊ป˜ ์ƒํ˜ธ์ž‘์šฉ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ์กฐํ•ฉ
  • ๋‹จ์ˆœ UI ๋ Œ๋”๋ง ๋ฐ ๊ฐ„๋‹จํ•œ ๋กœ์ง์„ ์‹คํ–‰ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ(๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜์ง€ ์•Š์•˜๋˜)๊ฐ€ ์ œ๋Œ€๋กœ ๋ Œ๋”๋ง ๋˜๋Š”์ง€ ํ•œ๋ฒˆ์— ๊ฒ€์ฆ

์˜ˆ์‹œ

์ˆ˜์—… ์ž๋ฃŒ์˜ ProductList์™€ ProductCard ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด์ž.

const ProductCard = ({
  product,
  onClickAddCartButton,
  onClickPurchaseButton,
}) => {
 const navigate = useNavigate();

  if (!product) {
    return null;
  }

  const { title, images, price, category, id } = product;

  const handleClickItem = () => {
    navigate(pathToUrl(pageRoutes.productDetail, { productId: id }));
  };
  
  const handleClickAddCartButton = ev => {
    onClickAddCartButton(ev, product);
  };
  
  const handleClickPurchaseButton = ev => {
    onClickPurchaseButton(ev, product);
  };
  
  return (.....);
};

ํ”„๋กœ๋•ํŠธ ์นด๋“œ๋Š” ์ž์‹ ์„ ํ˜ธ์ถœํ•˜๋Š” ํ”„๋กœ๋•ํŠธ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ „๋‹ฌ๋ฐ›์€ prop๋“ค(product ๊ฐ์ฒด, ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋“ค)์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์ด prop๋“ค์„ ๊ฐ€์ ธ์˜ค๊ณ  ์ •์˜ํ•˜๋Š” ๊ณณ์€ ํ”„๋กœ๋•ํŠธ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ๋‹ค.

const ProductList = ({ limit = PRODUCT_PAGE_LIMIT }) => {
	...
	...
	
	const products =
    data?.pages.reduce((acc, cur) => [...acc, ...cur.products], []) ?? [];

  const handleClickCart = (ev, product) => {
    ev.stopPropagation();
    if (isLogin) {
      addCartItem(product, user.id, 1);
      toast.success(`${product.title} ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ ์™„๋ฃŒ!`, { id: TOAST_ID });
    } else {
      navigate(pageRoutes.login);
    }
  };
  
  const handleClickPurchase = (ev, product) => {
    ev.stopPropagation();
    if (isLogin) {
      addCartItem(product, user.id, 1);
      navigate(pageRoutes.cart);
    } else {
      navigate(pageRoutes.login);
    }
  };
	return (
	    <Grid container spacing={1} rowSpacing={1} justifyContent="center">
	      {products.map((product, index) => (
	        <ProductCard
	          key={`${product.id}_${index}`}
	          product={product}
	          onClickAddCartButton={handleClickCart}
	          onClickPurchaseButton={handleClickPurchase}
	        />
			))}
		...
		...
	);
};

๋จผ์ € ProductCard๋ฅผ ๋‹จ์œ„ํ…Œ์ŠคํŠธ ํ–ˆ์„ ๋•Œ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋Š” ํ•ญ๋ชฉ๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค :

  1. prop ๊ธฐ์ค€์œผ๋กœ ์ œํ’ˆ ์ •๋ณด๊ฐ€(price, name ..) ์ œ๋Œ€๋กœ ๋ Œ๋”๋ง ๋˜๋Š”์ง€
  2. ์ƒํ’ˆ์„ ํด๋ฆญ ํ–ˆ์„ ๋•Œ(handleClickItem) navigate ๋ชจํ‚น์„ ํ†ตํ•ด ์ƒ์„ธํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๋Š”์ง€
  3. ์žฅ๋ฐ”๊ตฌ๋‹ˆ, ๊ตฌ๋งค ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ spy ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๊ฐ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋˜๋Š”์ง€
    • ํ˜ธ์ถœ์ด ๋˜๋Š”์ง€๋Š” ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•  ์ˆ˜ ์—†๋‹ค.

๋ฐ˜๋ฉด ProductList๋ฅผ ํ†ตํ•ฉํ…Œ์ŠคํŠธ ํ–ˆ์„ ๋•Œ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋Š” ํ•ญ๋ชฉ๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค :

  1. ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ api์— ๋งž๊ฒŒ ์ƒํ’ˆ ์ •๋ณด๊ฐ€ ์ œ๋Œ€๋กœ ๋ Œ๋”๋ง ๋˜๋Š”์ง€
  2. ์ƒํ’ˆ์„ ํด๋ฆญ ํ–ˆ์„ ๋•Œ navigate ๋ชจํ‚น์„ ํ†ตํ•ด ์ƒ์„ธํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๋Š”์ง€
  3. ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํ˜น์€ ๊ตฌ๋งค ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ๋‹ค์Œ๊ณผ๊ฐ™์ด ์ž‘๋™ ํ•˜๋Š”์ง€
    • ๋กœ๊ทธ์ธ : ์ƒํ’ˆ ์ถ”๊ฐ€ ํ›„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋กœ ์ด๋™
    • ๋น„๋กœ๊ทธ์ธ : ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™
  4. ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋” ์žˆ๋Š” ๊ฒฝ์šฐ show more ๋ฒ„ํŠผ์ด ๋…ธ์ถœ๋˜๊ณ , ์ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋” ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š”์ง€

์œ„์˜ ๋‘ ํ•ญ๋ชฉ๋“ค์„ ๋น„๊ตํ•ด๋ณด๋ฉด 1,2,3๋ฒˆ์€ ์ค‘๋ณต๋œ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค. (ํ†ต1,2,3 ์ด ์„ฑ๊ณตํ•œ๋‹ค๋Š”๊ฒƒ์€ ๋‹จ1,2,3 ์ด ์„ฑ๊ณตํ•จ์„ ํ•จ์˜ํ•œ๋‹ค) ๊ทธ๋ž˜์„œ ํ”„๋กœ๋•ํŠธ ์นด๋“œ์— ๋Œ€ํ•œ ๋‹จ์œ„ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜์ง€ ์•Š๊ณ  ํ”„๋กœ๋•ํŠธ๋ฆฌ์ŠคํŠธ์˜ ํ†ตํ•ฉํ…Œ์ŠคํŠธ์—์„œ ํ•œ๋ฒˆ์— ๊ฒ€์ฆํ•˜๋Š”๊ฒƒ์ด ๋” ํšจ๊ณผ์ ์ด๋‹ค.

2. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ์„ ์ •ํ•˜๊ธฐ

๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋Š” ์˜์กด์„ฑ์ด ์ ๊ฑฐ๋‚˜ ์—†๋Š” ๋‹จ์ˆœํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•œ๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” API, ์ƒํƒœ ๊ด€๋ฆฌ ์Šคํ† ์–ด, ๋ฆฌ์•กํŠธ ์ปจํ…์ŠคํŠธ ๋“ฑ ๋‹ค์–‘ํ•œ ์š”์†Œ๋“ค์ด ๊ฒฐํ•ฉ๋œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํŠน์ • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜ํ–‰ํ•˜๋Š”์ง€ ๊ฒ€์ฆ. (์ฆ‰ ์ปดํฌ๋„ŒํŠธ๊ฐ™์˜ ์ƒํ˜ธ์ž‘์šฉ, api ํ˜ธ์ถœ ๋ฐ ์ƒํƒœ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ UI ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๊ฒ€์ฆ)

์˜ˆ์ œ์† ๋ฉ”์ธ ํŽ˜์ด์ง€์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋“ค๋กœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€๊ฒƒ๋“ค์ด ์žˆ๋‹ค :

  • ๋„ค๋น„๊ฒŒ์ด์…˜ ์˜์—ญ์˜ ๋กœ๊ทธ์ธ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ๋™์ž‘
  • api๋ฅผ ํ†ตํ•ด ํ•„ํ„ฐ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ Œ๋”๋ง ํ•˜๋Š”์ง€
  • ํ•„ํ„ฐ ํ•ญ๋ชฉ์„ ์ˆ˜์ •ํ–ˆ์„ ๋•Œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐ˜์˜๋˜๋Š”์ง€
  • api ์‘๋‹ต์— ๋”ฐ๋ผ ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ Œ๋”๋ง ๋˜๋Š”์ง€
  • ๋“ฑ๋“ฑ

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง :

  • ํ”„๋กœ๊ทธ๋žจ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ์ฝ”๋“œ
  • ์ฆ‰ ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ์–ป๊ธฐ ์œ„ํ•œ ๊ณ„์‚ฐ, ์ฒ˜๋ฆฌ, ์˜์‚ฌ ๊ฒฐ์ •์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์ฝ”๋“œ

์ข‹์€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋„๋ฉ”์ธ ๋‹จ์œ„๋กœ ์ž˜ ๋‚˜๋ˆ„์–ด ์ˆ˜ํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค.
๋ฉ”์ธ ํŽ˜์ด์ง€์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ถ„๋ฅ˜๋  ์ˆ˜ ์žˆ๋‹ค.

  • ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ” ์˜์—ญ
    • ๋กœ๊ทธ์ธ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ UI ๋ Œ๋”๋ง(์žฅ๋ฐ”๊ตฌ๋‹ˆ)๊ณผ ์ƒํ˜ธ์ž‘์šฉ(๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ)
  • ์ƒํ’ˆ ๊ฒ€์ƒ‰ ์˜์—ญ
    • ํ•„ํ„ฐ ์„ค์ •์— ๋”ฐ๋ฅธ ๊ฒ€์ƒ‰ ์กฐ๊ฑด ์„ค์ •
  • ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ ์˜์—ญ
    • ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง๊ณผ ๋ฒ„ํŠผ ํด๋ฆญ ์ƒํ˜ธ์ž‘์šฉ(์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€, ์ž์„ธํžˆ ๋ณด๊ธฐ)

๋งŒ์•ฝ์— ์ƒํ’ˆ ๊ฒ€์ƒ‰ ์˜์—ญ์—์„œ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ์— ์ƒˆ๋กœ์šด ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ํ•˜๋Š”๋“ฑ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ๊ฑฐ๋‚˜, ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ์—์„œ ๋ณด์—ฌ์ฃผ๋Š” ์ƒํ’ˆ์˜ ์ •๋ณด๊ฐ€ ๋ณ€๊ฒฝ๋˜๊ฑฐ๋‚˜, ์Šคํฌ๋กค ๋กœ๋”ฉ ๋ฐฉ์‹์—์„œ ํŽ˜์ด์ง• ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๋“ฑ์˜ ๊ฐ ๋„๋ฉ”์ธ์—์„œ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์–ด๋„ ํ•ด๋‹น ๋„๋ฉ”์ธ์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋งŒ ๋ณ€๊ฒฝํ•˜๋ฉด ๋œ๋‹ค.

์ฆ‰ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ…Œ์ŠคํŠธ๋ฅผ ์„ค๊ณ„ํ•˜๋ฉด ๋œ๋‹ค.

  1. ๋ฉ”์ธ ํŽ˜์ด์ง€๋ฅผ ์ด๋ฃจ๋Š” ๋ชจ๋“  ์š”์†Œ๋“ค์„ ๋‚˜์—ดํ•˜๊ณ , ๊ทธ ์š”์†Œ๋“ค์„ ๋„๋ฉ”์ธ ๋‹จ์œ„๋กœ ๋ฌถ์–ด์„œ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค.
    • ํŽ˜์ด์ง€(page.tsx) -> ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ(ProductList.tsx)-> ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ(๋ฒ„ํŠผ, ์ƒํ’ˆ์ •๋ณด ๋“ฑ ๋ Œ๋”๋ง ๋˜๋Š” ui ์š”์†Œ)
  2. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์ƒํƒœ ๊ด€๋ฆฌ๋‚˜ api ํ˜ธ์ถœ์„ ์‘์ง‘์‹œํ‚จ๋‹ค.
  3. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ์œ„์˜ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„๋กœ ์ˆ˜ํ–‰ํ•œ๋‹ค.

3. ์ƒํƒœ ๊ด€๋ฆฌ ๋ชจํ‚นํ•˜๊ธฐ

์ƒํƒœ : ์‹œ๊ฐ„์— ๋”ฐ๋ผ ๋ณ€ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ. ํ”„๋ก ํŠธ์—์„œ ํŠนํžˆ '์ƒํƒœ'๊ฐ€ ์ค‘์š”ํ•˜๊ฒŒ ๋‹ค๋ค„์ง€๋Š” ์ด์œ ๋Š” UI๊ฐ€ ์ƒํƒœ์— ์ง์ ‘์ ์œผ๋กœ ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๊ฐ•์˜ ์˜ˆ์ œ์—์„œ CartTable ์ปดํฌ๋„ŒํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋‹ค.

const CartTable = () => {
  return (
    <>
      <PageTitle />
      <ProductInfoTable />
      <Divider sx={{ padding: 2 }} />
      <PriceSummary />
    </>
  );
};

์—ฌ๊ธฐ์„œ PageTitle๊ณผ Divider ์ปดํฌ๋„ŒํŠธ๋Š” ๋„ˆ๋ฌด ๋‹จ์ˆœํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š๊ณ 

ProductInfoTable : ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ธด ์ƒํ’ˆ ๋ชฉ๋ก
PriceSummary : ๊ฐ€๊ฒฉ ์ดํ•ฉ

์œ„์˜ ๋‘ ์ปดํฌ๋„ŒํŠธ๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ? ์ผ๋‹จ ๋‘ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋กœ ์˜์กดํ•˜๊ณ  ์žˆ์ง€ ์•Š๊ณ  ๋ชจ๋‘ zustand ์Šคํ† ์–ด์—์„œ ์ƒํƒœ๋‚˜ ์•ก์…˜์„ ์‚ฌ์šฉํ•œ๋‹ค.
๊ทธ๋ž˜์„œ ์ด ์ƒํƒœ์™€ ์•ก์…˜์„ ๋ชจํ‚นํ•œ๋‹ค๋ฉด ๋‘ ์ปดํฌ๋„ŒํŠธ ๋ชจ๋‘ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

์ฃผ๋กœ ๋ฃจํŠธ์— __mocks__ ํด๋”๋ฅผ ๋งŒ๋“ค๊ณ  ํ•˜์œ„ ๊ฒฝ๋กœ์— ๋ชจํ‚นํŒŒ์ผ์„ ์ž‘์„ฑํ•ด๋‘๋ฉด vitest๋‚˜ jest ์—์„œ ์ž๋™์œผ๋กœ ๋ชจํ‚น์‹œ์— ์‚ฌ์šฉํ•œ๋‹ค.

// __mocks__/zustand.js
const { create: actualCreate } = await vi.importActual('zustand');
import { act } from '@testing-library/react';

// ์•ฑ์— ์„ ์–ธ๋œ ๋ชจ๋“  ์Šคํ† ์–ด์— ๋Œ€ํ•ด ์žฌ์„ค์ • ํ•จ์ˆ˜๋ฅผ ์ €์žฅ
const storeResetFns = new Set();

// ์Šคํ† ์–ด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์ดˆ๊ธฐ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์™€ ๋ฆฌ์…‹ ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜๊ณ  set์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
export const create = createState => {
  const store = actualCreate(createState);
  const initialState = store.getState();
  storeResetFns.add(() => store.setState(initialState, true));
  return store;
};

// ํ…Œ์ŠคํŠธ๊ฐ€ ๊ตฌ๋™๋˜๊ธฐ ์ „ ๋ชจ๋“  ์Šคํ† ์–ด๋ฅผ ๋ฆฌ์…‹ํ•ฉ๋‹ˆ๋‹ค.
beforeEach(() => {
  act(() => storeResetFns.forEach(resetFn => resetFn()));
});
// src/utils/test/mockZustandStore.jsx
import { useCartStore } from '@/store/cart';
import { useFilterStore } from '@/store/filter';
import { useUserStore } from '@/store/user';

const mockStore = (hook, state) => {
  const initStore = hook.getState();
  hook.setState({ ...initStore, ...state }, true);
};

export const mockUseUserStore = state => {
  mockStore(useUserStore, state);
};

export const mockUseCartStore = state => {
  mockStore(useCartStore, state);
};

export const mockUseFilterStore = state => {
  mockStore(useFilterStore, state);
};

์ฃผ์Šคํƒ ๋“œ ๋ชจํ‚น ํ•จ์ˆ˜๋“ค์„ ๋ณด๋ฉด ๋ฐ”๋กœ ์ฃผ์Šคํƒ ๋“œ ์Šคํ† ์–ด์˜ ์ƒํƒœ๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝ(hook.setState())ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
์ด๊ฒŒ ๊ฐ€๋Šฅํ•œ ์ด์œ ๋Š” /__mocks__/zustand.js์—์„œ ํ…Œ์ŠคํŠธ ์‹คํ–‰์‹œ ์ž„์‹œ ์Šคํ† ์–ด๋ฅผ ๋งŒ๋“ค๊ธฐ ๋•Œ๋ฌธ.

๊ทธ๋Ÿฐ๋ฐ ์–ด์ฐจํ”ผ ์ž„์‹œ ์Šคํ† ์–ด๋ฅผ ๋งŒ๋“ ๋‹ค๋ฉด ๊ตณ์ด ๋ชจํ‚น ํ•จ์ˆ˜ mockZustandStore.jsx๊ฐ€ ํ•„์š”ํ• ๊นŒ?

export const useUserStore = create(set => ({
  isLogin: Cookies.get('access_token'),
  user: null,
  setIsLogin: isLogin => set(state => ({ ...state, isLogin })),
  setUserData: user => set(state => ({ ...state, user })),
}));

์›๋ž˜ ์ฃผ์Šคํƒ ๋“œ ์•ก์…˜์„ ํ†ตํ•ด isLogin์„ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด setIsLogin()์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜์ง€๋งŒ, ์œ„์˜ ๋ฐฉ์‹์ฒ˜๋Ÿผ ์ง์ ‘ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•œ๋ฒˆ์— ์—ฌ๋Ÿฌ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.

mockUseUserStore({ isLogin: true, user: { id: 10 } });
// useUserStore.setIsLogin(true);
// useUserStore.setUserData({id:10});
// ์œ„์˜ ๋‘ ์•ก์…˜์„ ๋™์‹œ์— ํ•˜๋Š” ํšจ๊ณผ

์ŠคํŒŒ์ด ํ•จ์ˆ˜ ์‚ฌ์šฉ์„ ์œ„ํ•ด

๋ฌด์—‡๋ณด๋‹ค ์ฃผ์Šคํƒ ๋“œ ์Šคํ† ์–ด ๋ชจํ‚น ํ•จ์ˆ˜๊ฐ€ ํ•„์š”ํ•œ ์ด์œ ๋Š” ์ŠคํŒŒ์ด ํ•จ์ˆ˜ ์‚ฌ์šฉ์˜ ์šฉ์ด์„ฑ์„ ์œ„ํ•ด์„œ๋‹ค.
์ž์„ธํ•œ ๋‚ด์šฉ์€ ์•„๋ž˜ "6. RTL ๋น„๋™๊ธฐ ์œ ํ‹ธ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•œ ๋…ธ์ถœ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ" ๋ถ€๋ฌธ์— ์žˆ๋‹ค.

4. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ - ์ƒํƒœ ๊ด€๋ฆฌ ๋ชจํ‚น

์œ„์—์„œ ๋งํ•œ ๋‘ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๋ชจํ‚นํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•˜์œผ๋‹ˆ ์ด์ œ ์ ์šฉํ•ด๋ณธ๋‹ค.

ProductInfoTable

const ProductInfoTable = () => {
  const { cart, removeCartItem, changeCartItemCount } = useCartStore(state =>
    pick(state, 'cart', 'removeCartItem', 'changeCartItemCount'),
  );
  const { user } = useUserStore(state => pick(state, 'user'));

  return (
    <TableContainer component={Paper} sx={{ wordBreak: 'break-word' }}>
      <Table aria-label="์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฆฌ์ŠคํŠธ">
        <TableBody>
          {Object.values(cart).map(item => (
            <ProductInfoTableRow
              key={item.id}
              item={item}
              user={user}
              removeCartItem={removeCartItem}
              changeCartItemCount={changeCartItemCount}
            />
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
};

๋‹จ์œ„ ํ…Œ์ŠคํŠธ?

ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์ธ ProductInfoTableRow๋„ ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ์ธ๋ฐ ๋” ์ •ํ™•ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ๋Š” ์ด ์ปดํฌ๋„ŒํŠธ๋จผ์ € ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์•ผํ•˜์ง€ ์•Š์„๊นŒ? ์•„๋‹๊นŒ?

๊ฐ•์˜์—์„œ๋Š” ์–ด์ฐจํ”ผ ProductInfoTableRow๋ฅผ ๊ฒ€์ฆํ• ๋•Œ ํ•  ์ˆ˜ ์žˆ๋Š”๊ฑด ์ธ์ž๋กœ ์ „๋‹ฌ๋ฐ›์€ ์ƒํƒœ๋‚˜ ํ•จ์ˆ˜๋ฅผ ์ œ๋Œ€๋กœ ๋ Œ๋”๋งํ•˜๊ณ  ํ˜ธ์ถœํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์—ฌ๋ถ€ ๋ฐ–์— ๊ฒ€์ฆํ•  ์ˆ˜ ์—†์œผ๋‹ˆ๊นŒ ํ†ตํ•ฉํ…Œ์ŠคํŠธ์— ํฌํ•จํ•˜์—ฌ, ์ƒํ’ˆ ๋ชฉ๋ก์„ ์ œ๋Œ€๋กœ ์ถœ๋ ฅํ•˜๋Š”์ง€๋ฅผ ํ™•์ธํ•˜๋Š”๊ฒŒ ๋” ํšจ๊ณผ์ ์ด๋ผ๊ณ  ํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ์•„๋ฆฌ์†กํ•ด์ง€๋Š”๊ฒŒ ๊ทธ๋Ÿฌ๋ฉด ์• ์ดˆ์— ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์™œ ํ•„์š”ํ•œ๊ฑฐ์˜€๋”๋ผ?
์œ ํ‹ธํ•จ์ˆ˜๋‚˜ ์ปค์Šคํ…€ํ›… ๋ง๊ณ  ๋ Œ๋”๋ง ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๊ฒ€์ฆ ํ•  ํ•„์š”๊ฐ€ ์™œ ์žˆ๋Š”๊ฑฐ์ง€?
์ฐพ์•„๋ณด๋‹ˆ ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ์‚ฌ์šฉ๋˜์–ด ๋ณต์žกํ•œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” (<Button variant={} size={} disabled={} onClick={} ...... />) ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š”๊ฒƒ์ด ์ข‹๊ณ  ๊ทธ ์™ธ์˜ ๋Œ€๋ถ€๋ถ„์˜ ๋ Œ๋”๋ง ์ปดํฌ๋„ŒํŠธ๋“ค์€ ์‚ฌ์‹ค ํ†ตํ•ฉํ…Œ์ŠคํŠธ์— ๊ฒ์ฆํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

// ProductINfoTable.spec.jsx
beforeEach(() => {
  mockUseUserStore({user: {id: 1}});
  mockUseCartStore({
    cart: {
      6: {
        id: 6,
        title: 'Handmade Cotton Fish',
        price: 809,
        description:
          'The slim & simple Maple Gaming Keyboard from Dev Byte comes with a sleek body and 7- Color RGB LED Back-lighting for smart functionality',
        images: [
          'https://user-images.githubusercontent.com/35371660/230712070-afa23da8-1bda-4cc4-9a59-50a263ee629f.png',
        ],
        count: 3,
      },
      7: {
        id: 7,
        title: 'Awesome Concrete Shirt',
        price: 442,
        description:
          'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
        images: [
          'https://user-images.githubusercontent.com/35371660/230762100-b119d836-3c5b-4980-9846-b7d32ea4a08f.png',
        ],
        count: 4,
      },
    },
    totalCount: 7,
    totalPrice: 500,
  });
});

์ด๋ ‡๊ฒŒ ์…‹์—…์„ ํ†ตํ•ด ์ƒํƒœ๋ฅผ ๋ชจํ‚นํ•œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ๋“ค์„ ์ง„ํ–‰ํ•˜๋ฉด ๋œ๋‹ค.

it('์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ํฌํ•จ๋œ ์•„์ดํ…œ๋“ค์˜ ์ด๋ฆ„, ์ˆ˜๋Ÿ‰, ํ•ฉ๊ณ„๊ฐ€ ์ œ๋Œ€๋กœ ๋…ธ์ถœ๋œ๋‹ค', async () => {
  await render(<ProductInfoTable />);

  const [firstItem, secondItem] = screen.getAllByRole('row');

  expect(
    within(firstItem).getByText('Handmade Cotton Fish'),
  ).toBeInTheDocument();
  expect(within(firstItem).getByRole('textbox')).toHaveValue('3');
  expect(within(firstItem).getByText('$2,427.00')).toBeInTheDocument();

  expect(
    within(secondItem).getByText('Awesome Concrete Shirt'),
  ).toBeInTheDocument();
  expect(within(secondItem).getByRole('textbox')).toHaveValue('4');
  expect(within(secondItem).getByText('$1,768.00')).toBeInTheDocument();
});

getBy, queryBy

it('ํŠน์ • ์•„์ดํ…œ์˜ ์‚ญ์ œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ๊ฒฝ์šฐ ํ•ด๋‹น ์•„์ดํ…œ์ด ์‚ฌ๋ผ์ง„๋‹ค', async () => {
  const { user } = await render(<ProductInfoTable />);
  const [, secondItem] = screen.getAllByRole('row');
  const deleteButton = within(secondItem).getByRole('button');

  expect(screen.getByText('Awesome Concrete Shirt')).toBeInTheDocument();

  await user.click(deleteButton);

  expect(screen.queryByText('Awesome Concrete Shirt')).not.toBeInTheDocument();
});

getByText๋Š” ๋”์— ํ•ด๋‹น ํ…์ŠคํŠธ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ์‚ญ์ œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ•ด๋‹น ์š”์†Œ๊ฐ€ ์‚ฌ๋ผ์ ธ์•ผํ•˜๋Š”๋ฐ ์‚ฌ๋ผ์ง„๊ฒƒ์„ ํ™•์ธํ•˜๋ ค๋ฉด getBy๊ฐ€ ์•„๋‹Œ queryBy ๋งค์ฒ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.

๊ณต์‹๋ฌธ์„œ์˜ ์„ค๋ช…

  • getBy...: Returns the matching node for a query, and throw a descriptive error if no elements match or if more than one match is found (use getAllBy instead if more than one element is expected).
  • queryBy...: Returns the matching node for a query, and return null if no elements match. This is useful for asserting an element that is not present. Throws an error if more than one match is found (use queryAllBy instead if this is OK).
    getBy๋Š” ์š”์†Œ๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๋ฉด ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๋ฐ˜๋ฉด queryBy๋Š” null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์™œ queryBy ๋กœ ํ†ต์ผํ•˜์ง€ ์•Š๊ณ  getBy ๋งค์ฒ˜๋„ ์‚ฌ์šฉํ• ๊นŒ?

5. msw๋กœ API ๋ชจํ‚นํ•˜๊ธฐ

Mock Service Worker
๊ฐ•์˜์—์„œ๋Š” ๋„ˆ๋ฌด ๊ฐ„๋‹จํ•˜๊ฒŒ๋งŒ ์„ค๋ช…ํ•˜๊ณ  ๋„˜์–ด๊ฐ€์„œ ๋”ฐ๋กœ ์ฐพ์•„๋ดค๋‹ค.1 2

๊ธฐ์กด ๋ชจํ‚น ๋ฐฉ์‹

์‡ผํ•‘๋ชฐ์„ ๊ฐœ๋ฐœ์ค‘์ด๋ผ๋ฉด, ์ œํ’ˆ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์–ด์•ผ ui๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ œํ’ˆ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๋Š” ๋‹น์—ฐํžˆ ์„œ๋ฒ„์— ์žˆ๋‹ค. ์ด ์„œ๋ฒ„ ๋ฐฑ์—”๋“œ๊ฐ€ ์•„์ง ๊ฐœ๋ฐœ๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ์‚ฌ์œ ๋กœ ์ธํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ์—๋Š” ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ?

๊ฐ€์žฅ ๋จผ์ € ๋– ์˜ค๋ฅด๋Š” ์ƒ๊ฐ์€ ๊ทธ๋ƒฅ ๋ชจํ‚น์šฉ ๋ฐ์ดํ„ฐ๋ฅผ ์•ฑ ์•ˆ์— ์ง์ ‘ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋ ‡๊ฒŒ๋Š” UI๊ตฌํ˜„๊นŒ์ง€๋Š” ๋ฌธ์ œ ์—†์ด ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ์„œ๋ฒ„์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” api๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€๋„ ๊ฒ€์ฆ์„ ํ•ด์•ผํ•œ๋‹ค๋ฉด?

๋จผ์ € ์ž„์˜์˜ ๋ชจํ‚น ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์ˆ˜ ์žˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ง์ ‘ ์„œ๋ฒ„์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ์„œ๋น„์Šค ๋กœ์ง์„ ๋ณ€๊ฒฝํ•  ํ•„์š” ์—†์ด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๋ฐฉ๋ฒ•์€ ๋น„์šฉ(์‹œ๊ฐ„, ์ธ๋ ฅ ๋“ฑ)์ด ๋งŽ์ด ์†Œ๋ชจ๋˜๋Š” ๋ฐฉ๋ฒ•์ด๋ผ ์„ ํ˜ธ๋˜์ง€ ์•Š๋Š”๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๋ชจํ‚น ์„œ๋ฒ„์˜ ๋‹จ์ ์€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ต๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.
์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๊ณ , ์–ด๋–ค ์˜ค๋ฅ˜์ธ์ง€, ๋กœ๋”ฉ์ด ๊ธธ์–ด์ง€๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ•˜๋Š”์ง€ ๋“ฑ๋“ฑ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๊ณผ ๊ด€๋ จ๋œ ์—ฌ๋Ÿฌ ui์™€ ๋กœ์ง์„ ๊ฐœ๋ฐœํ•˜๋ ค๋ฉด ๊ฐœ๋ฐœ์ž๊ฐ€ ์‰ฝ๊ฒŒ ์„œ๋ฒ„ ์‘๋‹ต ์ƒํƒœ๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์–ด์•ผํ•˜๋Š”๋ฐ, ๋ชจํ‚น ์„œ๋ฒ„ ์‚ฌ์šฉ์‹œ์—๋Š” ์„œ๋ฒ„์˜ ์ฝ”๋“œ๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ํ˜น์€ ๊ธฐํƒ€ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

MSW

MSW ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์ด๋Ÿฐ ๋‹จ์ ์„ ์†์‰ฝ๊ฒŒ ํ•ด๊ฒฐํ•ด์ค€๋‹ค.
์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์—ญํ• ์€, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ „์†กํ•˜๋Š” api ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„์„œ ๋ชจํ‚น๋œ ์‘๋‹ต์„ ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค.
๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋– ๋‚œ ๋’ค์— ์‘๋‹ต์ด ์˜ค๋Š”๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ๊ณผ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์†์‰ฝ๊ฒŒ ์‘๋‹ต์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์–ด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ๊ฐœ๋ฐœํ•  ๋•Œ ์šฉ์ดํ•˜๋‹ค.

์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋ธŒ๋ผ์šฐ์ €์˜ Service Worker๋ผ๋Š” ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•ด ์ด๋Ÿฐ ํŽธ๋ฆฌ๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ๋‹ค.
์ผ๋‹จ Worker๋ผ๋Š” ๊ฒƒ์ด ์žˆ๋Š”๋ฐ, ์ด๊ฒƒ์€ ๊ฐ„๋‹จํžˆ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์›น ์•ฑ์„ ์‹คํ–‰ํ•˜๊ณ  ์žˆ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์•„๋‹Œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฌธ์„œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  Service Worker๋Š” ์•ฑ๊ณผ ๋ธŒ๋ผ์šฐ์ € ๊ทธ๋ฆฌ๊ณ  ๋„คํŠธ์›Œํฌ ์‚ฌ์ด์—์„œ ํ”„๋ก์‹œ ์„œ๋ฒ„ ์—ญํ• ์„ ํ•˜๋Š” Worker๋ฅผ ๋งํ•œ๋‹ค.

๊ตฌ์ฒด์ ์ธ ์‚ฌ์šฉ๋ฒ•์€ ๋‹ค์Œ ๊ฐ•์˜์—์„œ ๋‹ค๋ฃฌ๋‹ค.

6. RTL ๋น„๋™๊ธฐ ์œ ํ‹ธ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•œ ๋…ธ์ถœ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

์ด์ œ ProductList ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” api(useProducts)๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•ด์„œ ํ™”๋ฉด์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ Œ๋”๋ง ๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

it('๋กœ๋”ฉ์ด ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ๋ชจ๋‘ ๋…ธ์ถœ๋œ๋‹ค', async () => {
  await render(<ProductList limit={PRODUCT_PAGE_LIMIT} />);

  const productCards = screen.getByTestId('product-card'); // ์‚ฌ์ „์— ์ •์˜ํ•จ

  expect(productCards).toHaveLength(PRODUCT_PAGE_LIMIT);

  productCards.forEach((el, index) => {
    const productCard = within(el);
    const product = data.products[index];

    expect(productCard.getByText(product.title)).toBeInTheDocument();
    expect(productCard.getByText(product.category.name)).toBeInTheDocument();
    expect(
      productCard.getByText(formatPrice(product.price)),
    ).toBeInTheDocument();
    expect(
      productCard.getByRole('button', { name: '์žฅ๋ฐ”๊ตฌ๋‹ˆ' }),
    ).toBeInTheDocument();
    expect(
      productCard.getByRole('button', { name: '๊ตฌ๋งค' }),
    ).toBeInTheDocument();
  });
});

getByTestId : ํ”„๋กœ๋•ํŠธ์นด๋“œ ์ปดํฌ๋„ŒํŠธ๋Š” ์ง€๊ธˆ๊นŒ์ง€ ์‚ฌ์šฉํ•ด์˜จ ๋งค์ฒ˜ getByText๋‚˜ getByRole๋ฅผ ํ†ตํ•ด ํ™•์ธํ•˜๊ธฐ ํž˜๋“ค๊ธฐ ๋•Œ๋ฌธ์— ๋ฏธ๋ฆฌ ProductCard์ปดํฌ๋„ŒํŠธ์˜ ํ…Œ์ŠคํŠธ ์•„์ด๋””๋ฅผ ์ง€์ •ํ•ด์„œ ๊ทธ๊ฒƒ์œผ๋กœ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค.
- ์ปดํฌ๋„ŒํŠธ์˜ ์†์„ฑ์— ์ด๋ ‡๊ฒŒ ์ถ”๊ฐ€ํ•ด์„œ ์ •์˜ํ–ˆ๋‹ค - data-testid="product-card"

๊ทธ๋Ÿฐ๋ฐ ์œ„์˜ ํ…Œ์ŠคํŠธ์—๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š”๋ฐ, getByTestId ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด ๋น„๋™๊ธฐ(Promise) ์ฝ”๋“œ๋Š” ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” API ํ˜ธ์ถœ์€ ๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•˜๊ณ  promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— getBy๋กœ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†๊ฒŒ ๋˜๋Š”๊ฒƒ์ด๋‹ค.
๋Œ€์‹ ์— findBy... ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ •ํ•ด์ง„ ์‹œ๊ฐ„๋™์•ˆ ์—ฌ๋Ÿฌ๋ฒˆ ์žฌ์‹œ๋„๋ฅผ ๋ฐ˜๋ณตํ•ด api ์‘๋‹ต์ด ์™„๋ฃŒ๋˜์–ด ์™„์ „ํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ†ตํ•ฉํ…Œ์ŠคํŠธ์—์„œ์˜ ์ŠคํŒŒ์ด ํ•จ์ˆ˜

๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ŠคํŒŒ์ด ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ํ•จ์ˆ˜์˜ ํ˜ธ์ถœ ์—ฌ๋ถ€์™€ ์ „๋‹ฌ๋˜๋Š” ์ธ์ž๋“ค์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์—์„œ๋„ ์ŠคํŒŒ์ด ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์ˆ˜์—…์—์„œ ์‚ฌ์šฉํ•œ ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

it('์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ "์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ ์™„๋ฃŒ!" toast๋ฅผ ๋…ธ์ถœํ•˜๋ฉฐ, addCartItem ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.', async () => {
    const addCartItemFn = vi.fn();
    mockUseCartStore({ addCartItem: addCartItemFn });

    const { user } = await render(<ProductList limit={PRODUCT_PAGE_LIMIT} />);

    await screen.findAllByTestId('product-card');

    // ์ฒซ๋ฒˆ์งธ ์ƒํ’ˆ์„ ๋Œ€์ƒ์œผ๋กœ ๊ฒ€์ฆํ•œ๋‹ค.
    const productIndex = 0;
    const product = data.products[productIndex];
    await user.click(
      screen.getAllByRole('button', { name: '์žฅ๋ฐ”๊ตฌ๋‹ˆ' })[productIndex],
    );

    expect(addCartItemFn).toHaveBeenNthCalledWith(1, product, 10, 1);
    expect(
      screen.getByText(`${product.title} ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ ์™„๋ฃŒ!`),
    ).toBeInTheDocument();
  });
});

addCartItem์ด๋ผ๋Š” ์Šคํ† ์–ด ์•ก์…˜์„ ์ŠคํŒŒ์ดํ•จ์ˆ˜๋กœ ๋Œ€์ฒดํ•˜๊ณ  ์žˆ๋‹ค. src/utils/test/mockZustandStore.jsx ์— ์ฃผ์Šคํƒ ๋“œ ๋ชจํ‚น ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ด ๋‘์—ˆ๋Š”๋ฐ

// ProductList.jsx
const { addCartItem } = useCartStore(state => pick(state, 'addCartItem'));
...

const handleClickCart = (ev, product) => {
	ev.stopPropagation();
	if (isLogin) {
	  addCartItem(product, user.id, 1);
	  toast.success(`${product.title} ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ ์™„๋ฃŒ!`, { id: TOAST_ID });
	} else {
	  navigate(pageRoutes.login);
	}
};

ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ๋ Œ๋”๋ง ๋œ ์ปดํฌ๋„ŒํŠธ๋Š” ์ € handleClickCart๊ฐ€ ์‹คํ–‰๋  ๋•Œ ์ฃผ์Šคํƒ ๋“œ์— ์ •์˜๋˜์–ด ์žˆ๋Š” ๊ธฐ์กด์˜ addCartItem์ด ์•„๋‹Œ ์ŠคํŒŒ์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.