테스팅 (Testing) 소개
Next.js 프로젝트에서 기본적인 유닛 및 통합 테스트를 소개합니다.
Next.js 기초 입문 27회차: 테스팅 (Testing) 소개
이번 27회차에서는 Next.js 프로젝트의 견고성을 확보하는 데 필수적인 테스팅(Testing)의 기본 개념과 실제 적용 방법에 대해 학습합니다. 애플리케이션의 신뢰성을 높이고 개발 과정에서 발생할 수 있는 오류를 사전에 방지하기 위한 유닛 및 통합 테스트의 중요성을 이해하고, 널리 사용되는 테스팅 도구인 Jest와 React Testing Library를 활용하여 테스트 코드를 작성하는 방법을 실습합니다.
📌 이번 회차 학습 목표
- Next.js 프로젝트에서 테스팅의 중요성을 설명할 수 있습니다.
- 유닛 테스트와 통합 테스트의 차이점을 이해하고 구분할 수 있습니다.
- Jest와 React Testing Library를 사용하여 기본적인 컴포넌트 테스트를 작성할 수 있습니다.
- Next.js 환경에서 테스트 환경을 설정하고 실행할 수 있습니다.
- 테스트 주도 개발(TDD)의 개념을 이해하고 테스트 코드의 가치를 인식할 수 있습니다.
📝 개념 설명
테스팅(Testing)이란 무엇인가?
테스팅은 소프트웨어의 기능이 예상대로 작동하는지, 오류는 없는지 검증하는 과정입니다. Next.js와 같은 웹 애플리케이션 개발에서 테스팅은 사용자 경험을 향상시키고, 유지보수 비용을 절감하며, 코드의 안정성을 보장하는 데 결정적인 역할을 합니다. 특히 복잡한 애플리케이션에서는 수동 테스트만으로는 모든 경우의 수를 검증하기 어렵기 때문에 자동화된 테스트가 필수적입니다.
테스트의 종류: 유닛 테스트와 통합 테스트
소프트웨어 테스팅은 다양한 수준에서 이루어질 수 있으며, 그중 가장 기본적인 두 가지는 유닛 테스트(Unit Test)와 통합 테스트(Integration Test)입니다.
유닛 테스트 (Unit Test)
- 정의: 애플리케이션의 가장 작은 단위(함수, 컴포넌트, 모듈 등)가 독립적으로 올바르게 작동하는지 검증하는 테스트입니다.
- 목표: 개별 코드 조각의 정확성을 보장하고, 변경 사항이 다른 부분에 미치는 영향을 최소화합니다.
- 특징: 빠르고 격리된 환경에서 실행되며, 특정 유닛의 로직에 초점을 맞춥니다. 외부 의존성(API 호출, 데이터베이스 등)은 Mocking(모의 객체)을 통해 대체하는 경우가 많습니다.
통합 테스트 (Integration Test)
- 정의: 여러 유닛이나 모듈이 함께 작동할 때 예상대로 상호작용하는지 검증하는 테스트입니다.
- 목표: 컴포넌트 간의 인터페이스나 데이터 흐름이 올바른지 확인합니다.
- 특징: 유닛 테스트보다 느리지만, 실제 사용자 시나리오에 더 가깝게 동작을 검증합니다. Next.js 애플리케이션에서는 여러 컴포넌트가 결합된 페이지나 특정 기능의 흐름을 테스트하는 데 사용됩니다.
| 항목 | 유닛 테스트 | 통합 테스트 |
|---|---|---|
| 범위 | 가장 작은 코드 단위 (함수, 컴포넌트) | 여러 유닛 간의 상호작용, 모듈 결합 |
| 목표 | 개별 로직의 정확성 검증 | 컴포넌트 간의 데이터 흐름 및 인터페이스 검증 |
| 속도 | 빠름 | 유닛 테스트보다 느림 |
| 격리성 | 높음 (외부 의존성 Mocking) | 낮음 (실제 환경에 가깝게 동작) |
| 예시 | 특정 함수가 올바른 값을 반환하는지, 단일 컴포넌트가 렌더링되는지 | 로그인 기능 전체 흐름, API 호출 후 UI 업데이트 |
테스팅 도구: Jest와 React Testing Library
Next.js 프로젝트에서 React 컴포넌트를 테스트하기 위해 가장 널리 사용되는 두 가지 도구는 Jest와 React Testing Library입니다.
Jest
- 정의: Facebook에서 개발한 JavaScript 테스팅 프레임워크입니다.
- 특징: 강력한 테스트 러너, 단언(assertion) 라이브러리, Mocking 기능 등을 포함한 올인원 솔루션입니다. 설정이 간편하고 빠른 실행 속도를 제공합니다.
- 주요 기능:
describe(테스트 그룹),it또는test(개별 테스트),expect(기대값),toBe,toEqual(매처) 등.
React Testing Library (RTL)
- 정의: React 컴포넌트를 테스트하기 위한 라이브러리입니다.
- 특징: 실제 사용자가 컴포넌트와 상호작용하는 방식과 유사하게 테스트를 작성하도록 권장합니다. 내부 구현보다는 사용자 관점에서 컴포넌트의 동작을 검증하는 데 중점을 둡니다.
- 주요 기능:
render(컴포넌트 렌더링),screen.getByText,screen.getByRole(요소 쿼리),fireEvent(이벤트 발생) 등.
💡 예제 & 실습
Next.js 프로젝트에서 Jest와 React Testing Library를 설정하고 간단한 컴포넌트를 테스트하는 방법을 단계별로 살펴보겠습니다.
1단계: Next.js 프로젝트 생성 및 테스트 환경 설정
먼저 새로운 Next.js 프로젝트를 생성하고 필요한 테스팅 라이브러리를 설치합니다.
npx create-next-app@latest my-next-app --typescript --eslint
cd my-next-app
npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
설치 후, package.json 파일에 테스트 스크립트를 추가합니다.
// package.json
{
"name": "my-next-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest --watchAll" // 이 라인을 추가합니다.
},
"dependencies": {
"next": "14.1.0",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}
다음으로, Jest 설정을 위한 jest.config.js 파일을 프로젝트 루트에 생성합니다.
// jest.config.js
const nextJest = require('next/jest');
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
});
// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
// Handle module aliases (this will be automatically configured for you soon)
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/pages/(.*)$': '<rootDir>/pages/$1',
},
testEnvironment: 'jest-environment-jsdom',
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig);
그리고 jest.setup.js 파일을 프로젝트 루트에 생성하여 @testing-library/jest-dom을 설정합니다. 이 라이브러리는 Jest 매처를 확장하여 DOM 관련 단언을 더 쉽게 할 수 있도록 돕습니다.
// jest.setup.js
import '@testing-library/jest-dom';
2단계: 테스트할 컴포넌트 생성
components/Button.tsx 파일을 생성하여 간단한 버튼 컴포넌트를 만듭니다.
// components/Button.tsx
import React from 'react';
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
}
const Button: React.FC<ButtonProps> = ({ children, onClick }) => {
return (
<button onClick={onClick}>
{children}
</button>
);
};
export default Button;
3단계: 컴포넌트 테스트 작성
components/__tests__/Button.test.tsx 파일을 생성하여 Button 컴포넌트에 대한 테스트 코드를 작성합니다. 테스트 파일은 일반적으로 테스트 대상 파일과 동일한 디렉토리 내의 __tests__ 폴더에 위치하거나, .test.tsx 또는 .spec.tsx 접미사를 가집니다.
// components/__tests__/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '../Button';
describe('Button Component', () => {
it('renders with correct text', () => {
render(<Button>Click Me</Button>);
const buttonElement = screen.getByText(/Click Me/i);
expect(buttonElement).toBeInTheDocument();
});
it('calls onClick prop when clicked', () => {
const handleClick = jest.fn(); // Mock 함수 생성
render(<Button onClick={handleClick}>Click Me</Button>);
const buttonElement = screen.getByText(/Click Me/i);
fireEvent.click(buttonElement);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('renders multiple children correctly', () => {
render(
<Button>
<span>Hello</span>
<strong>World</strong>
</Button>
);
expect(screen.getByText('Hello')).toBeInTheDocument();
expect(screen.getByText('World')).toBeInTheDocument();
});
});
4단계: 테스트 실행
터미널에서 다음 명령어를 실행하여 테스트를 시작합니다.
npm test
--watchAll 옵션 덕분에 파일 변경 시 자동으로 테스트가 재실행됩니다. 모든 테스트가 성공적으로 통과하면 다음과 유사한 결과가 출력됩니다.
PASS components/__tests__/Button.test.tsx
Button Component
✓ renders with correct text (25 ms)
✓ calls onClick prop when clicked (10 ms)
✓ renders multiple children correctly (5 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.234 s
Ran all test suites.
⚠️ 자주 틀리는 것 / 주의사항
- 내부 구현 테스트 지양: React Testing Library는 컴포넌트의 내부 상태나 메서드 호출보다는 사용자 관점에서 컴포넌트가 어떻게 동작하는지에 초점을 맞춥니다. 예를 들어, 특정 CSS 클래스가 적용되었는지보다는 해당 클래스가 시각적으로 어떤 영향을 미치는지(예: 텍스트 색상 변경)를 테스트하는 것이 좋습니다.
screen.getByText,screen.getByRole등 사용자에게 보이는 요소를 쿼리하는 방법을 우선적으로 사용하세요. - 비동기 코드 처리: API 호출 등 비동기 로직이 포함된 컴포넌트를 테스트할 때는
async/await와waitFor,findBy*쿼리를 사용하여 비동기 작업이 완료될 때까지 기다려야 합니다. 그렇지 않으면 테스트가 실패하거나 예상치 못한 결과를 초래할 수 있습니다. - Mocking의 적절한 사용: 외부 API 호출, 데이터베이스 접근 등 테스트 환경에서 실제 실행하기 어려운 의존성은 Mocking을 통해 대체해야 합니다. Jest의
jest.fn(),jest.mock()등을 활용하여 의존성을 격리하고 유닛 테스트의 속도와 안정성을 유지합니다. 과도한 Mocking은 실제 통합 문제를 놓칠 수 있으므로, 통합 테스트에서는 실제 의존성을 사용하는 것을 고려해야 합니다. - 테스트 커버리지 맹신 금지: 높은 테스트 커버리지(코드의 많은 부분이 테스트되었다는 지표)가 항상 좋은 테스트를 의미하지는 않습니다. 중요한 것은 코드의 핵심 로직과 사용자 시나리오를 얼마나 잘 커버하는지입니다. 의미 없는 테스트는 유지보수 비용만 증가시킬 수 있습니다.
🔗 다음 회차 예고
이번 회차에서는 Next.js 프로젝트에서 테스팅의 기본 개념과 Jest, React Testing Library를 활용한 유닛/통합 테스트 작성 방법을 학습했습니다. 다음 28회차에서는 배포 (Deployment)에 대해 다룰 예정입니다. 개발된 Next.js 애플리케이션을 Vercel과 같은 플랫폼에 배포하는 과정과 배포 전략, 환경 변수 관리 등에 대해 심도 있게 학습할 것입니다. 이번 회차에서 작성한 견고한 애플리케이션을 실제 서비스로 전환하는 방법을 기대해 주세요!
- 테스팅은 소프트웨어의 기능 검증을 통해 안정성과 신뢰성을 확보하는 과정입니다.
- 유닛 테스트는 작은 코드 단위의 정확성을, 통합 테스트는 여러 유닛 간의 상호작용을 검증합니다.
- Jest는 JavaScript 테스팅 프레임워크로 테스트 실행, 단언, Mocking 기능을 제공합니다.
- React Testing Library는 사용자 관점에서 React 컴포넌트의 동작을 테스트하는 데 중점을 둡니다.
- Next.js 프로젝트에서 Jest와 RTL을 설정하고 사용하여 컴포넌트의 렌더링 및 이벤트 처리를 테스트할 수 있습니다.
- 테스트 작성 시 내부 구현보다는 사용자 경험에 초점을 맞추고, 비동기 처리 및 Mocking을 적절히 활용하는 것이 중요합니다.