๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ

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

๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ์„ ์ •ํ•˜๊ธฐ

์ €๋ฒˆ์‹œ๊ฐ„์— ๋ณด์•˜๋˜ ๋‹จ์œ„ํ…Œ์ŠคํŠธ๊ฐ€ ๋ฌด์—‡์ธ๊ฐ€ ๋Œ์•„๋ณด์ž๋ฉด, ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์™€์˜ ์ƒํ˜ธ์ž‘์šฉ์ด ์•„๋‹Œ ๋‹จ์ผ ์ปดํฌ๋„ŒํŠธ๋ฅผ, ๊ทธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์˜ˆ์ƒ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ…Œ์ŠคํŠธ์ด๋‹ค.
๊ทธ๋ž˜์„œ Atomic ์ปดํฌ๋„ŒํŠธ๋ผ๊ณ  ๋ถˆ๋ฆฌ๋Š” ๊ฒƒ๋“ค์ด ๋‹จ์œ„ํ…Œ์ŠคํŠธ์˜ ์ฃผ ๋Œ€์ƒ์ด๋ผ๊ณ  ํ–ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋ฐ˜๋ฉด ๋˜ ๋„ˆ๋ฌด ๋‹จ์ˆœํ•œ ๊ฒฝ์šฐ(๋‹จ์ˆœํ•œ ui ๋ Œ๋”๋ง, ๊ฐ„๋‹จํ•œ ๋กœ์ง)์—๋Š” ์ผ์ผ์ด ๋‹จ์œ„ํ…Œ์ŠคํŠธ๋ฅผ ๊ฑฐ์น  ํ•„์š” ์—†์ด ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋“ค๊ณผ ํ•จ๊ป˜ (์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ) ํ†ตํ•ฉํ…Œ์ŠคํŠธ์—์„œ ๊ฒ€์ฆํ•˜๋Š”๊ฒƒ์œผ๋กœ๋„ ์ถฉ๋ถ„ํ•˜๋‹ค.

๋ฆฌ์•กํŠธ ํ›…, ์œ ํ‹ธํ•จ์ˆ˜

์ปดํฌ๋„ŒํŠธ ์™ธ์—๋„ ๋ฆฌ์•กํŠธ ํ›…๊ณผ ์œ ํ‹ธ ํ•จ์ˆ˜๋„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์˜ ๋Œ€์ƒ์ด ๋œ๋‹ค. (๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๊ฒ€์ฆํ•˜๊ธฐ ๋งค์šฐ ์ ํ•ฉํ•˜๋‹ค)

์œ ํ‹ธ ํ•จ์ˆ˜๋Š” ๋ณดํ†ต ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ณตํ†ต์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ํ•จ์ˆ˜๋“ค์ด๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•๋“ค์„ ๊ฐ€์ง€๊ณ ์žˆ๋‹ค :

  • ์ˆœ์ˆ˜ ํ•จ์ˆ˜ : ํ•จ์ˆ˜ ์™ธ๋ถ€์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋“ฑ, ์™ธ๋ถ€์™€ ์ƒํ˜ธ์‚ญ์šฉ ํ•˜์ง€ ์•Š๋Š” ํ•จ์ˆ˜. (๋ถˆ๋ณ€ ์ƒ์ˆ˜ ๋‹จ์ˆœ ์ฐธ์กฐ๋Š” ์ œ์™ธ)
  • UI ๋กœ์ง๊ณผ ๋ฌด๊ด€
  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง : ๊ณ„์‚ฐ, ๋ณ€ํ™˜, ๊ฒ€์ฆ ๋“ฑ์˜ ์ˆœ์ˆ˜ํ•œ ๋กœ์ง

๋ชจ๋“ˆ ๋ชจํ‚น

shopping-mall-unit-test ๋ธŒ๋žœ์น˜์—์„œ EmptyNotice์™€ NotFoundPage,ErrorPage ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ•œ๋‹ค.

  • EmptyNotice : ์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ๋น„์–ด์žˆ๋Š” ์ƒํƒœ์—์„œ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€๋กœ ์ด๋™์‹œ์— ์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ๋น„์–ด์žˆ๋‹ค๊ณ  ๋ณด์—ฌ์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ
    • ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ์‹ค ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์กฐํ•ฉ๋˜์–ด ์ด๋ฃจ์–ด์ง„ ์ปดํฌ๋„ŒํŠธ์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ํ™ˆ์œผ๋กœ ์ด๋™ํ•œ๋‹ค๋Š” ํ•˜๋‚˜์˜ ๊ธฐ๋Šฅ๋งŒ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋…๋ฆฝ์ ์ธ ์ปดํฌ๋„ŒํŠธ๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  • NotFoundPage : ์‚ฌ์šฉ์ž๊ฐ€ ์ž˜๋ชป๋œ ๊ฒฝ๋กœ๋กœ ์ด๋™ํ–ˆ์„ ๋•Œ ๋ณด์—ฌ์ฃผ๋Š” ํŽ˜์ด์ง€
  • ErrorPage : ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๋ณด์—ฌ์ฃผ๋Š” ํŽ˜์ด์ง€
    • ๋‘ ์—๋Ÿฌ ํŽ˜์ด์ง€ ๋ชจ๋‘ ํŽ˜์ด์ง€ ๋‹จ์œ„์˜ ํฐ ์ปดํฌ๋„ŒํŠธ์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๊ต‰์žฅํžˆ ๋‹จ์ˆœํ•œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๊ฐ€์ง€๊ณ ์žˆ๊ณ  ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ '๋’ค๋กœ ์ด๋™'๊ณผ ๊ฐ™์€ ๊ฐ„๋‹จํ•œ ๊ธฐ๋Šฅ๋งŒ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์œ„ ์„ธ ์ปดํฌ๋„ŒํŠธ ๋ชจ๋‘ react-router-dom์˜ useNavigate()ํ›…์„ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉํ•˜๊ณ ์žˆ๋‹ค. (์ฆ‰ react-router-dom์— ์˜์กด์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.)
์ด ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ• ๋•Œ๋Š” ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์ œ๋Œ€๋กœ ํ˜ธ์ถœํ•˜๋Š”์ง€๋งŒ ๊ฒ€์ฆํ•˜๋ฉด ๋œ๋‹ค. (์™ธ๋ถ€ ๋ชจ๋“ˆ์˜ ๊ฒ€์ฆ์€ ํ•ด๋‹น ๋ชจ๋“ˆ ์ž์ฒด์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๊ฒ€์ฆํ•  ์ผ์ด์ง€ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋Š” ์—ฌ๊ธฐ์„œ ํ•  ์ผ์€ ์•„๋‹˜)
๊ทธ๋ฆฌ๊ณ  ์ด ์™ธ๋ถ€ ๋ชจ๋“ˆ์˜ ํŠน์ • ๊ธฐ๋Šฅ์„ ์ œ๋Œ€๋กœ ํ˜ธ์ถœํ•˜๋Š”์ง€ ์•Œ์•„๋ณด๊ธฐ ์œ„ํ•ด ๋ชจํ‚น์ด๋ผ๋Š” ๊ฒƒ์„ ํ•œ๋‹ค.

๋ชจํ‚น : ์‹ค์ œ ๋ชจ๋“ˆ,๊ฐ์ฒด์™€ ๋™์ผํ•œ ๋™์ž‘์„ ํ•˜๋„๋ก ๋งŒ๋“  ๋ชจ์˜ ๋ชจ๋“ˆ,๊ฐ์ฒด๋กœ ์‹ค์ œ๋ฅผ ๋Œ€์ฒดํ•˜๋Š” ๊ฒƒ.

  • ๋ชจํ‚น์„ ํ†ตํ•ด ์šฐ๋ฆฌ๋Š” ์™ธ๋ถ€ ๋ชจ๋“ˆ๊ณผ ๊ฒ€์ฆํ•  ๋ชจ๋“ˆ์„ ๋ถ„๋ฆฌํ•ด์„œ ํ•„์š”ํ•œ ๊ฒ€์ฆ๋งŒ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ ์‹ค์ œ ๋ชจ๋“ˆ๊ณผ ์™„์ „ํžˆ ๋™์ผํ•œ ๋ชจ์˜ ๊ฐ์ฒด๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์€ ํฐ ๋น„์šฉ์ด ๋“œ๋Š” ์ž‘์—…์ด๋ฉฐ, (์ •ํ™•ํžˆ ๋ฌด์Šจ๋œป์ผ๊นŒ?)
  • ๋ชจ์˜ ๊ฐ์ฒด๋ฅผ ๋‚จ์šฉํ•˜๋Š” ๊ฒƒ์€ ํ…Œ์ŠคํŠธ ์‹ ๋ขฐ์„ฑ์„ ๋‚ฎ์ถ”๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋‚ณ๋Š”๋‹ค.
const navigateFn = vi.fn();

vi.mock('react-router-dom', async () => {
  const original = await vi.importActual('react-router-dom');

  return { ...original, useNavigate: () => navigateFn };
});

it('Home์œผ๋กœ ์ด๋™ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ํ™ˆ ๊ฒฝ๋กœ๋กœ ์ด๋™ํ•˜๋Š” navigate๊ฐ€ ์‹คํ–‰๋œ๋‹ค', async () => {
  const { user } = await render(<NotFoundPage />);

  const button = await screen.getByRole('button', { name: 'Home์œผ๋กœ ์ด๋™' });

  await user.click(button);

  expect(navigateFn).toHaveBeenNthCalledWith(1, '/', { replace: true });
});

vi.mock()ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด react-router-dom์˜ ๋ชจํ‚น์„ ๋งŒ๋“ ๋‹ค.
๊ทธ๋Ÿฌ๋ฉด <NotFoundPage /> ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ถˆ๋Ÿฌ์˜ค๋Š” ํ•ด๋‹น ๋ชจ๋“ˆ์€ ์ง„์งœ ๋ชจ๋“ˆ์ด ์•„๋‹Œ ์—ฌ๊ธฐ์„œ ๋งŒ๋“ค์–ด์ง„ ๋ชจํ‚น์ด ๋œ๋‹ค.
๊ทธ ์ปดํฌ๋„ŒํŠธ์—์„œ useNavigate()๋ฅผ ํ˜ธ์ถœํ•  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ˜ธ์ถœํ•  ๋•Œ

  const handleClickNavigateHomeButton = () => {
    navigate(pageRoutes.main, { replace: true });
  };
  
  return(
  ...
  <button onClick={handleClickNavigateHomeButton}>Home์œผ๋กœ ์ด๋™</button>
  )

์‹ค์ œ๋กœ ํ˜ธ์ถœ๋˜๋Š” ๊ฒƒ์€ ๋ชจํ‚น์— ๋„ฃ์—ˆ๋˜ navigateFn ์ŠคํŒŒ์ด ํ•จ์ˆ˜๋‹ค.
๊ทธ๋ž˜์„œ expect()๋งค์ฒ˜๋กœ useNavigate()ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด์„œ ์–ด๋–ค ์ธ์ž๋ฅผ ๋„ฃ์—ˆ๋Š”์ง€ ("/", {replace : true})ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

๋ชจํ‚น ์ดˆ๊ธฐํ™”

์œ„์—์„œ useNavigate()์™€ react-router-dom ๋ชจ๋“ˆ์„ ๋ชจํ‚นํ–ˆ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ์ด ๋ชจํ‚น์ด ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ(๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์—์„œ๋Š” ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ๋ชจํ‚น์„ ํ•ด์•ผํ•œ๋‹ค๋˜๊ฐ€)๋ฅผ ์œ„ํ•ด ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰ ํ›„์—๋Š” ๋ชจํ‚น์„ ์ดˆ๊ธฐํ™”ํ•˜๋Š”๊ฒƒ์ด ์ข‹๋‹ค.

//setupTests.js
...

beforeAll(() => {
  server.listen();
});

afterEach(() => {
  server.resetHandlers();
  vi.clearAllMocks();
});

afterAll(() => {
  vi.resetAllMocks();
  server.close();
});

...

clearAllMocks() :

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

resetAllMocks() :

  • ๋ชจํ‚น๋œ ๋ชจ๋“ˆ์˜ ๊ตฌํ˜„์„ ์ดˆ๊ธฐํ™”

๋ฆฌ์•กํŠธ ํ›… ํ…Œ์ŠคํŠธ

๊ฐ•์˜์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ œ์˜ ๊ตฌ์กฐ๋Š” ์ด๋ ‡๋‹ค.
<NavigationBar />์•ˆ์— <ConfirmModal /> ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์ด ๋ชจ๋‹ฌ์„ ํ‘œ์‹œํ•˜๊ณ  ์•ˆํ•˜๊ณ ๋ฅผ isModalOpened๋ผ๋Š” ์ƒํƒœ๋กœ ๊ฒฐ์ •ํ•˜๋Š”๋ฐ,
๊ทธ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ <NavigationBar />์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ useState()ํ›…์„ ์‚ฌ์šฉํ•ด ์ง์ ‘ ๊ด€๋ฆฌํ•˜์ง€ ์•Š๊ณ ,
useConfirmModal()์ด๋ผ๋Š” ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋ถ„๋ฆฌ์‹œ์ผœ ๊ด€๋ฆฌํ•œ๋‹ค.

import { useState } from 'react';

const useConfirmModal = (initialValue = false) => {
  const [isModalOpened, setIsModalOpened] = useState(initialValue);

  const toggleIsModalOpened = () => {
    setIsModalOpened(!isModalOpened);
  };

  return {
    toggleIsModalOpened,
    isModalOpened,
  };
};

export default useConfirmModal;

์ด๋ ‡๊ฒŒ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋ถ„๋ฆฌํ•ด ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉด ๋˜‘๊ฐ™์€ ๊ธฐ๋Šฅ์„ ํ•„์š”๋กœ ํ•˜๋Š” ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด ์ฝ”๋“œ์˜ ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋†’์•„์ง„๋‹ค๋Š” ์žฅ์  ์™ธ์—๋„ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์žฅ์ ์ด ์žˆ๋‹ค.

  • ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋กœ ์ธํ•œ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ
  • ๋กœ์ง ์บก์Аํ™”๋กœ ์ธํ•œ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ ์ฆ๊ฐ€

renderHook

ํ…Œ์ŠคํŒ…๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” API.

๋ฆฌ์•กํŠธ ํ›…์„ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ›…์„ ํ˜ธ์ถœํ•˜๊ณ  ๋‚œ ํ›„์— ํ›…์ด ๋ฐ˜ํ™˜ํ•˜๋Š” isMOdalOpened์™€ ๊ฐ™์€ ์ƒํƒœ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ณ€๊ฒฝ๋˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ๋ฆฌ์•กํŠธ ๊ทœ์น™์ƒ ๋ฆฌ์•กํŠธ ํ›…(๋ฆฌ์•กํŠธ ์ž์ฒด ํ›…, ์ปค์Šคํ…€ ํ›… ๋ชจ๋‘)์€ ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ๋งŒ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.
์ง€๊ธˆ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ณ ์žˆ๋Š” ํŒŒ์ผ์ธ useCOnfirmModal.spec.jsx๋Š” ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์•„๋‹ˆ๊ธฐ๋•Œ๋ฌธ์— (์ผ๋ฐ˜ jsํŒŒ์ผ) ํ›…์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์—†์ง€๋งŒ, ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด renderHook() api๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.

const { result, rerender } = renderHook(useConfirmModal);
// or
const { result } = renderHook(() => useConfirmModal(true));

expect(result.current.isModalOpened).toBe(false);
// or 

result : ํ›…์„ ํ˜ธ์ถœํ•˜์—ฌ ์–ป์€ ๊ฒฐ๊ณผ ๊ฐ’.
rerender : ํ›…์„ ์›ํ•˜๋Š” ์ธ์ž์™€ ํ•จ๊ป˜์ƒˆ๋กœ ํ˜ธ์ถœํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ . (๊ฐ•์˜์—์„œ๋Š” ๋ณด์—ฌ์ฃผ๊ธฐ๋งŒ ํ•˜๊ณ  ์‚ฌ์šฉ์€ ์•ˆํ•œ๋‹ค?)

act ํ•จ์ˆ˜

์ƒํ˜ธ ์ž‘์šฉ(๋ Œ๋”๋ง, ์ดํŽ™๋“œ ๋“ฑ)์„ ํ•จ๊ป˜ ๊ทธ๋ฃนํ™”ํ•˜๊ณ  ์‹คํ–‰ํ•ด ๋ Œ๋”๋ง๊ณผ ์—…๋ฐ์ดํŠธ๊ฐ€ ์‹ค์ œ ์•ฑ์ด ๋™์ž‘ํ•˜๋Š” ๊ฒƒ๊ณผ ์œ ์‚ฌํ•œ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•จ.

  • ์ฆ‰ act๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์˜ ๊ฐ€์ƒ์˜ ๋”(jsdom)์— ์ œ๋Œ€๋กœ ๋ฐ˜์˜๋œ๋‹ค.
    ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•œ ๋’ค ์—…๋ฐ์ดํŠธ ํ•˜๋Š” ์ฝ”๋“œ์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์„๋•Œ ์‚ฌ์šฉ.

๊ทธ๋Ÿฐ๋ฐ ์™œ ์ด์ „ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ(UI ์ปดํฌ๋„ŒํŠธ)์—์„œ๋Š” actํ•จ์ˆ˜๊ฐ€ ์—†์ด ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์—ˆ์„๊นŒ?
์™œ๋ƒํ•˜๋ฉด, render ํ•จ์ˆ˜ ์ž์ฒด์—์„œ ๋‚ด๋ถ€์ ์œผ๋กœ act ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ.

// ์‹คํŒจ
it('ํ›…์˜ toggleIsModalOpened()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด isModalOpened ์ƒํƒœ๊ฐ€ toggle๋œ๋‹ค.', () => {
  const { result } = renderHook(useConfirmModal);
  result.current.toggleIsModalOpened();
  expect(result.current.isModalOpened).toBe(true);
});

//์„ฑ๊ณต
it('ํ›…์˜ toggleIsModalOpened()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด isModalOpened ์ƒํƒœ๊ฐ€ toggle๋œ๋‹ค.', () => {
  const { result } = renderHook(useConfirmModal);
  act(() => {
    result.current.toggleIsModalOpened();
  });
  expect(result.current.isModalOpened).toBe(true);
});

ํƒ€์ด๋จธ ํ…Œ์ŠคํŠธ

๋””๋ฐ”์šด์Šค : ํŠน์ • ํ•จ์ˆ˜์˜ ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ์ œํ•œ

  • ์ฃผ๋กœ ์Šคํฌ๋กค์ฒ˜๋Ÿผ ๋Œ€๋Ÿ‰์˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ
export const debounce = (fn, wait) => {
  let timeout = null;

  return (...args) => {
    const later = () => {
      timeout = -1;
      fn(...args);
    };

    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = window.setTimeout(later, wait);
  };
};

ํƒ€์ด๋จธ ๋ชจํ‚น

beforeEach(() => {
    vi.useFakeTimers();
});
afterEach(() => {
	vi.useRealTimers();
});
  
it('์—ฐ์ด์–ด ํ˜ธ์ถœํ•ด๋„ ๋งˆ์ง€๋ง‰ ํ˜ธ์ถœ ๊ธฐ์ค€์œผ๋กœ ์ง€์ •๋œ ํƒ€์ด๋จธ ์‹œ๊ฐ„์ด ์ง€๋‚œ ๊ฒฝ์šฐ์—๋งŒ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.', () => {
    const spy = vi.fn();

    const debouncedFn = debounce(spy, 300);

    debouncedFn();
    vi.advanceTimersByTime(200);
    debouncedFn();
    vi.advanceTimersByTime(100);
    debouncedFn();
    vi.advanceTimersByTime(200);
    debouncedFn();
    vi.advanceTimersByTime(300);
    debouncedFn();

    expect(spy).toHaveBeenCalledTimes(1);
});

userEvent์™€ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ

fireEvent : userEvent์ฒ˜๋Ÿผ ๋ฆฌ์•กํŠธ ํ…Œ์ŠคํŒ… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ DOM ์ด๋ฒคํŠธ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ•˜๊ธฐ ์œ„ํ•ด ์ œ๊ณตํ•˜๋Š” api

userEvent vs fireEvent ?

fireEvent๋Š” "ํ•ด๋‹น ์ด๋ฒคํŠธ๋งŒ ๋””์ŠคํŒจ์น˜" ํ•œ๋‹ค.

์ด๊ฒŒ ๋ฌด์Šจ๋œป์ด๋ƒ๋ฉด, userEvent๋กœ ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค๋ฉด mouseOver -> mouseMove -> mouseDown -> mouseUp -> click ์˜ ์ˆœ์„œ๋Œ€๋กœ ์‹ค์ œ ์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ์šฉํ•  ๋•Œ ์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€๋งŒ,
fireEvent๋Š” ๋‹จ์ˆœํžˆ click ์ด๋ฒคํŠธ๋งŒ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค๋Š” ๊ฒƒ.

์ด์ „ ์‹œ๊ฐ„์— ํ…์ŠคํŠธ ์ธํ’‹์„ ํด๋ฆญํ•˜๋ฉด onFocus๊ฐ€ ํ˜ธ์ถœ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ํ–ˆ๋Š”๋ฐ, ์ด ๋•Œ ์‚ฌ์šฉํ•œ๊ฒƒ์€ userEvent.
๋งŒ์•ฝ fireEvent๋กœ ํ…์ŠคํŠธ ์ธํ’‹์„ ํด๋ฆญํ•œ๋‹ค๋ฉด onFocus๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค.

๊ทธ๋ž˜์„œ ๋˜๋„๋ก userEvent ์‚ฌ์šฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด userEvent๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

  • ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ : ex.scroll

๋‹จ์œ„ ํ…Œ์ŠคํŠธ์˜ ํ•œ๊ณ„

์ €๋ฒˆ ์žฅ์—์„œ ๋ฐฐ์› ๋“ฏ, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋Š” ์•ฑ์—์„œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ฐ€์žฅ ์ž‘์€ ๋‹จ์œ„์˜ ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
์ฆ‰, ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ์ด ์กฐํ•ฉ๋˜์–ด ์ƒํ˜ธ์ž‘์šฉํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜๋ฅผ ์ฐพ์•„๋‚ด๊ณ  ๊ฒ€์ฆํ•˜์ง€ ๋ชปํ•˜๋Š” ์•„์ฃผ ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ์ด๋‹ค.

๊ทธ๋ž˜์„œ ์ˆ˜ํ–‰ํ•˜๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ตํ•ฉํ…Œ์ŠคํŠธ, E2E ํ…Œ์ŠคํŠธ, ์‹œ๊ฐ์  ํ…Œ์ŠคํŠธ ๋“ค์ด๋‹ค.
ํ†ตํ•ฉํ…Œ์ŠคํŠธ๋ž€ ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ์ด ์กฐํ•ฉ๋˜์—ˆ์„ ๋•Œ ๊ธฐ๋Šฅํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ฒ€์ฆํ•˜๋Š” ์šฉ๋„๋กœ ์ˆ˜ํ–‰๋˜๋Š” ํ…Œ์ŠคํŠธ๋‹ค.