우리는 그동안 React의 핵심 개념부터 상태 관리, 라우팅, 보안에 이르기까지 많은 것을 배웠습니다.
이제는 우리가 작성한 코드가 제대로 동작하는지 확인하고, 성능 병목 현상을 찾아 개선하며, 미래의 변경 사항에도 코드가 깨지지 않을 것이라는 확신을 가질 수 있도록 도와주는 도구와 기법에 대해 알아볼 시간입니다.
훌륭한 개발자는 코드 작성 능력만큼이나 디버깅, 성능 분석, 테스트 작성 능력도 중요합니다.
오늘은 React 개발 과정을 더욱 효율적이고 안정적으로 만들어주는 필수적인 개발자 도구인
React DevTools
와, 코드 품질 보증의 핵심인
테스트
(특히 Jest와 React Testing Library 조합), 그리고 UI 컴포넌트 개발 및 시각적 테스트를 위한
React DevTools - Chrome 웹스토어↗
React DevTools는 React 애플리케이션을 디버깅하고 검사하기 위한 공식 브라우저 확장 프로그램입니다.
Chrome, Firefox, Edge 등 주요 브라우저에서 사용할 수 있으며, React 컴포넌트 트리, 각 컴포넌트의 props, state, hooks 상태 등을 실시간으로 확인하고 조작해 볼 수 있어 개발 및 디버깅 생산성을 크게 향상시켜 줍니다.
현재 페이지를 구성하는 React 컴포넌트들의 계층 구조를 트리 형태로 보여줍니다.
일반 DOM 트리와 달리 React 컴포넌트 기준으로 구조를 파악할 수 있어 편리합니다.
Props 및 State 검사/수정
특정 컴포넌트를 선택하면 오른쪽 패널에 해당 컴포넌트가 받은 props와 내부 state, 그리고 사용 중인 hooks의 현재 값을 상세하게 보여줍니다.
심지어 이 값들을 개발자 도구에서 직접 수정하여 UI 변화를 실시간으로 확인해 볼 수도 있습니다. (수정은 개발 중에만 임시로 적용됩니다)
렌더링 소스 추적
특정 컴포넌트가 왜 리렌더링되었는지 그 원인(어떤 prop이나 hook 상태가 변경되었는지)을 추적하는 데 도움을 줄 수 있습니다. (설정 필요)
컴포넌트 소스 코드 이동
컴포넌트 이름을 클릭하면 해당 컴포넌트의 소스 코드로 바로 이동하는 기능도 제공합니다 (소스 맵 설정 필요).
React DevTools의 'Components' 탭
컴포넌트를 클릭하면 우측에 해당 컴포넌트의 props, state, hooks 상태가 표시됩니다.
Profiler 탭은 React 애플리케이션의 렌더링 성능을 분석하고 병목 지점을 찾는 데 사용됩니다.
느리게 렌더링되는 컴포넌트나 불필요하게 자주 렌더링되는 컴포넌트를 식별하여 성능 최적화 작업을 수행하는 데 매우 유용합니다.
녹화 (Record)
: 파란색 녹화 버튼을 누르고 애플리케이션과 상호작용(페이지 로드, 버튼 클릭, 입력 등)을 수행한 다음, 다시 녹화 버튼을 눌러 프로파일링 데이터를 수집합니다.
결과 분석
: 녹화가 완료되면 다음과 같은 다양한 형태로 렌더링 성능 정보를 분석할 수 있습니다.
Flamegraph 차트
각 커밋(렌더링 업데이트)별로 어떤 컴포넌트가 렌더링되었고, 각 컴포넌트 렌더링에 얼마나 시간이 걸렸는지를 계층적인 막대 그래프(불꽃 모양)로 보여줍니다. 막대의 너비가 넓을수록 렌더링 시간이 오래 걸린 컴포넌트입니다. 이를 통해 어떤 컴포넌트가 성능 병목의 원인인지 시각적으로 파악할 수 있습니다.
Ranked 차트
각 커밋에서 렌더링 시간이 가장 오래 걸린 컴포넌트 순서대로 목록을 보여줍니다. 성능 개선이 시급한 컴포넌트를 빠르게 찾는 데 유용합니다.
Component 차트
특정 컴포넌트를 클릭하면 해당 컴포넌트가 프로파일링 기간 동안 언제, 왜 렌더링되었는지 상세 정보를 확인할 수 있습니다.
Facebook(Meta)에서 만든 JavaScript 테스트 프레임워크입니다.
테스트 실행(Test Runner), 코드 커버리지 측정, 모킹(Mocking), 단언(Assertion) 라이브러리 등 테스트에 필요한 대부분의 기능을 내장하고 있어 별도의 도구를 많이 설치할 필요 없이 편리하게 사용할 수 있습니다.
Create React App(CRA) 등 많은 React 프로젝트 템플릿에 기본적으로 포함되어 있습니다.
에서 컴포넌트가 어떻게 동작하는지를 테스트하도록 권장하는 라이브러리입니다.
사용자가 실제로 컴포넌트를 사용하는 방식(예: 특정 텍스트 찾기, 버튼 클릭, 폼 입력)과 유사하게 테스트 코드를 작성하도록 유도하여, 컴포넌트 내부 구조가 변경되더라도 테스트가 쉽게 깨지지 않고 실제 사용자 경험과 관련된 문제를 더 잘 찾아낼 수 있도록 돕습니다.
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 호출과 같은 비동기 작업은 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 모듈을 모킹합니다.
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' },};// ... 더 많은 이야기들 ...