Next.js에서 React Server Components 테스트
It looks like only the source line was provided. Could you please share the rest of the text you’d like translated? Once I have the full content, I’ll translate it into Korean while preserving the formatting, markdown, and code blocks.
React 서버 컴포넌트 이해하기
Before writing tests, it helps to understand what distinguishes RSCs from client components:
- No client‑side state or lifecycle methods. RSC는
useState,useEffect, 또는 브라우저 환경에 의존하는 다른 훅을 사용할 수 없습니다. - Direct server‑side data access. 그들은 최상위 레벨에서 데이터베이스 쿼리, 파일 읽기, 혹은
fetch호출을await할 수 있으며,useEffect‑기반 데이터 페칭이 필요하지 않습니다. - Zero client‑side JavaScript output. 컴포넌트 로직은 서버에 머무르고, 결과 HTML만 브라우저에 전송됩니다.
In Next.js’s app directory, a component is a server component unless it explicitly includes the 'use client' directive. This means the majority of your component tree — layouts, pages, and most data‑fetching components — will be RSCs.
RSC 테스트가 다른 이유
표준 React 테스트 도구인 @testing-library/react는 브라우저를 시뮬레이션하는 jsdom 환경에 컴포넌트를 마운트하여 동작합니다. RSC는 브라우저에서 실행되지 않기 때문에 jsdom이 적절한 환경이 아닙니다. 표준 render 함수로 RSC를 렌더링하려고 하면 즉시 실패하거나 오해를 일으키는 결과가 나올 수 있습니다.
계획해야 할 구체적인 과제
- 서버 컨텍스트. RSC는
cookies(),headers()와 같은 서버 전용 API나 직접적인 데이터베이스 접근에 의존할 수 있습니다. 이러한 API는 명시적인 모킹 없이 테스트 환경에 존재하지 않습니다. - 비동기 렌더링. 클래스 컴포넌트나 훅 기반 컴포넌트와 달리, RSC는 JSX를 반환하는
async함수입니다. 이는 테스트에서 렌더링하고 단언(assert)하는 방식에 영향을 줍니다. - Next.js 전용 API.
next/navigation의redirect()또는notFound(), 그리고next/headers모듈과 같은 함수들은 테스트 중 오류를 방지하기 위해 모킹이 필요합니다.
도구 및 설정
의존성
npm install --save-dev jest @testing-library/react @testing-library/jest-dom msw
TypeScript 프로젝트의 경우 ts-jest와 @types/jest도 함께 설치합니다.
Jest 설정
jsdom이 아니라 node 테스트 환경을 사용합니다. RSC는 서버에서 실행되기 때문입니다.
// jest.config.js
module.exports = {
testEnvironment: 'node',
moduleNameMapper: {
'^@/(.*)$': '/src/$1',
},
setupFilesAfterEnv: ['/jest.setup.js'],
};
// jest.setup.js
import '@testing-library/jest-dom';
Next.js 서버 API 모킹
테스트 실패를 방지하기 위해 여러 Next.js 모듈을 모킹해야 합니다. 프로젝트 루트에 __mocks__ 디렉터리를 만들고 다음 파일들을 추가합니다:
// __mocks__/next/headers.js
export const cookies = jest.fn(() => ({
get: jest.fn(),
set: jest.fn(),
}));
export const headers = jest.fn(() => new Headers());
// __mocks__/next/navigation.js
export const redirect = jest.fn();
export const notFound = jest.fn();
export const useRouter = jest.fn(() => ({
push: jest.fn(),
replace: jest.fn(),
}));
RSC 테스트 작성
테스트 대상 컴포넌트
다음은 사용자를 가져와서 리스트로 표시하는 간단한 RSC입니다:
// app/components/UserList.js
export default async function UserList() {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await res.json();
return (
<ul>
{users.map((user) => (
<li key={user.id}>- {user.name}</li>
))}
</ul>
);
}
테스트에서 RSC 렌더링
RSC는 async 함수이므로 결과를 렌더러에 전달하기 전에 await 해야 합니다. react-dom/server의 renderToString을 사용해 HTML 출력을 생성합니다:
import React from 'react';
import { renderToString } from 'react-dom/server';
import UserList from '@/components/UserList';
// Mock the global fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () =>
Promise.resolve([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
]),
})
);
describe('UserList', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders a list of users', async () => {
const component = await UserList();
const html = renderToString(component);
expect(html).toContain('John Doe');
expect(html).toContain('Jane Smith');
});
it('fetches data from the correct endpoint', async () => {
await UserList();
expect(fetch).toHaveBeenCalledWith(
'https://jsonplaceholder.typicode.com/users'
);
});
it('renders the correct number of items', async () => {
const component = await UserList();
const html = renderToString(component);
const listItems = (html.match(/<li>/g) || []).length;
expect(listItems).toBe(2);
});
});
컴포넌트를 async 함수처럼 호출하면 렌더링 전에 해결된 출력물을 얻을 수 있습니다. 이는 RSC가 JSX로 해결되는 프라미스를 반환하기 때문에 필요합니다. renderToString으로 해결된 JSX를 렌더링하면 프로덕션에서 서버가 컴포넌트를 처리하는 방식과 동일합니다.
복잡한 API 시나리오를 위한 MSW 사용
보다 현실적인 API 모킹—여러 엔드포인트, 오류 상태, 네트워크 지연—을 위해서는 fetch를 수동으로 스텁하는 것보다 MSW가 더 유지보수가 용이합니다:
// tests/mocks/handlers.js
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('https://jsonplaceholder.typicode.com/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
]);
}),
];
// tests/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// jest.setup.js (updated)
import { server } from './tests/mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
이 설정을 통해 테스트는 현실적인 모의 응답과 상호작용하게 되며, 테스트별로 핸들러를 오버라이드하여 오류나 엣지 케이스를 시뮬레이션할 수 있습니다:
it('handles API errors gracefully', async () => {
server.use(
http.get('https://jsonplaceholder.typicode.com/users', () => {
return new HttpResponse(null, { status: 500 });
})
);
await expect(UserList()).rejects.toThrow();
});
Next.js 서버 API를 사용하는 컴포넌트 테스트
컴포넌트가 쿠키나 헤더를 읽을 때는 앞서 만든 모크를 사용합니다:
// app/components/AuthenticatedGreeting.js
import { cookies } from 'next/headers';
export default async function AuthenticatedGreeting() {
const cookieStore = cookies();
const session = cookieStore.get('session-token');
if (!session) return <p>Please log in.</p>;
return <p>Welcome back!</p>;
}
// __tests__/AuthenticatedGreeting.test.js
import { cookies } from 'next/headers';
import AuthenticatedGreeting from '@/components/AuthenticatedGreeting';
import { renderToString } from 'react-dom/server';
jest.mock('next/headers');
describe('AuthenticatedGreeting', () => {
it('shows login prompt when no session exists', async () => {
cookies.mockReturnValue({ get: jest.fn(() => null) });
const component = await AuthenticatedGreeting();
const html = renderToString(component);
expect(html).toContain('Please log in.');
});
it('shows welcome message when session exists', async () => {
cookies.mockReturnValue({
get: jest.fn(() => ({ value: 'valid-token' })),
});
const component = await AuthenticatedGreeting();
const html = renderToString(component);
expect(html).toContain('Welcome back!');
});
});
일반적인 문제 디버깅
| Issue | Fix |
|---|---|
window is not defined | 테스트 환경이 jsdom으로 설정되어 있습니다. jest.config.js를 testEnvironment: 'node' 로 변경하세요. |
Cannot read properties of undefined on Next.js imports | 문제 모듈(next/headers, next/navigation 등)을 __mocks__ 폴더에 모킹하거나 테스트 파일 상단에 jest.mock()을 사용하세요. |
| Async component not rendering correctly | 컴포넌트를 renderToString에 전달하기 전에 await 하세요: const component = await MyComponent(props); renderToString(component); |
| Fetch mock not being called | global.fetch가 컴포넌트가 실행되기 전에 할당되었는지 확인하세요(예: beforeEach 안이나 describe 블록 상단). |
| Tests pass locally but fail in CI | CI에서 필요한 환경 변수가 모두 설정되어 있는지 확인하거나, 테스트 설정에서 명시적으로 모킹하세요. |
테스트할 항목 (그리고 건너뛸 항목)
RSC 전체가 전용 유닛 테스트가 필요한 것은 아닙니다. 실용적인 구분은 다음과 같습니다:
-
테스트할 가치가 있는 경우
- 데이터 변환 로직.
- 가져온 데이터나 서버 상태에 따라 조건부 렌더링.
- 오류 및 로딩 상태.
- 올바른 API 호출이 이루어지는지 여부.
-
통합 테스트 또는 E2E 테스트로 다루는 것이 좋은 경우
- 전체 렌더링 파이프라인.
- 레이아웃 구성 및 라우팅 동작.
- 실제 Next.js 서버가 필요한 모든 상황.
Playwright나 Cypress와 같은 도구가 엔드‑투‑엔드 시나리오에 더 적합합니다. Jest 유닛 테스트는 컴포넌트 로직을 격리된 상태에서 집중적으로 다루세요.
결론
React Server Components 테스트는 클라이언트 컴포넌트 테스트보다 크게 어렵지는 않으며, 다만 다른 도구와 약간 다른 사고 모델이 필요합니다. 핵심 변화는 다음과 같습니다:
jsdom대신node테스트 환경을 사용합니다.render()를 사용하지 않고 비동기 컴포넌트를 직접 호출합니다 (await MyComponent()).- Next.js 서버 API를 명시적으로 모킹합니다.
생태계가 성숙함에 따라 이 과정이 더 원활해질 것으로 기대됩니다. Next.js 팀과 React 코어 팀은 RSC를 위한 더 나은 테스트 기본 기능을 적극적으로 개발하고 있습니다. 현재는 위 패턴이 복잡한 인프라 없이도 가장 일반적인 시나리오를 포괄하는 신뢰할 수 있는 기반을 제공합니다.