๐Ÿ’ป์šฉ๋‡ฝ ๊ฐœ๋ฐœ ๋…ธํŠธ๐Ÿ’ป
article thumbnail
๋ฐ˜์‘ํ˜•

Jest์™€ React testing library๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ„๋‹จํ•œ React app ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ธฐ (feat. TDD)

๐Ÿ“– ๋“ค์–ด๊ฐ€๋ฉฐ

์ตœ๊ทผ 'ํด๋ฆฐ ์ฝ”๋“œ'์ฑ…๊ณผ ์—ฌ๋Ÿฌ ๊ฐœ๋ฐœ ๊ด€๋ จ ์˜์ƒ์ด๋‚˜ ๋ฌธ์„œ๋ฅผ ๋ณด๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์ค‘์š”์„ฑ์„ ๊นจ๋‹ซ๊ฒŒ ๋˜์—ˆ๋‹ค.

์•ž์œผ๋กœ ๊ฐœ๋ฐœ์„ ํ•˜๋ฉด์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•˜๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ†ต๊ณผํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” TDD ๊ฐœ๋ฐœ ๋ฐฉ์‹์œผ๋กœ React์—์„œ Jest์™€ React testing library๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฒ•์„ ๊ณต๋ถ€ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ๊ณต๋ถ€ํ•œ ๋‚ด์šฉ์„ ๋ณต์Šตํ•˜๊ณ  ์ •๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๊ธ€์„ ์ž‘์„ฑํ•ด ๋ณธ๋‹ค.

ํ•ด๋‹น ๊ธ€์—์„œ๋Š” ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์ˆซ์ž๊ฐ€ ์ฆ๊ฐ๋˜๋Š” ๊ฐ„๋‹จํ•œ ์•ฑ์„ TDD ๊ฐœ๋ฐœ ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋งŒ๋“ค์–ด ๋ณผ ๊ฒƒ์ด๋‹ค.

Jest์™€ React testing library

Jest ?

Jest๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์™€ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋‹ค.

๋‹จ์ˆœํ•จ์„ ๊ฐ•์ ์œผ๋กœ ์„ค๊ณ„๋œ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ๊ฐ๊ฐ์˜ ํ…Œ์ŠคํŠธ, ์Šค๋ƒ…์ƒท ๋น„๊ต, mocking, coverage ๋“ฑ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ API๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

React testing library ?

React testing library๋Š” React Component๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด ํŠน๋ณ„ํžˆ ์ œ์ž‘๋œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ์ด๋‹ค. ๊ฐ๊ฐ์˜ ๊ตฌ์„ฑ์š”์†Œ์— ๋Œ€ํ•œ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์„ ํ…Œ์ŠคํŠธํ•˜๊ณ , UI๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿผ ๋‘˜ ์ค‘์— ๋ญ˜ ์“ฐ๋ผ๊ณ ?๐Ÿค”

๋‹ต์€ ๋‘˜ ๋‹ค ํ•„์š”ํ•˜๋‹ค.

Jest๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ฐพ์•„์„œ ์‹คํ–‰ํ•˜๊ณ , ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋Š”์ง€ ๊ฒ€์‚ฌํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ suites, ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ๋“ฑ์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

React testing library๋Š” ์˜ˆ๋ฅผ ๋“ค์–ด, ์œ ์ €๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด div๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ๋“ฑ React ์•ฑ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€์ƒ DOM์„ ์ œ๊ณตํ•ด์ค€๋‹ค.

์ƒˆ ํ”„๋กœ์ ํŠธ ๋งŒ๋“ค๊ธฐ

CRA๋กœ ์ง„ํ–‰ํ•  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— CRA๋ฅผ ์„ค์น˜ํ•ด ์ค€๋‹ค.

npx create-react-app ํด๋”์ด๋ฆ„

์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋์œผ๋ฉด ํ„ฐ๋ฏธ๋„ ์ฐฝ์„ ์—ด๊ณ  npm test ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด ์‹คํ–‰ํ•œ๋‹ค.

npm test

์‹คํ–‰ํ•˜๋ฉด ์ดˆ๊ธฐ์— ์ž‘์„ฑ๋˜์–ด์žˆ๋Š” APP component๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ํŒŒ์ผ์—์„œ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜จ๋‹ค.

App.test.js ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ

CRA๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ตฌ์„ฑํ•˜๋ฉด Jest์™€ React testing library๋Š” ๋‚ด์žฅ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ์„ค์น˜ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.

์š”๊ตฌ ์‚ฌํ•ญ

๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ˆซ์ž๊ฐ€ ์ฆ๊ฐ€ํ•˜๊ณ  ๊ฐ์†Œํ•˜๋Š” Counter App์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ๊ฐํ•ด๋ณด๋ฉด

  • CountView ์ปดํฌ๋„ŒํŠธ: ํ˜„์žฌ ์ˆซ์ž(์นด์šดํŠธ ์ƒํƒœ)๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ
  • CountButtons ์ปดํฌ๋„ŒํŠธ: '+' ๋ฒ„ํŠผ๊ณผ '-'๋ฒ„ํŠผ์ด ์žˆ๊ณ  ๊ฐ๊ฐ์˜ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์ˆซ์ž๊ฐ€ 1์”ฉ ์ฆ๊ฐ€ํ•˜๊ณ  ๊ฐ์†Œํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œํ‚ค๋Š” ์ปดํฌ๋„ŒํŠธ
  • APP ์ปดํฌ๋„ŒํŠธ: ์ „์ฒด์ ์ธ ๋กœ์ง์„ ๋‹ด๋‹น (์นด์šดํŠธ ์ƒํƒœ, ์นด์šดํŠธ์— ๊ด€ํ•œ ํ•จ์ˆ˜)

์ด๋ ‡๊ฒŒ ํ•„์š”ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ, ๋งค์šฐ ๊ฐ„๋‹จํ•œ ์•ฑ์ด๋‹ค.

CountView ์ปดํฌ๋„ŒํŠธ

ํ˜„์žฌ ์นด์šดํŠธ ์ƒํƒœ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” CountView ์ปดํฌ๋„ŒํŠธ๋ถ€ํ„ฐ ์ž‘์„ฑํ•ด๋ณด๊ฒ ๋‹ค.

๋จผ์ € srcํด๋”์— components ํด๋”๋ฅผ ์ƒ์„ฑํ•˜๊ณ  CountView ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ค€๋‹ค.

src/components/CountView.js

function CountView() {
  return <div>CountView</div>;
}

export default CountView;

props๋กœ ๋ฐ›์€ ์ˆซ์ž ๊ด€๋ฆฌ

ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์—๋Š” props๋กœ ๋ฐ›์€ ํ˜„์žฌ count๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์—ญํ• ์„ ํ•  ๊ฒƒ์ด๋‹ค. ์ด์— ๋งž์ถฐ์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

src/components/CountView.test.js

import { render, screen } from '@testing-library/react';
import CountView from './CountView';

describe('<CountView />', () => {
  it('shows the current count state.', () => {
    let sampleCount = 0;
    render(<CountView count={sampleCount} />);
    const initialState = screen.getByText('ํ˜„์žฌ ์ˆซ์ž: 0');

    expect(initialState).toBeInTheDocument();

    sampleCount = 13;
    render(<CountView count={sampleCount} />);
    const countState = screen.getByText('ํ˜„์žฌ ์ˆซ์ž: 13');

    expect(countState).toBeInTheDocument();
  });
});

props๋กœ count๋ฅผ ๋ฐ›์•„์˜ค๋Š” ์ƒํ™ฉ์„ ๋งŒ๋“ค๊ณ  CountView ์ปดํฌ๋„ŒํŠธ์— props๋กœ sampleCount๋ฅผ ๋„˜๊ฒจ์ฃผ์—ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  getByText๋กœ ๋ณด์—ฌ์ค„ ์š”์†Œ๋ฅผ ์„ ํƒํ•˜๋„๋ก ํ•˜๊ณ  ํ™”๋ฉด์— ํ•ด๋‹น ์š”์†Œ๊ฐ€ ์žˆ๋Š”์ง€ toBeInTheDocument()๋ฅผ ํ†ตํ•ด์„œ ํ™•์ธํ•œ๋‹ค.

์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๊ณ  npm test ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด๋ณด๋ฉด ๋‹น์—ฐํžˆ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” ์‹คํŒจํ•˜๊ฒŒ ๋œ๋‹ค.

์ด์ œ ์ด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ํ†ต๊ณผ๋  ์ˆ˜ ์žˆ๋„๋ก CountView ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.

scr/components/CountView.js

function CountView({ count }) {
  return <h1>ํ˜„์žฌ ์ˆซ์ž: {count}</h1>;
}

export default CountView;

์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๊ณ  ๋‹ค์‹œ npm test ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผ๋˜๋Š” ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ฌ ๊ฒƒ์ด๋‹ค.

CountButtons ์ปดํฌ๋„ŒํŠธ

์ด๋ฒˆ์—๋Š” '+' ๋ฒ„ํŠผ๊ณผ '-'๋ฒ„ํŠผ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” CountButtons ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

๋จผ์ € ๋นˆ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘์„ฑํ•ด์ค€๋‹ค.

src/components/CountButtons.js

function CountButtons() {
  return (
    <div>
     CountButtons
    </div>
  );
}

export default CountButtons;

UI ๊ตฌ์„ฑํ•˜๊ธฐ

๊ฐ€์žฅ ๋จผ์ € ์ž‘์„ฑํ•  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” UI๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๊ฐ€ '+'์˜ ๋ฒ„ํŠผ๊ณผ '-'์˜ ๋ฒ„ํŠผ ์ด ๋‘ ๊ฐ€์ง€ ๋ฒ„ํŠผ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.

import { render, screen } from '@testing-library/react';
import CountButtons from './CountButtons';

describe('<CountButtons />', () => {
  it('has an increment button and a decrement button', () => {
    render(<CountButtons />);
    const incrementBtn = screen.getByTestId('incrementBtn');
    const decrementBtn = screen.getByTestId('decrementBtn');

    expect(incrementBtn).toBeInTheDocument();
    expect(decrementBtn).toBeInTheDocument();
  });

์ด๋ฒˆ์—๋Š” getByTestId๋ฅผ ํ†ตํ•ด์„œ '+' ๋ฒ„ํŠผ๊ณผ '-'๋ฒ„ํŠผ์„ ์žก์„ ์ˆ˜ ์žˆ๋„๋ก ์ž‘์„ฑํ–ˆ๋‹ค.

์ด์ œ ์ด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ํ†ต๊ณผํ•  ์ˆ˜ ์žˆ๋„๋ก ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.

๋ฐ˜์‘ํ˜•

src/components/CountButtons.js

function CountButtons() {
  return (
    <div>
      <button data-testid="incrementBtn">
        +
      </button>
      <button data-testid="decrementBtn">
        -
      </button>
    </div>
  );
}

export default CountButtons;

์ด๋ ‡๊ฒŒ data-testid๋ฅผ ๋ถ€์—ฌํ•˜๊ฒŒ ๋˜๋ฉด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ getByTestId๋ฅผ ํ†ตํ•ด์„œ ํ•ด๋‹น ์š”์†Œ๋ฅผ ์ฐพ์•„๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

Click ์ด๋ฒคํŠธ ๊ด€๋ฆฌ

์ด๋ฒˆ์—๋Š” '+' ๋ฒ„ํŠผ๊ณผ '-'๋ฒ„ํŠผ์„ ํด๋ฆญํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ๊ด€๋ฆฌํ•ด๋ณด์ž.

CountButtons ์ปดํฌ๋„ŒํŠธ ์—์„œ๋Š” ๋ฒ„ํŠผ์„ ํด๋ฆญํ–ˆ์„ ๋•Œ ์ˆซ์ž๊ฐ€ ์ฆ๊ฐ€ํ•˜๋Š” ํ•จ์ˆ˜์™€ ๊ฐ์†Œํ•˜๋Š” ํ•จ์ˆ˜, ์ด ๋‘ ๊ฐ€์ง€ ํ•จ์ˆ˜๋ฅผ props๋กœ ๋ฐ›์•„์™€์„œ ํ˜ธ์ถœํ•˜๋„๋ก ํ•  ๊ฒƒ์ด๋‹ค.

src/components/CountButtons.test.js

import { fireEvent, render, screen } from '@testing-library/react';
import CountButtons from './CountButtons';

describe('<CountButtons />', () => {
  it('has an increment button and a decrement button', () => {
    render(<CountButtons />);
    const incrementBtn = screen.getByTestId('incrementBtn');
    const decrementBtn = screen.getByTestId('decrementBtn');

    expect(incrementBtn).toBeInTheDocument();
    expect(decrementBtn).toBeInTheDocument();
  });

  it('calls incrementFn and decrementFn', () => {
    const incrementFn = jest.fn();
    const decrementFn = jest.fn();
    render(
      <CountButtons incrementFn={incrementFn} decrementFn={decrementFn} />
    );
    const incrementBtn = screen.getByTestId('incrementBtn');
    const decrementBtn = screen.getByTestId('decrementBtn');

    fireEvent.click(incrementBtn);
    fireEvent.click(decrementBtn);

    expect(incrementFn).toBeCalled();
    expect(decrementFn).toBeCalled();
  });
});

incrementFn๊ณผ decrementFn ๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ mocking ํ•ด์„œ ํ•จ์ˆ˜๋“ค์ด ๋ฒ„ํŠผ์ด ํด๋ฆญ๋์„ ๋•Œ ํ˜ธ์ถœ์ด ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์‚ฌ๋ฅผ ํ•˜๋„๋ก ํ–ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ด ํ…Œ์ŠคํŠธ๋“ค์ด ํ†ต๊ณผ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ ์ž‘์„ฑ์„ ๋งˆ๋ฌด๋ฆฌํ•ด๋ณด์ž.

src/components/CountButtons.js

function CountButtons({ incrementFn, decrementFn }) {
  return (
    <div>
      <button onClick={incrementFn} data-testid="incrementBtn">
        +
      </button>
      <button onClick={decrementFn} data-testid="decrementBtn">
        -
      </button>
    </div>
  );
}

export default CountButtons;

App ์ปดํฌ๋„ŒํŠธ

App ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” counter์— ๊ด€ํ•œ ๋ชจ๋“  ์ƒํƒœ๊ฐ€ ๊ด€๋ฆฌ๋œ๋‹ค.

์ด ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ๊ธฐ์กด์— unit ํ…Œ์ŠคํŠธ๊ฐ€ ์ด๋ฃจ์–ด์ง„ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„๋˜๋ฏ€๋กœ ์ด๋ฒˆ์— ์ž‘์„ฑํ•˜๊ฒŒ ๋˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์ด๋‹ค.

๋จผ์ € ๊ธฐ์กด์— ์žˆ๋˜ App.js ํŒŒ์ผ์„ ๋นˆ ์ƒํƒœ๋กœ ๋งŒ๋“ค์–ด์ค€๋‹ค.

src/App.js

function App() {
  return (
    <div>App</div>
  );
}

export default App;

CountView ์ปดํฌ๋„ŒํŠธ์™€ CountButtons ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ™•์ธ

ํ•ด๋‹น App ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฒซ ๋ฒˆ์งธ๋กœ ์ž‘์„ฑํ•  ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋Š” CountView์™€ CountButtons๊ฐ€ ๋ Œ๋”๋ง์ด ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

๊ธฐ์กด์— App.test.js ํŒŒ์ผ์— ์žˆ๋˜ ๋‚ด์šฉ์„ ์ง€์šฐ๊ณ  ์ƒˆ๋กญ๊ฒŒ ์ž‘์„ฑํ•œ๋‹ค.

src/App.test.js

import { render, screen } from '@testing-library/react';
import App from './App';

describe('<App />', () => {
  it('renders ConterView and CountButtons', () => {
    render(<App />);
    const view = screen.getByText('ํ˜„์žฌ ์ˆซ์ž: 0');
    const buttons = screen.getAllByRole('button');

    expect(view).toBeInTheDocument();
    expect(buttons.length).toBe(2);
  });
});

getAllByRole์„ ํ†ตํ•ด์„œ ํ™”๋ฉด์˜ ๋ชจ๋“  ๋ฒ„ํŠผ์„ ๋‹ด์„ ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” CountButtons ์ปดํฌ๋„ŒํŠธ์—์„œ ๋‘ ๊ฐœ์˜ ๋ฒ„ํŠผ์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐฐ์—ด์˜ ๊ธธ์ด๊ฐ€ 2๊ฐ€ ๋  ๊ฒƒ์ด๋‹ค. (getAllByRole๋กœ ์š”์†Œ๋ฅผ ๋‹ด๊ฒŒ ๋˜๋ฉด ๋ฐฐ์—ด๋กœ ๋ฐ›๋Š”๋‹ค.)

์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ํ†ต๊ณผ๋  ์ˆ˜ ์žˆ๋„๋ก App ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.

src/App.js

import { useState } from 'react';
import CountButtons from './components/CountButtons';
import CountView from './components/CountView';

function App() {
  const [count, setCount] = useState(0);
  
  return (
    <>
      <CountView count={count} />
      <CountButtons />
    </>
  );
}

export default App;

์ฆ๊ฐํ•˜๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„

์ด์ œ '+'๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด 1์”ฉ ์ฆ๊ฐ€, '-'๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด 1์”ฉ ๊ฐ์†Œํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ๊ฐ๊ฐ ๋งŒ๋“ค์–ด ๋ณด์ž.

src/App.test.js

import { fireEvent, render, screen } from '@testing-library/react';
import App from './App';

describe('<App />', () => {
  it('renders ConterView and CountButtons', () => {
    render(<App />);
    const view = screen.getByText('ํ˜„์žฌ ์ˆซ์ž: 0');
    const buttons = screen.getAllByRole('button');

    expect(view).toBeInTheDocument();
    expect(buttons.length).toBe(2);
  });

  it('increments by 1 each time incrementBtn is clicked', () => {
    render(<App />);
    const initialScreen = screen.getByText('ํ˜„์žฌ ์ˆซ์ž: 0');
    expect(initialScreen).toBeInTheDocument();

    const incrementBtn = screen.getByTestId('incrementBtn');

    fireEvent.click(incrementBtn);
    fireEvent.click(incrementBtn);
    fireEvent.click(incrementBtn);

    const changedScreen = screen.getByText('ํ˜„์žฌ ์ˆซ์ž: 3');
    expect(changedScreen).toBeInTheDocument();
  });

  it('decrements by 1 each time decrementBtn is clicked', () => {
    render(<App />);
    const initialScreen = screen.getByText('ํ˜„์žฌ ์ˆซ์ž: 0');
    expect(initialScreen).toBeInTheDocument();

    const decrementBtn = screen.getByTestId('decrementBtn');

    fireEvent.click(decrementBtn);
    fireEvent.click(decrementBtn);

    const changedScreen = screen.getByText('ํ˜„์žฌ ์ˆซ์ž: -2');
    expect(changedScreen).toBeInTheDocument();
  });
});

์ฆ๊ฐ€ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์—์„œ๋Š” fireEvent์˜ click ์ด๋ฒคํŠธ๋ฅผ ์„ธ ๋ฒˆ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์ƒํ™ฉ์„ ๋งŒ๋“ค์–ด์„œ ์ˆซ์ž์˜ ์ดˆ๊ธฐ๊ฐ’ 0์—์„œ + 3์ด ๋˜์–ด์„œ ํ™”๋ฉด์— 3์ด ํ‘œ์‹œ๋˜์–ด์•ผ ํ•œ๋‹ค.

๊ฐ์†Œํ•˜๋Š” ๋ถ€๋ถ„์—์„œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋‹ค. ๋‘ ๋ฒˆ ๋ฐœ์ƒ์‹œ์ผœ์„œ -2๊ฐ€ ํ‘œ์‹œ๋˜์–ด์•ผ ํ•œ๋‹ค.

์ด์ œ ์ด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋“ค์„ ํ†ต๊ณผ์‹œ์ผœ๋ณด์ž.

src/App.js

import { useState } from 'react';
import CountButtons from './components/CountButtons';
import CountView from './components/CountView';

function App() {
  const [count, setCount] = useState(0);

  const incrementHandler = () => {
    setCount((count) => count + 1);
  }

  const decrementtHandler = () => {
    setCount((count) => count - 1);
  }

  return (
    <>
      <CountView count={count} />
      <CountButtons
        incrementFn={incrementHandler}
        decrementFn={decrementtHandler}
      />
    </>
  );
}

export default App;

์ด์ œ npm test ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด๋ณด๋ฉด ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ํ†ต๊ณผ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿผ ๋งˆ์ง€๋ง‰์œผ๋กœ npm start๋ฅผ ํ†ตํ•ด์„œ ์ง€๊ธˆ๊นŒ์ง€ ์ž‘์„ฑํ•œ Counter App์„ ์‹คํ–‰ํ•ด์„œ ํ™•์ธํ•ด ๋ณด์ž.

์ตœ์ข… ๊ฒฐ๊ณผ

TDD ๊ฐœ๋ฐœ ํ๋ฆ„์œผ๋กœ ๊ฐœ๋ฐœํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋งค๋ฒˆ ํ™•์ธํ•˜์ง€ ์•Š์•„๋„ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๊ฒŒ ๊ฐœ๋ฐœ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

๋งˆ์ง€๋ง‰์œผ๋กœ Counter App์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ coverage๋ฅผ ํ™•์ธํ•ด๋ณด์ž.

ํ„ฐ๋ฏธ๋„์— ํ•ด๋‹น ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์‹คํ–‰ํ•œ๋‹ค.

npm test -- --coverage .

test code coverage

์ง€๊ธˆ๊นŒ์ง€ ์ž‘์„ฑํ•œ App.js, CountButtons.js, CountView.js ํŒŒ์ผ๋“ค์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ coverage๊ฐ€ 100%์ž„์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์•ฝ๊ฐ„์˜ ๋ฆฌํŒฉํ† ๋ง

๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ๋งˆ๋‹ค CountButtons ์ปดํฌ๋„ŒํŠธ์˜ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฌด๋ถ„๋ณ„ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ๋ง‰๊ธฐ ์œ„ํ•ด์„œ๋Š” React.memo๋กœ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

src/components/CountButtons.js

import { memo } from 'react';

function CountButtons({ incrementFn, decrementFn }) {
  return (
    <div>
      <button onClick={incrementFn} data-testid="incrementBtn">
        +
      </button>
      <button onClick={decrementFn} data-testid="decrementBtn">
        -
      </button>
    </div>
  );
}

export default memo(CountButtons);

ํ•˜์ง€๋งŒ App ์ปดํฌ๋„ŒํŠธ์—์„œ count state๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค incrementHandler ํ•จ์ˆ˜์™€ decrementHandler ํ•จ์ˆ˜๊ฐ€ ์žฌ์„ ์–ธ ๋˜๋ฉด์„œ ๋ฒ„ํŠผ์„ ๋‚˜ํƒ€๋‚ด๋Š” CountButtons ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง ๋˜๊ณ  ์žˆ๋‹ค. 

๊ทธ๋Ÿฌ๋ฏ€๋กœ App ์ปดํฌ๋„ŒํŠธ์—์„œ ํ•ด๋‹น ํ•จ์ˆ˜๋“ค์„ useCallback์œผ๋กœ ๊ฐ์‹ธ์ฃผ๋„๋ก ํ•˜์ž.

useCallback์—์„œ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋นˆ ๋ฐฐ์—ด์„ ๋„ฃ์–ด์ฃผ๋ฉด ์ฒซ ๋ Œ๋”๋ง ์‹œ์—๋งŒ ์„ ์–ธ๋˜๊ณ  ๊ทธ ๋’ค๋กœ๋Š” ์ฒ˜์Œ ์„ ์–ธ๋œ ํ•จ์ˆ˜๋ฅผ unmount ๋  ๋•Œ๊นŒ์ง€ ๊ฐ€์ ธ๊ฐ„๋‹ค.

src/App.js

import { useCallback, useState } from 'react';
import CountButtons from './components/CountButtons';
import CountView from './components/CountView';

function App() {
  const [count, setCount] = useState(0);

  const incrementHandler = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  const decrementtHandler = useCallback(() => {
    setCount((count) => count - 1);
  }, []);

  return (
    <>
      <CountView count={count} />
      <CountButtons
        incrementFn={incrementHandler}
        decrementFn={decrementtHandler}
      />
    </>
  );
}

export default App;

๐Ÿ“•๋งˆ์น˜๋ฉฐ

์ด๋ ‡๊ฒŒ TDD ๊ฐœ๋ฐœ ํ๋ฆ„์œผ๋กœ Jest์™€ React testing library๋ฅผ ํ™œ์šฉํ•ด์„œ ๊ฐ„๋‹จํ•œ Counter ์•ฑ์„ ๋งŒ๋“ค์–ด๋ดค๋‹ค.

๊ณต๋ถ€ํ•˜๋ฉด์„œ๋„ ๋Š๋‚€ ๊ฑฐ์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์ค‘์š”์„ฑ์€ ์ •๋ง ํฌ๊ฒŒ ๋Š๊ปด์ง„๋‹ค.

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฆฌํŒฉํ† ๋ง์„ ํ•˜๊ณ  ๋‚˜์„œ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง์ ‘ ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์•„๋„ ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์‹œ๊ฐ„์ด ์ ˆ์•ฝ๋œ๋‹ค.

๊ฐœ๋ฐœํ•  ๋•Œ์—๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์—†์ด ๊ฐœ๋ฐœํ•˜๋Š” ํŽธ์ด ์‹œ๊ฐ„์ด ๋‹จ์ถ•๋˜์ง€๋งŒ ๋‚˜์ค‘์— ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ด์ง€๊ฑฐ๋‚˜ ๋ฆฌํŒฉํ† ๋ง์ด ํ•„์š”ํ•  ๋•Œ๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์‹œ๊ฐ„์ด ํ›จ์”ฌ ์ ˆ์•ฝ๋˜๊ณ  ๋ฆฌํŒฉํ† ๋ง ํ•˜๊ธฐ๋„ ํŽธํ•ด์ง€๊ธฐ ๋•Œ๋ฌธ์—,

์•ž์œผ๋กœ ๊ฐœ๋ฐœ์„ ํ•˜๋ฉด์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๊ผญ ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

๋ฐ˜์‘ํ˜•
profile

๐Ÿ’ป์šฉ๋‡ฝ ๊ฐœ๋ฐœ ๋…ธํŠธ๐Ÿ’ป

@์šฉ๋‡ฝ

ํฌ์ŠคํŒ…์ด ์ข‹์•˜๋‹ค๋ฉด "์ข‹์•„์š”โค๏ธ" ๋˜๋Š” "๊ตฌ๋…๐Ÿ‘๐Ÿป" ํ•ด์ฃผ์„ธ์š”!