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

date
2025-10-19
order
3

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

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

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

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

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

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

์œ„ ๋‘ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด ๊ฐ๊ฐ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค. ์™œ CartTable ์ปดํฌ๋„ŒํŠธ์— ๋กœ์ง์„ ์‘์ง‘์‹œ์ผœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ•˜์ง€ ์•Š๊ณ  ๊ตณ์ด ๋‘๊ฐœ๋กœ ๋‚˜๋ˆด์„๊นŒ?

zustand ๋ชจํ‚น

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

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

// __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()));
});

๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์˜ ๊ธ€๋กœ๋ฒŒ ์„ค์ •์„ ํ•˜๋Š” ์…‹์—…ํŒŒ์ผ(์˜ˆ์ œ์—์„œ๋Š” setupTests.js)์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•œ๋‹ค.

import '@testing-library/jest-dom/vitest'

vi.mock('zustand') // to make it work like Jest (auto-mocking)

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ zustand๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ์—๋Š” __mocks__/zustand.js์— ์ž‘์„ฑ๋œ ์ฝ”๋“œ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.

๋ชจํ‚น ์œ ํ‹ธ ํ•จ์ˆ˜

์œ„์˜ __mocks__/zustand.js ํŒŒ์ผ์„ ๋ณด๋ฉด beforeEach๋กœ ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์‹œ์ž‘ ์ „์— ์Šคํ† ์–ด๋ฅผ ์ดˆ๊ธฐํ™” ํ•œ๋‹ค.

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())ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.)

์‹ค์ œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ์ด๋ ‡๊ฒŒ ํ˜ธ์ถœํ•˜์—ฌ ์ƒํƒœ๋ฅผ ์„ค์ •ํ•œ๋‹ค.

mockUseUserStore({ user: { id: 10 } });