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

10/7/2025

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

์•ฑ์—์„œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ๊ฐ€์žฅ ์ž‘์€ ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ์‹คํ–‰ํ•ด ์˜ˆ์ƒ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ…Œ์ŠคํŠธ.

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

ํ”„๋ก ํŠธ(๋ฆฌ์•กํŠธ)์—์„œ๋Š” ๋ฒ„ํŠผ, ํ…์ŠคํŠธ ์ธํ’‹, ์บ๋Ÿฌ์…€, ์•„์ฝ”๋””์–ธ ๋“ฑ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋‹ค๋ฐ˜์‚ฌ์ธ๋ฐ ์ด๋Ÿฐ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์— ์•„์ฃผ ์ ํ•ฉํ•œ ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. (Atomic ์ปดํฌ๋„ŒํŠธ๋ผ๊ณ  ๋ถ€๋ฅด๊ธฐ๋„ ํ•œ๋‹ค)

AAA ํŒจํ„ด

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ์—๋„ ์—ฌ๋Ÿฌ ํŒจํ„ด์ด ์žˆ๋‹ค. AAA ํŒจํ„ด์ด๋ž€ Arrange-Act-Assert ํŒจํ„ด์˜ ์ค„์ž„๋ง.

Arrange : ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ํ™˜๊ฒฝ ๋งŒ๋“ค๊ธฐ (์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ•˜๋Š” ์ฝ”๋“œ ๋“ฑ)

  • testing-library ์‚ฌ์šฉ
    Act : ํ…Œ์ŠคํŠธํ•  ๋™์ž‘ ๋ฐœ์ƒ
    Assert : ์˜ฌ๋ฐ”๋ฅธ ๋™์ž‘์ด ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ๋˜๋Š” ๋ณ€๊ฒฝ์‚ฌํ•ญ ๊ฒ€์ฆ
  • vitest ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
it('className prop์œผ๋กœ ์„ค์ •ํ•œ css class๊ฐ€ ์ ์šฉ๋œ๋‹ค.', async () => {
  // Arrange
  // - className์„ ์ง€๋‹Œ ์ปดํฌ๋„ŒํŠธ ๋žœ๋”๋ง
  await render(<TextField className={`my-class`} />);
  
  // Act
  // - ํด๋ฆญ์ด๋‚˜ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ, prop ๋ณ€๊ฒฝ ๋“ฑ๋“ฑ์— ๋Œ€ํ•œ ์ž‘์—…
  
  // Assert
  // - ๋ Œ๋”๋ง ํ›„ DOM์— ํ•ด๋‹น class๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ๊ฒ€์ฆ
  expect(screen.getByPlaceholderText('ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.')).toHaveClass(
    'my-class',
  );
});

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ๊ณผ ๋งค์ฒ˜

ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ : ํƒœ์Šคํฌ ๋Ÿฌ๋„ˆ, ์–ด์„ค์…˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, ํ”Œ๋Ÿฌ๊ทธ์ธ ๋“ฑ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ๋ฐ ๊ฒ€์ฆ์— ํ•„์š”ํ•œ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•˜๋Š” ๋„๊ตฌ (Vitest, Cypress ๋“ฑ)

Vitest ์„ค์ •

vite.config.jsํŒŒ์ผ์˜ test ํ•„๋“œ์— ์ •์˜ํ•˜๊ฑฐ๋‚˜, ์ฝ”๋“œ๋Ÿ‰์ด ๋งŽ๋‹ค๋ฉด vitest.config.js๋ผ๋Š” ํŒŒ์ผ์„ ๋งŒ๋“ค๊ธฐ๋„ ํ•œ๋‹ค.

//vite.config.js
export default defineConfig({
  plugins: [react(), eslint({ exclude: ['/virtual:/**', 'node_modules/**'] })],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/utils/test/setupTests.js',
  },
  resolve: {
    alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }],
  },
});



JSDOM

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

<body>
  <div>
    <input
      class="text-input my-class"
      placeholder="ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."
      type="text"
      value=""
    />
  </div>
</body>

it() ํ•จ์ˆ˜

ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๊ธฐ ์œ„ํ•œ ์กฐ๊ฑด์„ ๊ธฐ์ˆ ํ•˜์—ฌ ๊ฒ€์ฆ์„ ์‹คํ–‰ํ•œ๋‹ค.
it ํ•จ์ˆ˜๋Š” ๊ฐ„๋‹จํžˆ test ํ•จ์ˆ˜์˜ alias. test ํ•จ์ˆ˜์™€์˜ ๊ธฐ๋Šฅ์ƒ์˜ ์ฐจ์ด๋Š” ์—†๋‹ค.

describe()

๊ธฐ๋ณธ์ ์œผ๋กœ itํ•จ์ˆ˜๋กœ ์ •์˜๋œ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์€ '๋ฃจํŠธ'์ด๊ธฐ ๋•Œ๋ฌธ์— ์ตœ์ƒ์œ„ ์ปจํ…์ŠคํŠธ์—์„œ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰๋œ๋‹ค.
describe ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ it ํ•จ์ˆ˜๋“ค์„ ๊ทธ๋ฃน์ง€์„ ์ˆ˜ ์žˆ๋‹ค.

๋งค์ฒ˜ Matcher

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” API ์ง‘ํ•ฉ. (์œ„์˜ expect().toHaveClass())

setup ๊ณผ teardown

ํ…Œ์ŠคํŠธ์˜ ์›์น™์ค‘ '๋ชจ๋“  ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค'๋Š” ์›์น™์ด ์žˆ๋‹ค.

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

์ด๋Ÿฐ ๋…๋ฆฝ์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด setup๊ณผ teardown์ด ์‚ฌ์šฉ๋œ๋‹ค.
setup : ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์ „ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ์ž‘์—….
- beforeEach : ํ…Œ์ŠคํŠธ ํŒŒ์ผ ํ˜น์€ ์Šค์ฝ”ํ”„(describeํ•จ์ˆ˜) ๋‚ด์— ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „์— ํ˜ธ์ถœ
- beforeAll : ํŒŒ์ผ ํ˜น์€ ์Šค์ฝ”ํ”„ ๋‚ด์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „ ๋‹จ ํ•œ๋ฒˆ๋งŒ ํ˜ธ์ถœ
teardown : ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•œ ๋’ค ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ์ž‘์—….
- afterEach
- afterAll

React Testing Library์™€ ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ

ํŠน์ • ๊ฐœ๋ฐœ(๋ฆฌ์•กํŠธ ์™ธ ๋ทฐ,์Šค๋ฒจํŠธ์—์„œ๋„), ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ์— ์ข…์†๋˜์ง€๋Š” ์•Š๋Š”๋‹ค.

Testing Library์˜ ํ•ต์‹ฌ ์ฒ ํ•™์€ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธ ํ•˜๋Š”๊ฒƒ.

  • ์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹ : DOM ๋…ธ๋“œ ์กฐํšŒ, ์‚ฌ์šฉ์ž์™€ ๋น„์Šทํ•œ ๋ฐฉ์‹์œผ๋กœ ์ด๋ฒคํŠธ ๋ฐœ์ƒ.
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ์›์น™์ค‘ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑํ•ด์•ผํ•œ๋‹ค๋Š” ์›์น™๊ณผ ์ผ์น˜.
  • getBy api์ค‘ role, label-text, image-alt-text ์ฒ˜๋Ÿผ ์‚ฌ์šฉ์ž ๊ด€์ ์— ๊ฐ€๊นŒ์šด api๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

userEvent

// TextField.spec.jsx
import render from '@/utils/test/render';

it('ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด onChange prop์œผ๋กœ ๋“ฑ๋กํ•œ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค', async () => {
  const { user } = await render(<TextField />);
  const textInput = screen.getByPlaceholderText('์ƒํ’ˆ๋ช…์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.');
  await user.type(textInput, 'test');
  ...
});

// render.jsx
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

export default async component => {
  const user = userEvent.setup();

  return {
    user,
    ...render(component),
  };
};

userEvent.setup() ํ•จ์ˆ˜๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” user๊ฐ์ฒด๋Š” ์‚ฌ์šฉ์ž์˜ ํ–‰๋™์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

spy ํ•จ์ˆ˜

vi.fn() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
์ŠคํŒŒ์ด ํ•จ์ˆ˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ํŠน์ • ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€, ํ•จ์ˆ˜์˜ ์ธ์ž๋กœ ์–ด๋–ค ๊ฒƒ์ด ๋„˜์–ด์™”๊ณ  ์–ด๋–ค ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ฐ’๋“ค์„ ์ €์žฅํ•˜๊ณ  ์žˆ๋‹ค.
์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜๋ฅผ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด ํ•„์ˆ˜๋‹ค.

it('ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด onChange prop์œผ๋กœ ๋“ฑ๋กํ•œ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค', async () => {
  const spy = vi.fn();

  const { user } = await render(<TextField onChange={spy} />);
  const textInput = screen.getByPlaceholderText('ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.');
  await user.type(textInput, 'test');

  expect(spy).toHaveBeenCalledWith('test');
});