Next.js에서 Jest 사용법 완전 가이드

주요 개념

Next.js에서 Jest는 컴포넌트 테스트, API 라우트 테스트, 서버 사이드 함수 테스트를 포괄하는 종합 테스트 솔루션이다. Next.js 12 이상에서는 내장 Rust 컴파일러를 통해 복잡한 설정 없이 Jest를 바로 사용할 수 있으며, TypeScript 지원과 다양한 모킹 기능을 제공한다.

1. Next.js 프로젝트에서 Jest 설치 및 설정

1.1 빠른 시작 (공식 템플릿 사용)

npx create-next-app@latest --example with-jest with-jest-app

1.2 수동 설치 (기존 프로젝트)

npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node @types/jest

1.3 Jest 설정 파일 생성

jest.config.ts 파일을 프로젝트 루트에 생성:

import type { Config } from 'jest'
import nextJest from 'next/jest.js'
 
const createJestConfig = nextJest({
  dir: './',  // Next.js 설정과 .env 파일 로드용 경로
})
 
const config: Config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',  // DOM 환경 시뮬레이션
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',  // 절대 경로 별칭 설정
  },
  testMatch: [
    '**/__tests__/**/*.(test|spec).ts?(x)',
    '**/*.(test|spec).ts?(x)'
  ]
}
 
export default createJestConfig(config)

1.4 Jest 설정 파일 생성

jest.setup.ts 파일:

import '@testing-library/jest-dom'

2. 컴포넌트 테스트

2.1 기본 컴포넌트 테스트

// components/Button.tsx
interface ButtonProps {
  onClick: () => void;
  children: React.ReactNode;
}
 
export const Button: React.FC<ButtonProps> = ({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
);
 
// __tests__/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '../components/Button'
 
describe('Button Component', () => {
  it('renders button with correct text', () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    expect(screen.getByRole('button')).toHaveTextContent('Click me')
  })
  
  it('calls onClick when clicked', () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    fireEvent.click(screen.getByRole('button'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

2.2 스냅샷 테스트

UI 컴포넌트의 예상치 못한 변화를 감지:

import { render } from '@testing-library/react'
import { HomePage } from '../pages/index'
 
test('HomePage snapshot', () => {
  const { asFragment } = render(<HomePage />)
  expect(asFragment()).toMatchSnapshot()
})

3. API 라우트 테스트

3.1 GET 요청 테스트

// app/api/users/route.ts
import { NextResponse } from 'next/server'
 
export async function GET() {
  const users = [
    { id: 1, name: 'John Doe' },
    { id: 2, name: 'Jane Smith' }
  ]
  return NextResponse.json(users, { status: 200 })
}
 
// app/api/users/route.test.ts
/**
 * @jest-environment node
 */
import { GET } from './route'
 
describe('/api/users', () => {
  it('returns users list', async () => {
    const response = await GET()
    const data = await response.json()
    
    expect(response.status).toBe(200)
    expect(data).toHaveLength(2)
    expect(data[^0]).toHaveProperty('name', 'John Doe')
  })
})

3.2 POST 요청 테스트

// app/api/users/route.ts
export async function POST(request: Request) {
  const body = await request.json()
  const newUser = { id: Date.now(), ...body }
  return NextResponse.json(newUser, { status: 201 })
}
 
// route.test.ts
import { POST } from './route'
 
it('creates new user', async () => {
  const mockRequest = new Request('http://localhost:3000/api/users', {
    method: 'POST',
    body: JSON.stringify({ name: 'New User' }),
    headers: { 'Content-Type': 'application/json' }
  })
  
  const response = await POST(mockRequest)
  const data = await response.json()
  
  expect(response.status).toBe(201)
  expect(data.name).toBe('New User')
})

4. getServerSideProps 테스트

4.1 컨텍스트 모킹

// pages/user/[id].page.tsx
export const getServerSideProps: GetServerSideProps = async (context) => {
  const { id } = context.params!
  // API 호출 로직...
  return {
    props: {
      userId: id,
      userData: mockUserData
    }
  }
}
 
// pages/user/[id].test.tsx
import { getServerSideProps } from './[id].page'
import type { GetServerSidePropsContext } from 'next'
 
describe('getServerSideProps', () => {
  it('returns correct props', async () => {
    const context = {
      params: { id: '123' },
      query: {},
      req: {},
      res: {}
    } as GetServerSidePropsContext
    
    const result = await getServerSideProps(context)
    
    expect(result).toEqual({
      props: {
        userId: '123',
        userData: expect.any(Object)
      }
    })
  })
})

5. 모킹(Mocking) 기법

5.1 외부 API 모킹

// lib/api.ts
export const fetchUserData = async (id: string) => {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}
 
// __tests__/api.test.ts
import { fetchUserData } from '../lib/api'
 
// fetch 모킹
global.fetch = jest.fn()
 
describe('fetchUserData', () => {
  beforeEach(() => {
    (fetch as jest.Mock).mockClear()
  })
  
  it('fetches user data successfully', async () => {
    const mockUser = { id: '1', name: 'John' };
    (fetch as jest.Mock).mockResolvedValue({
      json: () => Promise.resolve(mockUser)
    })
    
    const result = await fetchUserData('1')
    expect(result).toEqual(mockUser)
    expect(fetch).toHaveBeenCalledWith('/api/users/1')
  })
})

5.2 Next.js 훅 모킹

// __tests__/components/Navigation.test.tsx
import { useRouter } from 'next/router'
import { Navigation } from '../components/Navigation'
 
jest.mock('next/router', () => ({
  useRouter: jest.fn()
}))
 
describe('Navigation', () => {
  it('highlights current page', () => {
    const mockPush = jest.fn();
    (useRouter as jest.Mock).mockReturnValue({
      pathname: '/about',
      push: mockPush
    })
    
    render(<Navigation />)
    expect(screen.getByText('About')).toHaveClass('active')
  })
})

6. 환경 변수 테스트 설정

6.1 환경 변수 로딩 설정

// setupEnv.ts
import { loadEnvConfig } from '@next/env'
 
export default async (): Promise<void> => {
  const projectDir = process.cwd()
  loadEnvConfig(projectDir)
}
 
// jest.config.ts에 추가
const config: Config = {
  globalSetup: '<rootDir>/setupEnv.ts',
  // ... 기타 설정
}

6.2 테스트용 환경 변수

# .env.test.local
NEXT_PUBLIC_API_URL=http://localhost:3000/api
DATABASE_URL=postgresql://test:test@localhost/testdb

7. 고급 테스트 패턴

7.1 App Router 서버 컴포넌트 테스트

// app/users/page.tsx (Server Component)
async function getUsers() {
  // 서버 사이드 데이터 페칭
  return fetch('https://api.example.com/users').then(r => r.json())
}
 
export default async function UsersPage() {
  const users = await getUsers()
  return <UsersList users={users} />
}
 
// __tests__/users.test.tsx
/**
 * @jest-environment node
 */
import UsersPage from '../app/users/page'
 
// MSW 또는 fetch mock 사용
jest.mock('https://api.example.com/users', () => 
  Promise.resolve([{ id: 1, name: 'Test User' }])
)
 
describe('UsersPage', () => {
  it('renders users from server', async () => {
    const page = await UsersPage()
    // 서버 컴포넌트 테스트 로직
  })
})

7.2 커스텀 렌더 함수

// test-utils.tsx
import { render, RenderOptions } from '@testing-library/react'
import { ThemeProvider } from 'styled-components'
 
const AllTheProviders = ({ children }: { children: React.ReactNode }) => (
  <ThemeProvider theme={mockTheme}>
    {children}
  </ThemeProvider>
)
 
const customRender = (ui: React.ReactElement, options?: RenderOptions) =>
  render(ui, { wrapper: AllTheProviders, ...options })
 
export * from '@testing-library/react'
export { customRender as render }

8. 실행 및 CI/CD 통합

8.1 package.json 스크립트

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:ci": "jest --ci --coverage --watchAll=false"
  }
}

8.2 GitHub Actions 설정

# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run test:ci

9. 베스트 프랙티스

9.1 테스트 구조

  • 단위 테스트: 개별 함수/컴포넌트 (*.test.tsx)
  • 통합 테스트: 페이지/API 라우트 (*.integration.test.tsx)
  • 스냅샷 테스트: UI 변경 감지용

9.2 성능 최적화

  • --runInBand: 직렬 실행으로 메모리 사용량 제어
  • --maxWorkers=4: 워커 수 제한
  • setupFilesAfterEnv보다 globalSetup 활용으로 초기화 비용 절감

9.3 모킹 전략

  • External API: MSW(Mock Service Worker) 활용
  • Next.js 훅: jest.mock() 사용
  • 환경 변수: .env.test.local 파일 분리

10. 트러블슈팅

10.1 공통 에러 해결

  • “Cannot find module”: moduleNameMapper 경로 별칭 확인
  • “ReferenceError: fetch is not defined”: Node.js 환경에서 global.fetch 모킹 필요
  • 환경 변수 undefined: loadEnvConfig 함수로 명시적 로딩

10.2 App Router vs Pages Router

  • App Router: 서버 컴포넌트는 @jest-environment node 주석 필요
  • Pages Router: 기존 방식대로 getServerSideProps 직접 테스트 가능

결론

Next.js에서 Jest는 프런트엔드부터 API, 서버 사이드 로직까지 전 영역 테스트를 지원하는 강력한 도구다. 내장 Rust 컴파일러를 활용하면 제로 설정에 가까운 환경에서 TypeScript와 함께 안정적인 테스트 환경을 구축할 수 있다. 컴포넌트 렌더링 테스트, API 라우트 검증, 서버 사이드 함수 모킹까지 체계적으로 접근하여 코드 품질과 안정성을 크게 향상시킬 수 있다.