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

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

1. ProductFilter ์ปดํฌ๋„ŒํŠธ

์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์ƒํ’ˆ๋ช…, ์นดํ…Œ๊ณ ๋ฆฌ, ๊ฐ€๊ฒฉ ๋ฒ”์œ„ ๋“ฑ์˜ ๊ฒ€์ƒ‰ ์กฐ๊ฑด์„ ์„ ํƒํ•ด ํ™”๋ฉด์— ๋ณด์ผ ์ƒํ’ˆ๋“ค์˜ ๋ชฉ๋ก์„ ๋ณ€๊ฒฝํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋‹ค.

๋ Œ๋”๋ง ๋˜๋Š” ์š”์†Œ๋กœ๋Š” ๋‹ค์Œ์˜ ๊ฒƒ๋“ค์ด ์žˆ๋‹ค :

  • ์ƒํ’ˆ๋ช… ํ…์ŠคํŠธ ์ธํ’‹
  • ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ ๋ผ๋””์˜ค ๋ฒ„ํŠผ
  • ๊ฐ€๊ฒฉ ๋ฒ”์œ„ ํ…์ŠคํŠธ ์ธํ’‹ (์ตœ์†Œ, ์ตœ๋Œ€)

์ด ์ปดํฌ๋„ŒํŠธ์˜ ๋„๋ฉ”์ธ์€ ๋ฌด์—‡์ด ์žˆ์„๊นŒ? ์ฆ‰ ๋ฌด์—‡์„ ๊ฒ€์ฆํ•ด์•ผ ํ• ๊นŒ? (๋ Œ๋”๋ง ์—ฌ๋ถ€๋Š” ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋‹ค๋ฃจ๊ธฐ ๋•Œ๋ฌธ์— ํ•จ์ˆ˜์˜ ํ˜ธ์ถœ ์—ฌ๋ถ€๋งŒ ๊ฒ€์ฆํ•œ๋‹ค.)

  • ์ƒํ’ˆ๋ช…์„ ์ž…๋ ฅํ•˜๋ฉด ๊ทธ๊ฒƒ์ด ์ ์šฉ ๋˜๋Š”๊ฐ€?
  • ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ํด๋ฆญํ•˜๋ฉด ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ์ด ์ ์šฉ๋˜๋Š”๊ฐ€?
    • ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ ์ด์ „์— ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์ œ๋Œ€๋กœ ๋ Œ๋”๋ง ๋˜๋Š”์ง€๋ถ€ํ„ฐ ๊ฒ€์ฆํ•ด์•ผ ํ•œ๋‹ค.
  • ๊ฐ€๊ฒฉ ๋ฒ”์œ„ ์„ค์ •์ด ์ ์šฉ ๋˜๋Š”๊ฐ€?

์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์œ„์˜ ๊ธฐ๋Šฅ๋“ค์„ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•จ์ˆ˜๋“ค๋กœ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋‹ค.
- setTitle
- setCategoryId
- setMinPrice ์™€ setMaxPrice

์œ„์˜ ํ•จ์ˆ˜๋“ค์€ ๋ชจ๋‘ useFilterStore ์ฃผ์Šคํƒ ๋“œ ์Šคํ† ์–ด ์•ก์…˜๋“ค์ด๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์šฐ๋ฆฌ๋Š” useFilterStore๋ฅผ ๋ชจํ‚นํ•ด์•ผ ํ•จ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๊ทธ ์ „์— ํ™•์ธํ•ด์•ผ ํ•˜๋Š”๊ฒƒ์ด ์žˆ๋Š”๋ฐ ๋ฐ”๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” api useCategories์— ๋Œ€ํ•œ ๋ชจํ‚น์ด๋‹ค.

api ๋ชจํ‚น - MSW

์—ฌ๊ธฐ์„œ ์„ ํƒ๋  ์นดํ…Œ๊ณ ๋ฆฌ๋“ค์˜ ๋ชฉ๋ก์€ useCategories ํ›…์„ ์‚ฌ์šฉํ•ด ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜จ๋‹ค.
(ProductFilter ๋‚ด๋ถ€์˜ CategoryRadioGroup ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ์ค‘)

const useCategories = options =>
  useFetch({ url: pathToUrl(apiRoutes.categories), options });

์›๋ž˜๋ผ๋ฉด ์ด ์ฝ”๋“œ๋Š” ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒ ์ง€๋งŒ, msw๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋Œ€์‹ src/__mocks__/handler.js ์— ์ ํžŒ๋Œ€๋กœ ์ž‘๋™ํ•œ๋‹ค.
์˜ˆ์ œ ์ฝ”๋“œ์—์„œ๋Š” src/__mocks__/response ์— ์ž‘์„ฑํ•œ json ํŒŒ์ผ์„ ๋ถˆ๋Ÿฌ์™€ ์ „๋‹ฌํ•˜๋Š”๊ฒƒ์œผ๋กœ ๋˜์–ด์žˆ๋‹ค.

// handler.js
import response from '@/__mocks__/response';
...
rest.get(`${API_DOMAIN}${path}`, (_, res, ctx) =>
      res(ctx.status(200), ctx.json(response[path])),
    ),
...

// response/categories.json
[
  {
    "creationAt": "2023-03-25T12:00:00.000Z",
    "id": 1,
    "image": "https://api.lorem.space/image/fashion",
    "name": "category1",
    "updatedAt": "2023-03-25T12:00:00.000Z"
  },
  ...
it('์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜จ ํ›„ ์นดํ…Œ๊ณ ๋ฆฌ ํ•„๋“œ์˜ ์ •๋ณด๋“ค์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ Œ๋”๋ง๋œ๋‹ค.', async () => {
  await render(<ProductFilter />);

  expect(await screen.findByLabelText('category1')).toBeInTheDocument();
  expect(await screen.findByLabelText('category2')).toBeInTheDocument();
  expect(await screen.findByLabelText('category3')).toBeInTheDocument();
});

์œ„์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด api๊ฐ€ json์— ์ž‘์„ฑ๋˜์–ด ์žˆ๋Š”๋Œ€๋กœ ์ž˜ ๊ฐ€์ ธ์˜จ๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์Šคํ† ์–ด ๋ชจํ‚น

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

beforeEach(() => {
  mockUseFilterStore({
    setMinPrice: setMinPriceFn,
    setMaxPrice: setMaxPriceFn,
    setTitle: setTitleFn,
  });
});

it('์ƒํ’ˆ๋ช…์„ ์ˆ˜์ •ํ•˜๋Š” ๊ฒฝ์šฐ setTitle ์•ก์…˜์ด ํ˜ธ์ถœ๋œ๋‹ค.', async () => {
  ...
  expect(setTitleFn).toHaveBeenCalledWith('test');
});

it('์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ํด๋ฆญ ํ•  ๊ฒฝ์šฐ์˜ ํด๋ฆญํ•œ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์ฒดํฌ๋œ๋‹ค.', async () => {
  ...
  expect(category3).toBeChecked();
});

it('์ตœ์†Œ ๊ฐ€๊ฒฉ ๋˜๋Š” ์ตœ๋Œ€ ๊ฐ€๊ฒฉ์„ ์ˆ˜์ •ํ•˜๋ฉด setMinPrice๊ณผ setMaxPrice ์•ก์…˜์ด ํ˜ธ์ถœ๋œ๋‹ค.', async () => {
  ...
  expect(setMinPriceFn).toHaveBeenCalledWith('1');
  ...
  expect(setMaxPriceFn).toHaveBeenCalledWith('2');
});