PromleeBlog
sitemapaboutMe

posting thumbnail
React 개발자 도구와 테스트 - React, 알고 쓰자 13일차
React Mastering Developer Tools & Testing - React Explained Day 13

📅

🚀

들어가기 전에 🔗

우리는 그동안 React의 핵심 개념부터 상태 관리, 라우팅, 보안에 이르기까지 많은 것을 배웠습니다.
이제는 우리가 작성한 코드가 제대로 동작하는지 확인하고, 성능 병목 현상을 찾아 개선하며, 미래의 변경 사항에도 코드가 깨지지 않을 것이라는 확신을 가질 수 있도록 도와주는 도구와 기법에 대해 알아볼 시간입니다.
훌륭한 개발자는 코드 작성 능력만큼이나 디버깅, 성능 분석, 테스트 작성 능력도 중요합니다.


오늘은 React 개발 과정을 더욱 효율적이고 안정적으로 만들어주는 필수적인 개발자 도구인
React DevTools
와, 코드 품질 보증의 핵심인
테스트
(특히 Jest와 React Testing Library 조합), 그리고 UI 컴포넌트 개발 및 시각적 테스트를 위한
Storybook
Chromatic
에 대해 자세히 살펴보겠습니다.

🚀

React DevTools: 컴포넌트 내부 들여다보기 🔗

React DevTools - Chrome 웹스토어
React DevTools는 React 애플리케이션을 디버깅하고 검사하기 위한 공식 브라우저 확장 프로그램입니다.
Chrome, Firefox, Edge 등 주요 브라우저에서 사용할 수 있으며, React 컴포넌트 트리, 각 컴포넌트의 props, state, hooks 상태 등을 실시간으로 확인하고 조작해 볼 수 있어 개발 및 디버깅 생산성을 크게 향상시켜 줍니다.

주요 기능 살펴보기 🔗

React DevTools를 설치하고 브라우저 개발자 도구를 열면 'Components' 탭과 'Profiler' 탭이 추가된 것을 볼 수 있습니다.
➡️

1. Components 탭: 구조와 상태 확인 🔗

React DevTools의 'Components' 탭
React DevTools의 'Components' 탭
컴포넌트를 클릭하면 우측에 해당 컴포넌트의 props, state, hooks 상태가 표시됩니다.
➡️

2. Profiler 탭: 성능 병목 지점 찾기 🔗

Profiler 탭은 React 애플리케이션의 렌더링 성능을 분석하고 병목 지점을 찾는 데 사용됩니다.
느리게 렌더링되는 컴포넌트나 불필요하게 자주 렌더링되는 컴포넌트를 식별하여 성능 최적화 작업을 수행하는 데 매우 유용합니다.
React DevTools의 'Profiler' 탭
React DevTools의 'Profiler' 탭

효과적인 디버깅 및 프로파일링 팁 🔗


🚀

Jest + React Testing Library 🔗

테스트는 코드 변경이 기존 기능을 망가뜨리지 않았는지 확인하고, 코드의 동작 방식을 문서화하며, 리팩토링에 대한 자신감을 심어주는 중요한 활동입니다.
React 애플리케이션 테스트에는 주로 테스트 러너(Test Runner)인
Jest
와 테스팅 유틸리티 라이브러리인
React Testing Library(RTL)
조합이 널리 사용됩니다.

Jest 🔗

Facebook(Meta)에서 만든 JavaScript 테스트 프레임워크입니다.
테스트 실행(Test Runner), 코드 커버리지 측정, 모킹(Mocking), 단언(Assertion) 라이브러리 등 테스트에 필요한 대부분의 기능을 내장하고 있어 별도의 도구를 많이 설치할 필요 없이 편리하게 사용할 수 있습니다.
Create React App(CRA) 등 많은 React 프로젝트 템플릿에 기본적으로 포함되어 있습니다.

React Testing Library (RTL) 🔗

컴포넌트의
내부 구현
보다는
사용자 관점
에서 컴포넌트가 어떻게 동작하는지를 테스트하도록 권장하는 라이브러리입니다.
사용자가 실제로 컴포넌트를 사용하는 방식(예: 특정 텍스트 찾기, 버튼 클릭, 폼 입력)과 유사하게 테스트 코드를 작성하도록 유도하여, 컴포넌트 내부 구조가 변경되더라도 테스트가 쉽게 깨지지 않고 실제 사용자 경험과 관련된 문제를 더 잘 찾아낼 수 있도록 돕습니다.

테스트 실행 방법 🔗

테스트는 코드를 작성하는 것만큼 중요한 개발 과정입니다. 여기서는 간단한 카운터 컴포넌트를 예로 들어 단계별로 테스트 코드를 작성하는 방법을 살펴보겠습니다.
➡️

1. 테스트할 컴포넌트 작성 🔗

먼저 간단한 Counter 컴포넌트를 작성해봅시다:
/src/components/Counter.tsx
import React, { useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
      <button onClick={() => setCount(count - 1)}>감소</button>
    </div>
  );
}
 
export default Counter;
➡️

2. 테스트 파일 작성 🔗

위 컴포넌트를 테스트하는 코드를 작성합니다:
/src/components/Counter.test.tsx
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom"; // 커스텀 DOM 매처 사용 (toBeInTheDocument 등)
import Counter from "./Counter";
 
describe("Counter 컴포넌트", () => {
  it("초기값이 0으로 렌더링된다", () => {
    render(<Counter />); // 1. 컴포넌트 렌더링
    // 2. 화면에서 요소 찾기 (텍스트 내용으로)
    const countElement = screen.getByText("현재 카운트: 0");
    // 3. 단언(Assertion) - 요소가 존재하는지 확인
    expect(countElement).toBeInTheDocument();
  });
 
  it("증가 버튼 클릭 시 카운트가 1 증가한다", () => {
    render(<Counter />);
    // 버튼 찾기 (버튼의 텍스트 이름으로)
    const incrementButton = screen.getByRole("button", { name: "증가" });
    // 이벤트 발생시키기
    fireEvent.click(incrementButton);
    // 결과 확인 (변경된 텍스트로 요소 찾기)
    const countElement = screen.getByText("현재 카운트: 1");
    expect(countElement).toBeInTheDocument();
  });
 
  it("감소 버튼 클릭 시 카운트가 -1 감소한다", () => {
    render(<Counter />);
    const decrementButton = screen.getByRole("button", { name: "감소" });
    fireEvent.click(decrementButton);
    const countElement = screen.getByText("현재 카운트: -1");
    expect(countElement).toBeInTheDocument();
  });
});
➡️

3. 테스트 실행하기 🔗

Jest 테스트를 실행하는 방법은 보통 프로젝트 설정에 따라 조금씩 다를 수 있지만, 일반적으로 다음과 같은 명령어를 사용합니다.
  1. 모든 테스트 실행
    : package.json 파일의 scripts 부분에 정의된 테스트 명령어를 실행합니다. Create React App 등으로 생성된 프로젝트는 보통 다음과 같습니다.
    npm test
    # 또는
    yarn test
    이 명령은 보통
    watch 모드
    로 실행되어, 파일 변경을 감지하고 관련된 테스트만 자동으로 다시 실행해 줍니다. 처음 실행 시에는 모든 테스트 파일을 찾아 실행합니다.
  2. 특정 파일 테스트
    : watch 모드가 아닌 환경에서 특정 파일만 테스트하고 싶다면 Jest 실행 파일에 파일 경로를 직접 지정할 수 있습니다. (설정에 따라 명령어는 다를 수 있음)
    npx jest src/components/Button.test.tsx
  3. Watch 모드 옵션
    : watch 모드에서는 다양한 키를 눌러 테스트 실행 방식을 제어할 수 있습니다.
    • a: 모든 테스트를 다시 실행합니다.
    • f: 실패한 테스트만 다시 실행합니다.
    • p: 파일 이름 패턴으로 필터링합니다.
    • t: 테스트 이름 패턴으로 필터링합니다.
    • q: watch 모드를 종료합니다.
    • Enter: 변경 사항을 감지하여 테스트를 다시 실행합니다.
➡️

4. 테스트 결과 해석하기 🔗

성공적인 테스트 결과는 다음과 같이 표시됩니다:
 PASS  src/components/Counter.test.tsx
  Counter 컴포넌트
    ✓ 초기값이 0으로 렌더링된다 (56 ms)
    ✓ 증가 버튼 클릭 시 카운트가 1 증가한다 (119 ms)
    ✓ 감소 버튼 클릭 시 카운트가 -1 감소한다 (36 ms)
실패한 테스트는 상세한 오류 메시지와 함께 표시됩니다. 예를 들어 버튼이 예상대로 동작하지 않으면:
● Counter 컴포넌트 › 증가 버튼 클릭 시 카운트가 1 증가한다

  TestingLibraryElementError: Unable to find an element with the text: 현재 카운트: 1

패턴별 테스트 예시 🔗

➡️

기본 렌더링 테스트 🔗

import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom'; // 추가적인 DOM 매처 사용 가능
import Button from './Button'; // 테스트할 컴포넌트
 
test('버튼이 정상적으로 렌더링된다', () => {
  render(<Button>클릭하세요</Button>);
 
  // '클릭하세요' 텍스트를 가진 버튼(role)이 화면에 있는지 확인
  const buttonElement = screen.getByRole('button', { name: '클릭하세요' });
  expect(buttonElement).toBeInTheDocument();
});
 
test('disabled prop이 전달되면 버튼이 비활성화된다', () => {
  render(<Button disabled>클릭하세요</Button>);
 
  const buttonElement = screen.getByRole('button', { name: '클릭하세요' });
  // 버튼이 비활성화 상태인지 확인
  expect(buttonElement).toBeDisabled();
});
➡️

이벤트 핸들러 테스트 🔗

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; // 사용자 이벤트 시뮬레이션
import Button from './Button';
 
test('버튼 클릭 시 onClick 핸들러가 호출된다', async () => {
  const handleClick = jest.fn(); // jest.fn()으로 모의(mock) 함수 생성
  render(<Button onClick={handleClick}>클릭하세요</Button>);
 
  const buttonElement = screen.getByRole('button', { name: '클릭하세요' });
  // 사용자 이벤트 시뮬레이션 (실제 클릭과 더 유사)
  await userEvent.click(buttonElement);
 
  // handleClick 함수가 1번 호출되었는지 확인
  expect(handleClick).toHaveBeenCalledTimes(1);
});
fireEvent보다 userEvent 사용이 권장됩니다. userEvent는 실제 사용자의 상호작용 흐름(마우스 이동, 포커스 등)을 더 정확하게 모방합니다.
➡️

비동기 테스트 (예: API 호출 후 상태 변경) 🔗

API 호출과 같은 비동기 작업은 async/await와 RTL의 find* 쿼리 또는 waitFor 유틸리티를 사용하여 테스트합니다.
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import axios from 'axios'; // API 호출 라이브러리
import UserProfileLoader from './UserProfileLoader';
 
// axios.get 함수를 모킹
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
 
test('사용자 데이터 로드 후 프로필 정보 표시', async () => {
  // API 성공 응답 모킹
  const mockUserData = { name: 'Alice', email: 'alice@example.com' };
  mockedAxios.get.mockResolvedValue({ data: mockUserData });
 
  render(<UserProfileLoader userId="1" />);
 
  // 초기 로딩 상태 확인 (선택적)
  expect(screen.getByText(/로딩 중/i)).toBeInTheDocument();
 
  // API 호출 후 데이터가 화면에 표시될 때까지 기다림
  // findBy* 쿼리는 요소가 나타날 때까지 자동으로 기다림 (기본 타임아웃 1000ms)
  const userNameElement = await screen.findByText(mockUserData.name);
  expect(userNameElement).toBeInTheDocument();
 
  // waitFor: 특정 조건이 만족될 때까지 기다림 (find* 로 충분하지 않을 때 사용)
  // await waitFor(() => {
  //   expect(screen.getByText(mockUserData.email)).toBeInTheDocument();
  // });
 
  // 로딩 메시지가 사라졌는지 확인
  expect(screen.queryByText(/로딩 중/i)).not.toBeInTheDocument();
});
 
test('API 호출 실패 시 에러 메시지 표시', async () => {
  // API 실패 응답 모킹
  const errorMessage = '데이터를 불러올 수 없습니다.';
  mockedAxios.get.mockRejectedValue(new Error(errorMessage));
 
  render(<UserProfileLoader userId="1" />);
 
  // 에러 메시지가 화면에 표시될 때까지 기다림
  const errorElement = await screen.findByText(new RegExp(errorMessage, 'i'));
  expect(errorElement).toBeInTheDocument();
});
API 호출 자체를 테스트하는 것이 아니라, API 호출 결과에 따라 컴포넌트가 어떻게 반응하는지를 테스트하는 것이 중요합니다.
이를 위해 jest.mock을 사용하여 API 모듈을 모킹합니다.

🚀

Storybook & Chromatic: 컴포넌트 개발과 시각적 테스트 🔗

Storybook은 UI 컴포넌트를 독립적으로 개발하고 문서화하는 도구이며, Chromatic은 Storybook과 연동하여 시각적 변화를 자동으로 테스트하는 서비스입니다.

Storybook 🔗

➡️

1. 설치 🔗

만약 여러분의 React 프로젝트에 Storybook이 아직 없다면, 터미널(명령어를 입력하는 까만 창)을 열고 프로젝트 폴더 안에서 다음 명령어를 입력해 주세요. (TypeScript 프로젝트 기준)
npx storybook@latest init
이 명령어는 여러분의 프로젝트를 살펴보고 필요한 설정 파일과 예제 파일들을 자동으로 만들어 줄 거예요. 마치 장난감 상자를 가져다주는 것과 같아요.
➡️

2. 실행 🔗

설치가 끝나면, 이제 Storybook을 열어야 합니다. 터미널에 다음 명령어를 입력해 보세요.
npm run storybook
 # 또는 (yarn을 사용한다면)
yarn storybook
잠시 기다리면 브라우저가 자동으로 열리면서 Storybook 페이지가 나타날 거예요. 이 페이지는 여러분이 만든 컴포넌트들을 미리 볼 수 있는 페이지입니다.
➡️

3. 결과 확인 🔗

Storybook UI
Storybook UI
Storybook을 사용하면, 각 컴포넌트를 체계적으로 관리하고 쉽게 테스트하며 다른 사람들과 공유할 수 있답니다.
➡️

스토리 작성 예시 🔗

컴포넌트의 다양한 모습을 보여주는 '이야기'는 이렇게 작성해요.
/src/components/Button.stories.tsx
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import Button from './Button'; // 실제 컴포넌트
 
// 메타 정보: 컴포넌트 설명서 표지 같은 거예요
const meta: Meta<typeof Button> = {
  title: 'Components/Button', // 어디에 둘지 정해요
  component: Button, // 주인공 컴포넌트
  tags: ['autodocs'], // 자동으로 설명서 만들기
  argTypes: { /* ... props 조종판 설정 ... */ },
};
export default meta;
 
type Story = StoryObj<typeof Button>;
 
// 첫 번째 이야기: 기본 모습 버튼
export const Primary: Story = {
  args: { primary: true, label: 'Button' }, // 이렇게 생겼어요 하고 알려줘요
};
 
// 두 번째 이야기: 보조 역할 버튼
export const Secondary: Story = {
  args: { label: 'Button' },
};
 
// ... 더 많은 이야기들 ...

Chromatic: UI 변경사항 자동 감지 🔗

Storybook으로 컴포넌트를 개발하고, Chromatic으로 시각적 일관성을 유지하면 UI 품질을 효과적으로 높일 수 있습니다.

🚀

결론 🔗

오늘은 React 개발의 생산성과 코드 품질을 높이는 데 도움이 되는 개발자 도구와 테스트 전략에 대해 알아보았습니다.
이러한 도구와 테스트 전략을 꾸준히 활용하는 것은 당장은 시간과 노력이 더 드는 것처럼 보일 수 있지만, 장기적으로는 버그를 줄이고 유지보수 비용을 절감하며, 더 안정적이고 품질 높은 애플리케이션을 만드는 데 큰 도움이 됩니다.
다음 시간에는 TypeScript와 React를 함께 사용할 때 유용한 팁과 패턴에 대해 알아보겠습니다. 제네릭 컴포넌트 타이핑부터 커스텀 훅 타입 추론, Zod/Yup을 이용한 타입 안전성 강화까지 살펴보겠습니다.

참고 🔗