React Testing Library와 Jest를 활용해 안정적인 프론트엔드 테스트 환경을 구축하고, 실용적인 테스트 작성법을 익혀보세요. 컴포넌트부터 비동기 처리까지, 실제 프로젝트에 바로 적용 가능한 노하우를 공개합니다.
안녕하세요! 개발자 여러분, 오늘 다룰 주제는 프론트엔드 테스트입니다. 혹시 이런 경험 있으신가요? 열심히 개발한 기능이 작은 변경 하나로 엉뚱한 곳에서 오류를 뿜어내거나, 새로운 기능을 추가했는데 기존 기능이 망가지는 바람에 밤늦게까지 디버깅했던 경험 말이에요. 정말 생각만 해도 아찔하죠?
특히 React 같은 복잡한 프레임워크 기반의 프론트엔드 애플리케이션에서는 사용자 인터페이스(UI)와 비즈니스 로직이 긴밀하게 엮여 있어서, 예상치 못한 버그가 발생하기 쉽거든요. 이런 문제들을 미연에 방지하고, 더 나아가 개발 속도와 코드의 안정성을 높이는 데 필수적인 것이 바로 테스트입니다.
이번 글에서는 프론트엔드 테스트의 든든한 두 축인 Jest와 React Testing Library (RTL)를 활용해서, 여러분의 React 프로젝트에 견고한 테스트 환경을 구축하고 실전 테스트 코드를 작성하는 방법을 자세히 알려드릴 거예요. 이론부터 실전 예제까지 꼼꼼하게 다룰 테니, 함께 따라오시면 분명 도움이 되실 겁니다!
📑 목차
- 프론트엔드 테스트, 왜 중요할까요?
- 버그 감소와 사용자 경험 향상
- 유지보수성 및 확장성 증대
- 협업 효율 증대 및 문서화 효과
- 테스트 환경 구축의 첫걸음: Jest와 React Testing Library 소개
- Jest: 자바스크립트 테스트의 든든한 기반
- React Testing Library: 사용자의 관점에서 테스트하기
- React 프로젝트에 Jest와 React Testing Library 설치 및 설정
- 필수 패키지 설치
- Babel 설정 (`babel.config.js` 또는 `package.json`)
- Jest 설정 (`jest.config.js` 또는 `package.json`)
- 테스트 환경 설정 파일 (`src/setupTests.js`)
- `package.json`에 테스트 스크립트 추가
- 실전 테스트 작성 가이드: 컴포넌트 단위 테스트
- 기본 컴포넌트 테스트하기 (렌더링 확인)
- 사용자 인터랙션 테스트하기 (이벤트 처리)
- Props를 사용한 컴포넌트 테스트
- 비동기 처리와 Mocking: 실제 환경처럼 테스트하기
- 비동기 코드 테스트 (API 호출 시나리오)
- 외부 모듈 Mocking (예: React Router)
- Jest와 RTL, 더 효과적으로 활용하는 팁
- 스냅샷 테스트의 활용과 주의점
- 테스트 커버리지 확인 및 관리
- 테스트 파일 구조화
- 마무리하며: 견고한 프론트엔드의 시작
Image by analogicus on Pixabay
프론트엔드 테스트, 왜 중요할까요?
많은 분들이 테스트 코드 작성에 시간을 할애하는 것을 망설이곤 하죠. "일단 기능 구현이 먼저 아닐까?", "테스트는 나중에 여유 있을 때 해도 되지 않을까?" 하고요. 하지만 테스트는 단순한 추가 작업이 아니라, 개발 프로세스의 핵심이자 프로젝트의 성공을 위한 투자라고 할 수 있습니다.
버그 감소와 사용자 경험 향상
가장 직접적인 이점은 역시 버그 감소입니다. 테스트 코드를 작성하면 개발 과정에서 발생할 수 있는 잠재적인 오류들을 조기에 발견하고 수정할 수 있게 돼요. 이는 최종적으로 사용자에게 더 안정적이고 신뢰할 수 있는 서비스를 제공하는 기반이 됩니다. 사용자가 불편함을 겪는 버그를 최소화함으로써 사용자 경험(UX)을 크게 향상시킬 수 있는 거죠.
유지보수성 및 확장성 증대
애플리케이션은 시간이 지남에 따라 끊임없이 변화하고 확장됩니다. 새로운 기능을 추가하거나 기존 코드를 리팩토링할 때, 테스트 코드가 없다면 변경 사항이 다른 부분에 어떤 영향을 미칠지 예측하기 어렵거든요. 하지만 잘 작성된 테스트 코드는 안전망 역할을 해주기 때문에, 개발자는 자신감을 가지고 코드를 수정하고 개선할 수 있게 됩니다. 이는 장기적으로 유지보수 비용을 절감하고, 프로젝트의 확장성을 높이는 데 기여하죠.
협업 효율 증대 및 문서화 효과
여러 개발자가 함께 작업하는 프로젝트에서는 테스트 코드가 일종의 살아있는 문서 역할을 하기도 합니다. 다른 개발자가 작성한 테스트 코드를 읽으면 해당 컴포넌트나 모듈이 어떤 상황에서 어떻게 동작해야 하는지 명확하게 이해할 수 있거든요. 이는 팀원 간의 협업 효율을 높이고, 신규 개발자가 프로젝트에 빠르게 적응하는 데도 도움을 줍니다.
테스트 환경 구축의 첫걸음: Jest와 React Testing Library 소개
이제 프론트엔드 테스트의 중요성을 알았으니, 어떤 도구를 사용해야 할지 알아볼 차례입니다. Jest와 React Testing Library는 React 생태계에서 가장 널리 사용되고 강력한 조합이라고 할 수 있어요.
Jest: 자바스크립트 테스트의 든든한 기반
Jest는 페이스북에서 개발한 JavaScript 테스트 프레임워크입니다. 리액트 프로젝트의 기본 테스트 도구로도 많이 사용되고 있죠. Jest의 가장 큰 특징은 다음과 같습니다.
- 빠른 성능: 병렬 테스트 실행을 통해 테스트 시간을 단축시켜 줍니다.
- 설정의 용이성: 별도의 복잡한 설정 없이도 바로 사용할 수 있을 만큼 편리해요. `create-react-app`으로 프로젝트를 만들면 기본적으로 Jest가 포함되어 있기도 하죠.
- 다양한 기능 내장: 테스트 러너, 어설션 라이브러리, 목킹(Mocking) 기능, 스냅샷 테스트 등 테스트에 필요한 거의 모든 기능이 내장되어 있습니다.
- 뛰어난 개발자 경험: 상세한 에러 메시지, 자동 테스트 실행 기능 등 개발자가 테스트를 작성하고 디버깅하는 데 최적화된 환경을 제공합니다.
Jest는 주로 단위 테스트(Unit Test)와 통합 테스트(Integration Test)에 활용됩니다. 개별 함수, 컴포넌트, 모듈 등이 예상대로 동작하는지 검증하는 데 아주 강력하답니다.
React Testing Library: 사용자의 관점에서 테스트하기
React Testing Library (RTL)는 React 컴포넌트를 테스트하기 위한 라이브러리입니다. Jest와 함께 사용될 때 시너지가 극대화되는데요, RTL의 핵심 철학은 "사용자가 애플리케이션을 사용하는 방식과 동일하게 테스트하라"는 것입니다.
이 말인즉슨, 컴포넌트의 내부 구현 디테일(예: 상태, props)에 접근하기보다는, 사용자가 화면에서 보고 상호작용하는 요소들(텍스트, 버튼, 입력 필드 등)을 기준으로 테스트를 작성하도록 유도한다는 의미입니다. 이는 테스트 코드가 리팩토링에 더 강해지고, 실제 사용자 경험에 더 밀접하게 관련될 수 있도록 도와줍니다.
RTL의 주요 특징은 다음과 같습니다.
- 사용자 중심의 테스트: 실제 사용자가 보는 DOM 요소를 쿼리하고 상호작용하는 방식으로 테스트를 작성합니다.
- 접근성(Accessibility) 향상: `getByRole`, `getByLabelText` 등 접근성 친화적인 쿼리 방식을 권장하여 자연스럽게 접근성을 고려한 UI 개발을 유도합니다.
- 내부 구현 디테일로부터의 독립: 컴포넌트의 내부 구현이 변경되어도, 사용자가 경험하는 동작이 동일하다면 테스트 코드를 수정할 필요가 적습니다.
- 가벼움: 필요한 기능만 제공하여 가볍고 사용하기 쉽습니다.
기존에 널리 사용되던 Enzyme 라이브러리와 RTL을 간단히 비교해 볼까요?
| 특징 | React Testing Library | Enzyme |
|---|---|---|
| 테스트 철학 | 사용자 관점에서 UI 상호작용 테스트 (DOM 기반) | 컴포넌트 내부 구현에 접근하여 테스트 (컴포넌트 인스턴스 기반) |
| 접근 방식 | 실제 DOM에 렌더링된 요소 쿼리 (screen.getByText 등) |
컴포넌트 인스턴스 상태, props 직접 접근 (wrapper.setState, wrapper.setProps) |
| 리팩토링 내구성 | 높음 (내부 구현 변경에 강함) | 상대적으로 낮음 (내부 구현 변경 시 테스트 코드 수정 필요성 높음) |
| 권장 사항 | React 공식 문서에서 권장하는 테스트 방법 | 이전 프로젝트에서 많이 사용되었으나, 현재는 RTL이 대세 |
이 비교를 통해 RTL이 왜 현대 프론트엔드 테스트에서 더 선호되는지 이해가 되시죠? 사용자가 실제로 경험하는 바를 테스트한다는 철학이 그만큼 중요하거든요.
React 프로젝트에 Jest와 React Testing Library 설치 및 설정
이제 본격적으로 Jest와 RTL을 React 프로젝트에 통합해 볼까요? `create-react-app`으로 프로젝트를 시작했다면 이미 Jest와 RTL이 기본으로 포함되어 있어서 별다른 설정 없이 바로 사용 가능합니다. 하지만 수동으로 프로젝트를 설정하거나 기존 프로젝트에 추가하는 경우를 위해 자세히 설명해 드릴게요.
필수 패키지 설치
터미널을 열고 다음 명령어를 실행하여 필요한 패키지들을 설치합니다.
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @babel/preset-env @babel/preset-react babel-jest
# 또는 yarn 사용 시
yarn add --dev jest @testing-library/react @testing-library/jest-dom @babel/preset-env @babel/preset-react babel-jest
jest: JavaScript 테스트 프레임워크입니다.@testing-library/react: React 컴포넌트 테스트를 위한 RTL의 핵심 라이브러리입니다.@testing-library/jest-dom: Jest의 matcher를 확장하여 DOM 관련 어설션 기능을 추가해 줍니다 (예:toBeInTheDocument()).@babel/preset-env,@babel/preset-react: Jest가 JSX나 최신 JavaScript 문법을 이해하도록 Babel 프리셋을 사용합니다.babel-jest: Jest가 Babel을 사용하여 파일을 트랜스파일하도록 돕는 모듈입니다.
Babel 설정 (`babel.config.js` 또는 `package.json`)
Jest가 JSX 문법 등을 올바르게 해석하려면 Babel 설정이 필요합니다. 프로젝트 루트에 `babel.config.js` 파일을 생성하고 다음과 같이 작성하거나, `package.json`에 `babel` 필드를 추가해 주세요.
`babel.config.js` 파일 생성:
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-react', { runtime: 'automatic' }], // React 17+
],
};
`package.json`에 직접 추가하는 경우:
// package.json
{
// ...
"babel": {
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }],
["@babel/preset-react", { "runtime": "automatic" }]
]
},
// ...
}
여기서 "runtime": "automatic"은 React 17 이상에서 JSX 트랜스파일을 위해 별도로 import React from 'react'를 하지 않아도 되도록 해줍니다.
Jest 설정 (`jest.config.js` 또는 `package.json`)
Jest의 설정을 조정하여 RTL과 함께 더 잘 작동하도록 할 수 있습니다. 프로젝트 루트에 `jest.config.js` 파일을 생성하거나, `package.json`에 `jest` 필드를 추가해 주세요.
`jest.config.js` 파일 생성:
// jest.config.js
module.exports = {
testEnvironment: 'jsdom', // 브라우저 환경을 시뮬레이션
setupFilesAfterEnv: ['/src/setupTests.js'], // 테스트 실행 전 환경 설정 파일
moduleNameMapper: {
'\\.(css|less|sass|scss)$': 'identity-obj-proxy', // CSS 모듈 처리
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', // Babel을 사용하여 JS/TS 파일 트랜스파일
},
};
`package.json`에 직접 추가하는 경우:
// package.json
{
// ...
"jest": {
"testEnvironment": "jsdom",
"setupFilesAfterEnv": ["/src/setupTests.js"],
"moduleNameMapper": {
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
},
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
}
},
// ...
}
testEnvironment: 'jsdom': Jest가 Node.js 환경에서 DOM을 시뮬레이션할 수 있도록 합니다. React 컴포넌트는 DOM에 렌더링되므로 이 설정이 필수적입니다.setupFilesAfterEnv: ['<rootDir>/src/setupTests.js']: 모든 테스트 파일이 실행되기 전에 지정된 파일을 실행합니다. 여기에@testing-library/jest-dom을 임포트하여 Jest에 확장 matcher를 등록할 겁니다.moduleNameMapper: CSS 모듈 등 Jest가 직접 처리할 수 없는 파일을 mock 처리하기 위한 설정입니다.transform: Babel을 사용하여 JavaScript/TypeScript 파일을 트랜스파일하도록 지정합니다.
테스트 환경 설정 파일 (`src/setupTests.js`)
위에서 `setupFilesAfterEnv`에 지정했던 `src/setupTests.js` 파일을 생성하고 다음 내용을 추가합니다. 이 파일은 모든 테스트가 실행되기 전에 `@testing-library/jest-dom`을 불러와 Jest의 matcher 기능을 확장하는 역할을 해요.
// src/setupTests.js
import '@testing-library/jest-dom';
`package.json`에 테스트 스크립트 추가
마지막으로 `package.json`의 `scripts` 섹션에 테스트 명령어를 추가해두면 편리합니다.
// package.json
{
// ...
"scripts": {
"test": "jest",
"test:watch": "jest --watchAll",
"test:coverage": "jest --coverage"
},
// ...
}
npm test(또는yarn test): 모든 테스트를 한 번 실행합니다.npm run test:watch: 파일 변경을 감지하여 관련 테스트만 다시 실행합니다 (개발 중 유용).npm run test:coverage: 테스트 커버리지 보고서를 생성합니다.
이렇게 하면 React 프로젝트에서 Jest와 React Testing Library를 활용할 준비가 모두 끝났습니다! 이제 실제 테스트 코드를 작성해 볼 차례예요.
Image by Alexandra_Koch on Pixabay
실전 테스트 작성 가이드: 컴포넌트 단위 테스트
가장 기본이 되는 것은 역시 컴포넌트 단위 테스트입니다. React 애플리케이션은 컴포넌트의 조합으로 이루어져 있으니, 개별 컴포넌트가 예상대로 작동하는지 확인하는 것이 중요하죠. 여기서는 간단한 컴포넌트를 예시로 들어보겠습니다.
기본 컴포넌트 테스트하기 (렌더링 확인)
먼저, 사용자에게 메시지를 보여주는 간단한 `Greeting` 컴포넌트를 만들어 볼까요?
// src/components/Greeting.jsx
import React from 'react';
function Greeting({ name }) {
return <div>안녕하세요, {name}님!</div>;
}
export default Greeting;
이 컴포넌트가 제대로 렌더링되는지 테스트하는 코드를 작성해 보겠습니다. 테스트 파일은 보통 테스트할 컴포넌트와 같은 디렉토리 안에 `Greeting.test.jsx`와 같이 이름을 짓거나, `__tests__` 폴더 안에 위치시킵니다.
// src/components/Greeting.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
describe('Greeting 컴포넌트', () => {
test('props로 전달된 이름을 정확히 렌더링한다', () => {
// 1. 컴포넌트 렌더링
render(<Greeting name="React 개발자" />);
// 2. screen 객체를 사용하여 렌더링된 요소 찾기
// getByText는 정확한 텍스트를 찾습니다.
const greetingElement = screen.getByText('안녕하세요, React 개발자님!');
// 3. Jest matcher를 사용하여 검증
// toBeInTheDocument는 @testing-library/jest-dom에 의해 확장된 matcher입니다.
expect(greetingElement).toBeInTheDocument();
});
test('다른 이름을 전달해도 올바르게 렌더링한다', () => {
render(<Greeting name="테스터" />);
const greetingElement = screen.getByText('안녕하세요, 테스터님!');
expect(greetingElement).toBeInTheDocument();
});
});
코드를 보시면 render() 함수로 컴포넌트를 가상 DOM에 렌더링하고, screen 객체의 getByText() 쿼리 함수를 사용해서 렌더링된 텍스트 요소를 찾죠. 그리고 expect().toBeInTheDocument()로 해당 요소가 문서에 존재하는지 검증합니다. 정말 직관적이지 않나요?
사용자 인터랙션 테스트하기 (이벤트 처리)
이제 사용자의 클릭 이벤트에 반응하는 `Counter` 컴포넌트를 테스트해 볼까요?
// src/components/Counter.jsx
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(prevCount => prevCount + 1);
};
const handleDecrement = () => {
setCount(prevCount => prevCount - 1);
};
return (
<div>
<h1 data-testid="count-value">현재 카운트: {count}</h1>
<button onClick={handleIncrement}>증가</button>
<button onClick={handleDecrement}>감소</button>
</div>
);
}
export default Counter;
이 컴포넌트의 버튼 클릭 이벤트를 테스트하려면 fireEvent를 사용합니다.
// src/components/Counter.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
describe('Counter 컴포넌트', () => {
test('초기 카운트 값은 0이다', () => {
render(<Counter />);
// data-testid는 테스트를 위해 특별히 추가하는 속성입니다.
// 사용자가 보지 못하는 요소나, 텍스트가 자주 변경되는 요소에 유용해요.
const countElement = screen.getByTestId('count-value');
expect(countElement).toHaveTextContent('현재 카운트: 0');
});
test('증가 버튼을 클릭하면 카운트가 1 증가한다', () => {
render(<Counter />);
const incrementButton = screen.getByText('증가');
const countElement = screen.getByTestId('count-value');
// 클릭 이벤트 발생
fireEvent.click(incrementButton);
expect(countElement).toHaveTextContent('현재 카운트: 1');
// 한 번 더 클릭
fireEvent.click(incrementButton);
expect(countElement).toHaveTextContent('현재 카운트: 2');
});
test('감소 버튼을 클릭하면 카운트가 1 감소한다', () => {
render(<Counter />);
const decrementButton = screen.getByText('감소');
const countElement = screen.getByTestId('count-value');
// 초기 상태에서 감소 버튼 클릭
fireEvent.click(decrementButton);
expect(countElement).toHaveTextContent('현재 카운트: -1');
// 증가 후 감소
const incrementButton = screen.getByText('증가');
fireEvent.click(incrementButton); // 0
fireEvent.click(incrementButton); // 1
fireEvent.click(decrementButton); // 0
expect(countElement).toHaveTextContent('현재 카운트: 0');
});
});
여기서 fireEvent.click()은 실제 사용자가 버튼을 클릭하는 것과 동일한 방식으로 이벤트를 발생시킵니다. 그리고 toHaveTextContent()는 요소의 텍스트 내용을 검증하는 matcher입니다. data-testid는 테스트를 위해 특별히 추가하는 속성인데요, 텍스트 내용이 자주 바뀌거나 사용자에게 보이지 않는 요소들을 안정적으로 선택할 때 유용하게 쓰일 수 있습니다.
Props를 사용한 컴포넌트 테스트
이번에는 `props`에 따라 내용이 달라지는 `UserCard` 컴포넌트를 테스트해볼게요.
// src/components/UserCard.jsx
import React from 'react';
function UserCard({ user }) {
if (!user) {
return <div>사용자 정보가 없습니다.</div>;
}
return (
<div>
<h2>{user.name}</h2>
<p>이메일: {user.email}</p>
<p>역할: {user.role}</p>
</div>
);
}
export default UserCard;
// src/components/UserCard.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import UserCard from './UserCard';
describe('UserCard 컴포넌트', () => {
const mockUser = {
name: '김테스트',
email: 'test@example.com',
role: '개발자',
};
test('유효한 user props가 전달될 때 사용자 정보를 올바르게 렌더링한다', () => {
render(<UserCard user={mockUser} />);
expect(screen.getByText('김테스트')).toBeInTheDocument();
expect(screen.getByText(/이메일: test@example.com/i)).toBeInTheDocument(); // 정규식 사용
expect(screen.getByText('역할: 개발자')).toBeInTheDocument();
});
test('user props가 없을 때 "사용자 정보가 없습니다." 메시지를 렌더링한다', () => {
render(<UserCard user={null} />); // 또는 <UserCard />
expect(screen.getByText('사용자 정보가 없습니다.')).toBeInTheDocument();
// mockUser의 이름이 렌더링되지 않음을 확인
expect(screen.queryByText('김테스트')).not.toBeInTheDocument();
});
});
여기서는 screen.getByText()와 screen.queryByText()를 사용했습니다. getBy 계열 쿼리는 요소를 찾지 못하면 에러를 발생시키지만, queryBy 계열 쿼리는 요소를 찾지 못해도 `null`을 반환합니다. 따라서 특정 요소가 존재하지 않아야 할 때 queryBy를 사용하면 테스트가 실패하지 않고 `null`을 반환하기 때문에 not.toBeInTheDocument()와 함께 사용하기 좋습니다.
비동기 처리와 Mocking: 실제 환경처럼 테스트하기
실제 프론트엔드 애플리케이션에서는 API 호출과 같은 비동기 작업이 빈번하게 발생합니다. 이러한 비동기 코드를 테스트하는 것은 매우 중요하지만, 동시에 까다롭기도 해요. 외부 API에 실제로 의존하여 테스트를 실행하면 테스트가 느려지고, 네트워크 환경에 따라 불안정해질 수 있거든요. 이때 Mocking(목킹) 기법이 유용하게 사용됩니다.
비동기 코드 테스트 (API 호출 시나리오)
데이터를 불러와서 화면에 표시하는 `UserList` 컴포넌트를 예로 들어볼게요.
// src/components/UserList.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios'; // axios는 npm install axios로 설치했다고 가정합니다.
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
setUsers(response.data);
} catch (err) {
setError('사용자 정보를 불러오는데 실패했습니다.');
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <div>사용자 목록을 불러오는 중...</div>;
if (error) return <div data-testid="error-message">{error}</div>;
return (
<div>
<h2>사용자 목록</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
</div>
);
}
export default UserList;
이 컴포넌트를 테스트할 때는 `axios.get` 호출을 목킹하여 실제 네트워크 요청 없이 가짜 데이터를 반환하도록 해야 합니다. Jest의 목킹 기능과 RTL의 `findBy` 계열 쿼리, `waitFor`를 활용해 보겠습니다.
// src/components/UserList.test.jsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import axios from 'axios';
import UserList from './UserList';
// axios 모듈 전체를 목킹합니다.
jest.mock('axios');
describe('UserList 컴포넌트', () => {
const mockUsers = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
test('API 호출 후 사용자 목록을 올바르게 렌더링한다', async () => {
// 목킹된 axios.get이 특정 데이터를 반환하도록 설정
axios.get.mockResolvedValueOnce({ data: mockUsers });
render(<UserList />);
// 로딩 메시지 확인 (처음에는 보임)
expect(screen.getByText('사용자 목록을 불러오는 중...')).toBeInTheDocument();
// 비동기 작업이 완료될 때까지 기다리고, 특정 요소가 나타나는지 확인
// findByText는 기본적으로 요소가 나타날 때까지 기다립니다.
const userAlice = await screen.findByText('Alice (alice@example.com)');
expect(userAlice).toBeInTheDocument();
expect(screen.getByText('Bob (bob@example.com)')).toBeInTheDocument();
// 로딩 메시지가 사라졌는지 확인
expect(screen.queryByText('사용자 목록을 불러오는 중...')).not.toBeInTheDocument();
});
test('API 호출 실패 시 에러 메시지를 렌더링한다', async () => {
// API 호출이 실패하도록 목킹
axios.get.mockRejectedValueOnce(new Error('네트워크 에러'));
render(<UserList />);
// 에러 메시지가 나타날 때까지 기다림
const errorMessage = await screen.findByTestId('error-message');
expect(errorMessage).toHaveTextContent('사용자 정보를 불러오는데 실패했습니다.');
// 사용자 목록은 렌더링되지 않아야 함
expect(screen.queryByText('사용자 목록')).not.toBeInTheDocument();
});
});
여기서 중요한 부분은:
jest.mock('axios'): `axios` 모듈 전체를 가짜(mock) 모듈로 대체합니다.axios.get.mockResolvedValueOnce({ data: mockUsers }): `axios.get` 함수가 호출될 때 한 번만 특정 성공 값을 반환하도록 설정합니다. `mockRejectedValueOnce`는 실패 시 사용하죠.await screen.findByText(): 비동기적으로 요소가 나타날 때까지 기다립니다. `findBy` 계열 쿼리는 요소가 나타날 때까지 기본적으로 1000ms를 기다려줍니다.waitFor(): 특정 조건이 충족될 때까지 기다려야 할 때 명시적으로 사용할 수 있습니다. 예를 들어, 로딩 스피너가 사라질 때까지 기다리는 등의 상황에 유용합니다.
외부 모듈 Mocking (예: React Router)
React Router의 `useNavigate`나 `useParams` 같은 훅을 사용하는 컴포넌트를 테스트할 때도 목킹이 필요합니다. 실제 라우터 환경이 없기 때문이죠. 예를 들어, `useNavigate`를 사용하는 `GoBackButton` 컴포넌트가 있다고 해봅시다.
// src/components/GoBackButton.jsx
import React from 'react';
import { useNavigate } from 'react-router-dom'; // react-router-dom 설치했다고 가정
function GoBackButton() {
const navigate = useNavigate();
const handleClick = () => {
navigate(-1); // 이전 페이지로 이동
};
return (
<button onClick={handleClick}>뒤로 가기</button>
);
}
export default GoBackButton;
이 컴포넌트를 테스트하려면 `react-router-dom`의 `useNavigate` 훅을 목킹해야 합니다.
// src/components/GoBackButton.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import GoBackButton from './GoBackButton';
// react-router-dom 모듈을 목킹합니다.
// useNavigate 훅이 반환할 가짜 함수를 정의합니다.
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), // 실제 모듈의 다른 부분은 그대로 사용
useNavigate: () => mockNavigate, // useNavigate만 목킹된 함수로 대체
}));
describe('GoBackButton 컴포넌트', () => {
afterEach(() => {
// 각 테스트가 끝날 때마다 목킹 함수 호출 기록을 초기화합니다.
mockNavigate.mockClear();
});
test('버튼 클릭 시 navigate(-1)이 호출된다', () => {
render(<GoBackButton />);
const button = screen.getByText('뒤로 가기');
fireEvent.click(button);
// mockNavigate 함수가 호출되었는지, 그리고 어떤 인자로 호출되었는지 검증
expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(-1);
});
});
jest.mock('react-router-dom', ...)을 사용하여 `react-router-dom` 모듈 전체를 목킹했습니다. 특히 jest.requireActual('react-router-dom')를 사용하여 실제 모듈의 나머지 부분은 유지하고, `useNavigate` 훅만 우리가 만든 `mockNavigate` 함수로 대체하는 방식입니다. jest.fn()은 호출 여부, 호출 횟수, 인자 등을 추적할 수 있는 가짜 함수를 만들어줍니다.
Image by lukasmilan on Pixabay
Jest와 RTL, 더 효과적으로 활용하는 팁
Jest와 React Testing Library를 활용하는 데 도움이 되는 몇 가지 팁을 더 알려드릴게요.
스냅샷 테스트의 활용과 주의점
스냅샷 테스트(Snapshot Testing)는 컴포넌트의 렌더링된 출력(주로 DOM 구조)을 파일로 저장하고, 다음 테스트 실행 시 저장된 스냅샷과 현재 렌더링된 출력을 비교하는 방식입니다. UI가 의도치 않게 변경되었는지 빠르게 감지하는 데 유용합니다.
// src/components/Greeting.test.jsx (스냅샷 테스트 추가)
import React from 'react';
import { render } from '@testing-library/react';
import Greeting from './Greeting';
describe('Greeting 컴포넌트 (스냅샷)', () => {
test('Greeting 컴포넌트가 변경되지 않았는지 확인한다', () => {
const { asFragment } = render(<Greeting name="스냅샷 테스트" />);
// asFragment()는 렌더링된 DOM 트리를 DocumentFragment로 반환합니다.
// toMatchSnapshot()은 첫 실행 시 스냅샷 파일을 생성하고, 이후에는 비교합니다.
expect(asFragment()).toMatchSnapshot();
});
});
이 테스트를 처음 실행하면 `__snapshots__` 폴더에 `.snap` 파일이 생성됩니다. 이후 코드를 변경하고 테스트를 다시 실행했을 때, 스냅샷과 현재 출력이 다르면 테스트가 실패하죠. 이때 `npm test -- -u` (또는 `yarn test -- -u`) 명령어로 스냅샷을 업데이트할 수 있습니다.
주의점: 스냅샷 테스트는 UI의 의도치 않은 변경을 감지하는 데 좋지만, 너무 많은 스냅샷은 유지보수 비용을 증가시킬 수 있습니다. 작은 변경에도 스냅샷을 계속 업데이트해야 할 수 있기 때문이죠. 따라서 핵심 UI 컴포넌트나 레이아웃에 제한적으로 사용하는 것이 좋습니다.
테스트 커버리지 확인 및 관리
테스트 커버리지(Test Coverage)는 작성된 테스트 코드가 전체 소스 코드 중 얼마나 많은 부분을 실행했는지 측정하는 지표입니다. `package.json`에 추가했던 "test:coverage": "jest --coverage" 스크립트를 실행해 보세요.
npm run test:coverage
# 또는
yarn test --coverage
명령어를 실행하면 터미널에 보고서가 출력되고, `coverage` 폴더에 HTML 보고서가 생성됩니다. 이 보고서를 통해 어떤 파일, 함수, 라인이 테스트되었는지 또는 테스트되지 않았는지 시각적으로 확인할 수 있습니다.
주의점: 높은 테스트 커버리지 숫자가 항상 좋은 테스트 품질을 의미하는 것은 아닙니다. 단순히 코드를 한 번 실행만 하는 "얕은" 테스트가 많다면 커버리지는 높지만 실제 버그를 잡지 못할 수도 있거든요. 중요한 것은 의미 있는 시나리오를 커버하는 테스트를 작성하는 것입니다. 일반적으로 80% 이상의 커버리지를 목표로 하지만, 프로젝트의 특성과 중요도에 따라 유연하게 접근하는 것이 좋습니다.
테스트 파일 구조화
테스트 파일은 프로젝트의 규모가 커질수록 많아지기 때문에, 체계적으로 관리하는 것이 중요합니다. 일반적으로 다음과 같은 방식을 사용합니다.
- 컴포넌트/모듈 옆에 위치: `src/components/Button.jsx`가 있다면, `src/components/Button.test.jsx`와 같이 같은 디렉토리에 둡니다. 이는 관련 파일을 찾기 쉽게 해줍니다.
- `__tests__` 폴더 사용: `src/components/__tests__/Button.test.jsx`와 같이 특정 디렉토리에 모든 테스트 파일을 모아두는 방식입니다. 이는 테스트 파일과 실제 코드를 시각적으로 분리하는 데 도움이 됩니다.
- `.test.js`, `.spec.js` 확장자 사용: Jest는 기본적으로 `.test.js`, `.spec.js`, 그리고 `__tests__` 폴더 내의 파일을 테스트 파일로 인식합니다. 일관된 확장자 명명 규칙을 따르는 것이 좋습니다.
어떤 방식을 선택하든 팀 내에서 일관된 규칙을 정하고 따르는 것이 중요합니다. 잘 구조화된 테스트 파일은 나중에 다른 개발자가 테스트 코드를 이해하고 유지보수하는 데 큰 도움이 될 겁니다.
마무리하며: 견고한 프론트엔드의 시작
지금까지 Jest와 React Testing Library를 활용하여 프론트엔드 테스트 환경을 구축하고, 실제 컴포넌트와 비동기 코드를 테스트하는 방법에 대해 자세히 알아보았습니다. 어떠셨나요? 처음에는 테스트 코드를 작성하는 것이 어렵고 시간이 많이 드는 일처럼 느껴질 수 있지만, 한번 익숙해지면 개발 과정에서 얻는 이점이 훨씬 크다는 것을 느끼실 거예요.
테스트는 단순히 버그를 찾는 것을 넘어, 코드의 품질을 높이고, 리팩토링을 용이하게 하며, 팀원 간의 협업을 강화하는 강력한 도구입니다. 사용자에게 더 안정적이고 신뢰할 수 있는 서비스를 제공하기 위한 필수적인 과정이라고 할 수 있죠. 여러분의 React 프로젝트에 지금 바로 Jest와 React Testing Library를 도입하여, 더욱 견고하고 유지보수하기 쉬운 프론트엔드를 만들어나가시길 바랍니다.
이 글이 여러분의 프론트엔드 개발 여정에 조금이나마 도움이 되었기를 진심으로 바라며, 궁금한 점이나 공유하고 싶은 테스트 노하우가 있다면 언제든지 댓글로 남겨주세요! 함께 성장해 나가는 개발자 커뮤니티를 만들어가요!
📌 함께 읽으면 좋은 글
- [클라우드 인프라] Terraform과 GitOps로 구현하는 클라우드 인프라 자동화 전략: 효율적인 배포와 관리
- [커리어 취업] 개발자 연봉 협상 성공 전략: 커리어 가치를 높이는 실전 가이드
- [튜토리얼] Next.js App Router 기반 풀스택 개발: 서버 컴포넌트와 데이터 페칭 심층 분석
이 글이 도움이 되셨다면 공감(♥)과 댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.
'튜토리얼' 카테고리의 다른 글
| Go 언어 RESTful API 서버 구축: Gin 프레임워크 실전 가이드 (0) | 2026.05.26 |
|---|---|
| Docker Compose 활용 다중 컨테이너 애플리케이션 개발 환경 구축 상세 가이드 (0) | 2026.05.25 |
| GitHub Actions CI/CD 파이프라인 구축: 테스트, 빌드, 배포 자동화 실전 가이드 (0) | 2026.05.24 |
| Docker Compose 활용 다중 서비스 로컬 개발 환경 구축 완벽 가이드 (0) | 2026.05.23 |
| Next.js App Router 기반 풀스택 개발: 서버 컴포넌트와 데이터 페칭 심층 분석 (0) | 2026.05.23 |