통합 테스트 작성
상품 목록의 클라이언트 상태와 상호작용 경계를 Jest로 검증하고, 실제 Server Component 연결은 E2E로 확인하는 기준을 익힙니다.
소프트웨어 테스팅은 단위 테스트만으로는 충분하지 않습니다.
애플리케이션의 여러 부분이 함께 작동할 때 발생하는 문제를 발견하고 시스템 전체의 흐름을 검증하기 위해 통합 테스트(Integration Testing)가 필수적입니다.
통합 테스트는 개별적으로 잘 작동하는 단위들이 모여 올바르게 연동되는지를 확인하는 데 중점을 둡니다.
이 절에서는 통합 테스트의 개념과 중요성, Next.js 프로젝트에서 통합 테스트를 작성하는 방법, 특히 App Router 환경에서의 테스트 전략, 그리고 @testing-library/react와 Jest를 활용하여 실제 사용자 흐름을 모방한 테스트를 작성하는 방법에 대해 상세히 알아보겠습니다.
통합 테스트란 무엇이며 왜 중요한가요?
통합 테스트는 애플리케이션의 여러 모듈, 컴포넌트, 또는 서비스가 결합되어 함께 작동할 때 발생하는 상호작용 문제를 검증하는 테스트 방식입니다.
단위 테스트가 개별 코드 조각의 특정 동작을 확인한다면, 통합 테스트는 이 조각들이 연결되어 더 큰 기능 단위를 형성할 때의 올바른 동작을 확인합니다.
통합 테스트의 중요성- 모듈 간 상호작용 검증: 서로 다른 컴포넌트나 서비스 간의 데이터 흐름, 이벤트 전달, API 호출 등이 올바르게 이루어지는지 확인합니다.
- 엔드-투-엔드(End-to-End) 시나리오 반영: 사용자 인터페이스(UI)를 통해 입력이 주어지고, 백엔드와의 통신을 포함하여 전체 시스템이 예상대로 반응하는지 테스트할 수 있습니다.
- 시스템 전반의 안정성 확보: 단위 테스트로는 발견하기 어려운 시스템 레벨의 버그(예: 데이터 타입 불일치, API 응답 형식 문제)를 발견합니다.
- 배포 전 최종 검증: 실제 운영 환경과 유사한 조건에서 애플리케이션의 핵심 기능을 검증함으로써, 배포 후 발생할 수 있는 치명적인 문제를 예방합니다.
Next.js App Router에서의 통합 테스트 전략
Next.js App Router는 Server Components, Client Components, Data Fetching 등 다양한 개념을 도입하면서 통합 테스트 전략에도 변화가 필요합니다.
- 클라이언트 컴포넌트 중심의 통합 테스트:
@testing-library/react를 사용하여 사용자 상호작용이 발생하는 클라이언트 컴포넌트와 그 자식 컴포넌트들의 통합을 테스트합니다. 이는 사용자가 보는 화면과 직접적으로 상호작용하는 부분을 검증하는 데 효과적입니다. - 모킹 활용: API 호출, 데이터베이스 접근 등 외부 의존성은 실제 백엔드를 호출하기보다는 모킹(Mocking)하여 테스트의 독립성과 속도를 확보합니다. Mock Service Worker (MSW) 같은 라이브러리는 네트워크 요청 레벨에서 모킹을 수행하여 실제와 유사한 환경을 제공합니다.
- Server Components의 제한: 현재 Jest는
asyncServer Component를 지원하지 않습니다. 서버의 데이터 함수는 단위 테스트하고, 클라이언트 경계는 React Testing Library로 검증하며, 둘이 합쳐진 실제 페이지는 E2E 테스트로 확인합니다. - 데이터 페칭 통합 테스트:
fetch요청을 모킹하여 컴포넌트가 데이터를 가져와 렌더링하는 과정을 테스트합니다.
통합 테스트 작성 예시
간단한 상품 목록 및 검색 기능을 가진 페이지를 통합 테스트하는 시나리오를 구성해 보겠습니다.
시나리오상품 목록 페이지에 접속한다.
상품 목록이 로드되어 화면에 표시되는 것을 확인한다.
검색창에 검색어를 입력한다.
검색 버튼을 클릭한다.
검색어에 해당하는 상품만 필터링되어 표시되는 것을 확인한다.
필요한 컴포넌트 및 API 모듈
먼저 테스트할 컴포넌트와 모킹할 API 함수를 정의합니다.
export interface Product {
id: string;
name: string;
price: number;
}"use client";
import React, { useState } from 'react';
import type { Product } from '@/types/product';
interface ProductListProps {
initialProducts: Product[];
fetchProductsAction: (query?: string) => Promise<Product[]>;
}
export default function ProductList({ initialProducts, fetchProductsAction }: ProductListProps) {
const [products, setProducts] = useState<Product[]>(initialProducts);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const handleSearch = async () => {
setLoading(true);
try {
const fetchedProducts = await fetchProductsAction(searchQuery);
setProducts(fetchedProducts);
} catch (error) {
console.error("Failed to fetch products:", error);
setProducts([]); // 에러 시 빈 배열 또는 에러 메시지
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h2 style={{ marginBottom: '20px' }}>상품 목록</h2>
<div style={{ marginBottom: '20px', display: 'flex', gap: '10px' }}>
<input
type="text"
placeholder="상품 검색..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{ flexGrow: 1, padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
<button
onClick={handleSearch}
disabled={loading}
style={{ padding: '8px 15px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
>
{loading ? '검색 중...' : '검색'}
</button>
</div>
{products.length === 0 && !loading && (
<p style={{ textAlign: 'center', color: '#888' }}>상품이 없습니다.</p>
)}
<ul>
{products.map(product => (
<li key={product.id} style={{ marginBottom: '10px', padding: '10px', border: '1px solid #eee', borderRadius: '4px' }}>
{product.name} - {product.price.toLocaleString()}원
</li>
))}
</ul>
</div>
);
}"use server";
import type { Product } from '@/types/product';
// 이 함수는 테스트에서 모킹될 것입니다.
export async function getProductsApi(query?: string): Promise<Product[]> {
// 실제 서버의 데이터베이스나 외부 API 호출 로직
console.log(`API 호출: 상품 조회, 쿼리: ${query || '없음'}`);
return new Promise(resolve => {
setTimeout(() => {
const allProducts = [
{ id: 'p1', name: '노트북', price: 1500000 },
{ id: 'p2', name: '마우스', price: 30000 },
{ id: 'p3', name: '키보드', price: 80000 },
{ id: 'p4', name: '모니터', price: 300000 },
];
if (query) {
resolve(allProducts.filter(p => p.name.includes(query) || p.id.includes(query)));
} else {
resolve(allProducts);
}
}, 500); // 네트워크 지연 시뮬레이션
});
}// 이 페이지는 ProductList 컴포넌트를 렌더링하고 초기 데이터를 전달합니다.
// Server Component에서 API를 호출하고 Client Component로 props를 전달하는 일반적인 패턴입니다.
import { getProductsApi } from '@/lib/api'; // 서버에서 사용할 API
import ProductList from '@/components/ProductList'; // 클라이언트 컴포넌트
export default async function ProductsPage() {
const initialProducts = await getProductsApi(); // 서버에서 초기 데이터 페칭
return (
<div style={{ maxWidth: '900px', margin: '40px auto', padding: '20px', border: '1px solid #28a745', borderRadius: '10px', boxShadow: '0 4px 12px rgba(0,0,0,0.08)' }}>
<h1 style={{ color: '#28a745', textAlign: 'center', marginBottom: '30px' }}>모든 상품</h1>
{/* 명시적으로 선언한 Server Function만 Client Component에 전달할 수 있습니다. */}
<ProductList initialProducts={initialProducts} fetchProductsAction={getProductsApi} />
</div>
);
}일반 함수는 Server Component에서 Client Component의 props로 전달할 수 없습니다.
이 예제의 getProductsApi는 파일 상단의 "use server"로 명시한 Server Function이므로 클라이언트에서 호출 가능한 참조로 전달됩니다.
클라이언트 경계 테스트 작성
현재 Jest는 async Server Component를 직접 지원하지 않습니다.
여기서는 Jest와 React Testing Library로 ProductList를 테스트합니다. ProductsPage 전체 흐름에는 별도의 E2E 시나리오가 필요합니다. 다음 절은 로그인 화면 예제로 E2E 작성법을 설명하며 상품 페이지 E2E를 구현하지는 않습니다.
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ProductList from '../src/components/ProductList';
const initialProducts = [
{ id: 'p1', name: '노트북', price: 1500000 },
{ id: 'p2', name: '마우스', price: 30000 },
];
describe('ProductList', () => {
test('초기 상품 목록이 로드되고 렌더링되어야 합니다.', async () => {
const fetchProductsAction = jest.fn();
render(<ProductList initialProducts={initialProducts} fetchProductsAction={fetchProductsAction} />);
// "상품 목록" 제목이 있는지 확인
expect(screen.getByRole('heading', { name: '상품 목록' })).toBeInTheDocument();
// 초기 상품들이 로드될 때까지 기다립니다.
await waitFor(() => {
expect(screen.getByText('노트북 - 1,500,000원')).toBeInTheDocument();
expect(screen.getByText('마우스 - 30,000원')).toBeInTheDocument();
});
expect(fetchProductsAction).not.toHaveBeenCalled();
});
test('검색 기능이 올바르게 작동해야 합니다.', async () => {
const fetchProductsAction = jest.fn().mockResolvedValueOnce([
{ id: 'p3', name: '키보드', price: 80000 },
]);
render(<ProductList initialProducts={initialProducts} fetchProductsAction={fetchProductsAction} />);
// 검색 입력 필드 찾기 (placeholder 텍스트로)
const searchInput = screen.getByPlaceholderText('상품 검색...');
fireEvent.change(searchInput, { target: { value: '키보드' } }); // 검색어 입력
// 검색 버튼 찾기 (텍스트로)
const searchButton = screen.getByRole('button', { name: '검색' });
fireEvent.click(searchButton); // 검색 버튼 클릭
// "검색 중..." 텍스트가 나타나는지 확인
expect(screen.getByRole('button', { name: '검색 중...' })).toBeDisabled();
// 검색 결과가 로드될 때까지 기다립니다.
await waitFor(() => {
expect(screen.getByText('키보드 - 80,000원')).toBeInTheDocument();
});
// 이전에 있던 상품들이 사라졌는지 확인 (필터링 검증)
expect(screen.queryByText('노트북 - 1,500,000원')).not.toBeInTheDocument();
expect(screen.queryByText('마우스 - 30,000원')).not.toBeInTheDocument();
expect(fetchProductsAction).toHaveBeenCalledTimes(1);
expect(fetchProductsAction).toHaveBeenCalledWith('키보드');
});
test('검색 결과가 없을 때 "상품이 없습니다." 메시지를 표시해야 합니다.', async () => {
const fetchProductsAction = jest.fn().mockResolvedValueOnce([]);
render(<ProductList initialProducts={initialProducts} fetchProductsAction={fetchProductsAction} />);
const searchInput = screen.getByPlaceholderText('상품 검색...');
fireEvent.change(searchInput, { target: { value: '존재하지않는상품' } });
const searchButton = screen.getByRole('button', { name: '검색' });
fireEvent.click(searchButton);
await waitFor(() => {
expect(screen.getByText('상품이 없습니다.')).toBeInTheDocument();
});
expect(screen.queryByText('노트북 - 1,500,000원')).not.toBeInTheDocument();
});
});render(<ProductList ... />): Jest가 지원하는 클라이언트 경계만 직접 렌더링하고, 서버에서 내려올 초기 데이터와 검색 함수를 props 대역으로 주입합니다.
jest.fn(): Server Function의 네트워크 실행을 흉내 내는 비동기 Mock을 테스트마다 새로 만들어 호출 인자와 UI 결과를 함께 검증합니다.
mockResolvedValue, mockResolvedValueOnce: Mock 함수가 비동기적으로 특정 값을 반환하도록 설정합니다.
mockResolvedValueOnce는 한 번만 적용됩니다.
screen.getByRole, screen.getByPlaceholderText, screen.getByText: @testing-library/react의 쿼리를 사용하여 사용자가 실제로 보는 방식으로 DOM 요소를 찾습니다.
fireEvent.change, fireEvent.click: 사용자 상호작용(입력, 클릭 등)을 시뮬레이션합니다.
await waitFor(() => { ... }): 비동기 작업(데이터 로딩, UI 업데이트)이 완료될 때까지 기다립니다.
expect 문이 성공할 때까지 콜백 함수를 주기적으로 재실행합니다.
이 예제에서는 대역 Promise 뒤의 UI 갱신을 기다립니다. 단일 요소의 등장은 findBy 쿼리로 기다릴 수도 있습니다.
toBeInTheDocument(), toBeDisabled(): @testing-library/jest-dom에서 제공하는 매처로, DOM 요소의 특정 상태를 검증합니다.
상품 검색 예제의 테스트 경계
ProductList의 초기 목록, 검색 입력, action 대역을 기다리는 pending 상태, 응답 목록 또는 빈 목록으로 교체되는 실제 UI 상태를 추적할 수 있습니다. 서버 함수·DB·async ProductsPage까지 실행한 테스트로 확장하지 않습니다.
NEXT · 14.2
상품 검색 테스트는 ProductList의 클라이언트 경계를 확인합니다
초기 상품 배열과 fetchProductsAction 대역을 props로 전달하고 입력·클릭·로딩·목록 변경을 확인합니다. async ProductsPage와 Server Function 전송은 연결하지 않습니다.
테스트가 실행하는 Client UI와 action 대역
초기 props는 initialProducts이며 검색 action은 fetchProductsAction 대역입니다. 초기 렌더는 action을 호출하지 않습니다. 대역 reject도 catch에서 빈 배열이 되어 같은 빈 결과 문구를 보여 주며 별도 오류 UI는 없습니다. pending UI·호출 인자·횟수·응답 목록을 Client 경계에서 검사하며 서버 페이지·전송·DB를 실행한 시험으로 확대하지 않습니다.
원래 설명과 상세 기준
| 경계 | 예제 코드에서 확인할 범위 |
|---|---|
| 초기 목록 | 테스트가 initialProducts prop에 배열을 전달합니다. 서버 페이지를 실행하지 않습니다. |
| 검색 요청 | 입력과 클릭은 실제 ProductList에 전달하고, fetchProductsAction prop은 jest.fn() 대역으로 바꿉니다. |
| 검색 결과 | 대역 반환값으로 목록·빈 결과·로딩 UI와 호출 인자를 확인합니다. |
| 전체 페이지 | async ProductsPage 렌더링과 Server Function 통신은 이 Jest 테스트 범위 밖입니다. |
- 초기 목록
- 예제 코드에서 확인할 범위: 테스트가 initialProducts prop에 배열을 전달합니다. 서버 페이지를 실행하지 않습니다.
- 검색 요청
- 예제 코드에서 확인할 범위: 입력과 클릭은 실제 ProductList에 전달하고, fetchProductsAction prop은 jest.fn() 대역으로 바꿉니다.
- 검색 결과
- 예제 코드에서 확인할 범위: 대역 반환값으로 목록·빈 결과·로딩 UI와 호출 인자를 확인합니다.
- 전체 페이지
- 예제 코드에서 확인할 범위: async ProductsPage 렌더링과 Server Function 통신은 이 Jest 테스트 범위 밖입니다.
초기 렌더는 검색 함수를 호출하지 않고, 검색 후에는 대역의 반환값으로 목록을 교체합니다. 빈 결과 메시지와 호출 횟수·키보드 인자를 확인합니다. 예제의 별도 서버 함수는 고정 목록과 모의 지연을 사용하며 이 Jest 테스트가 실행하는 대상은 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 초기 목록 렌더 initialProducts 배열로 실제 ProductList를 렌더합니다. | 목록: initialProducts · action 초기 호출: 없음 · 서버 page: 미실행 | Client UI의 출발 목록이 표시됩니다. |
| 입력과 검색 click 사용자 입력·click이 실제 컴포넌트에 전달되고 jest.fn action 대역을 호출합니다. | 입력: 검색어 · 호출: action 대역 · 경계: Client 테스트 | 지역 입력이 검색 요청으로 넘어갑니다. |
| pending 표시 대역 반환을 기다리는 동안 loading UI를 확인합니다. | UI: loading · 결과: 대기 · Server Function 전송: 없음 | 표시가 요청 대기 상태로 바뀝니다. |
| 응답 목록 적용 대역 반환값으로 목록을 교체하거나 빈 결과 메시지를 표시합니다. | 목록: 대역 결과 · 빈 결과: 메시지 · 검증: 호출 인자·횟수 | Client 출력이 검색 결과로 교체됩니다. |
상품 검색 예제의 테스트 경계
ProductList의 초기 목록, 검색 입력, action 대역을 기다리는 pending 상태, 응답 목록 또는 빈 목록으로 교체되는 실제 UI 상태를 추적할 수 있습니다. 서버 함수·DB·async ProductsPage까지 실행한 테스트로 확장하지 않습니다.
NEXT · 14.2
상품 검색 테스트는 ProductList의 클라이언트 경계를 확인합니다
초기 상품 배열과 fetchProductsAction 대역을 props로 전달하고 입력·클릭·로딩·목록 변경을 확인합니다. async ProductsPage와 Server Function 전송은 연결하지 않습니다.
테스트가 실행하는 Client UI와 action 대역
초기 props는 initialProducts이며 검색 action은 fetchProductsAction 대역입니다. 초기 렌더는 action을 호출하지 않습니다. 대역 reject도 catch에서 빈 배열이 되어 같은 빈 결과 문구를 보여 주며 별도 오류 UI는 없습니다. pending UI·호출 인자·횟수·응답 목록을 Client 경계에서 검사하며 서버 페이지·전송·DB를 실행한 시험으로 확대하지 않습니다.
원래 설명과 상세 기준
| 경계 | 예제 코드에서 확인할 범위 |
|---|---|
| 초기 목록 | 테스트가 initialProducts prop에 배열을 전달합니다. 서버 페이지를 실행하지 않습니다. |
| 검색 요청 | 입력과 클릭은 실제 ProductList에 전달하고, fetchProductsAction prop은 jest.fn() 대역으로 바꿉니다. |
| 검색 결과 | 대역 반환값으로 목록·빈 결과·로딩 UI와 호출 인자를 확인합니다. |
| 전체 페이지 | async ProductsPage 렌더링과 Server Function 통신은 이 Jest 테스트 범위 밖입니다. |
- 초기 목록
- 예제 코드에서 확인할 범위: 테스트가 initialProducts prop에 배열을 전달합니다. 서버 페이지를 실행하지 않습니다.
- 검색 요청
- 예제 코드에서 확인할 범위: 입력과 클릭은 실제 ProductList에 전달하고, fetchProductsAction prop은 jest.fn() 대역으로 바꿉니다.
- 검색 결과
- 예제 코드에서 확인할 범위: 대역 반환값으로 목록·빈 결과·로딩 UI와 호출 인자를 확인합니다.
- 전체 페이지
- 예제 코드에서 확인할 범위: async ProductsPage 렌더링과 Server Function 통신은 이 Jest 테스트 범위 밖입니다.
초기 렌더는 검색 함수를 호출하지 않고, 검색 후에는 대역의 반환값으로 목록을 교체합니다. 빈 결과 메시지와 호출 횟수·키보드 인자를 확인합니다. 예제의 별도 서버 함수는 고정 목록과 모의 지연을 사용하며 이 Jest 테스트가 실행하는 대상은 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 초기 목록 렌더 initialProducts 배열로 실제 ProductList를 렌더합니다. | 목록: initialProducts · action 초기 호출: 없음 · 서버 page: 미실행 | Client UI의 출발 목록이 표시됩니다. |
| 입력과 검색 click 사용자 입력·click이 실제 컴포넌트에 전달되고 jest.fn action 대역을 호출합니다. | 입력: 검색어 · 호출: action 대역 · 경계: Client 테스트 | 지역 입력이 검색 요청으로 넘어갑니다. |
| pending 표시 대역 반환을 기다리는 동안 loading UI를 확인합니다. | UI: loading · 결과: 대기 · Server Function 전송: 없음 | 표시가 요청 대기 상태로 바뀝니다. |
| 응답 목록 적용 대역 반환값으로 목록을 교체하거나 빈 결과 메시지를 표시합니다. | 목록: 대역 결과 · 빈 결과: 메시지 · 검증: 호출 인자·횟수 | Client 출력이 검색 결과로 교체됩니다. |
대역의 실패도 추적해 보기 초기 목록은 노트북과 마우스 두 개입니다. 검색 함수 대역을 완료되지 않은 Promise로 두고 검색을 누른 직후에는 목록과 버튼이 어떻게 보일까요? 그 Promise를 거부시키면 마지막 UI는 무엇이며, 성공한 빈 검색과 화면만으로 구별할 수 있을까요?
try·catch·finally의 상태 변화
검색 시작은 loading만 true로 바꾸므로 기존 목록은 남고 버튼은 검색 중이며 비활성입니다. 거부되면 catch가 목록을 빈 배열로 바꾸고, finally가 로딩을 끝내 상품이 없습니다 문구가 나타납니다. 원 UI에는 별도 오류 상태가 없어 성공한 빈 결과와 같은 최종 문구입니다.
원 세 테스트에는 이 거부 경로가 없습니다. 추가할 때는 호출 인자·대기 중 버튼·완료 뒤 빈 목록을 함께 확인하세요. 사용자가 실패와 빈 결과를 구별해야 한다면 오류 상태와 안내 문구를 먼저 설계하고 그 기대값을 테스트합니다. 버튼의 로딩 표시가 원격 DB 검증을 뜻하는 것은 아닙니다.
통합 테스트 작성 시 고려사항 및 팁
- 진정한 통합에 초점: 단위 테스트에서 이미 검증된 개별 로직을 다시 테스트하기보다는, 여러 모듈이 상호작용하는 지점(예: 컴포넌트 간의
props전달, API 호출 후 데이터 처리, 사용자 입력에 따른 UI 변화)에 초점을 맞춥니다. - 실제 사용자 시나리오 모방: 사용자가 애플리케이션을 사용하는 방식과 유사하게 테스트를 작성합니다. 이 버튼을 클릭하면 무엇이 일어나야 하는가?와 같은 질문에 답하는 방식으로 테스트를 설계합니다.
- Mock Service Worker (MSW) 활용: 백엔드 API를 모킹해야 할 경우, MSW는 네트워크 레벨에서 요청을 가로채서 Mock 응답을 반환하므로, 실제 백엔드가 없는 환경에서도 통합 테스트를 작성하고 개발할 수 있게 해 줍니다. 함수 prop 대역과는 다른 경계이므로 실제 네트워크 요청 경로를 테스트할 때 선택합니다.
- 테스트 환경 일관성: 개발, 테스트, 운영 환경 간에 설정(환경 변수, API 엔드포인트)의 일관성을 유지하여 테스트 통과 후 실제 배포 시 문제가 발생하지 않도록 합니다.
- 테스트 피라미드: 일반적으로 단위 테스트의 개수가 가장 많고, 통합 테스트, 그리고 엔드-투-엔드 테스트(Cypress, Playwright 등) 순으로 개수가 줄어드는 '테스트 피라미드' 전략을 따르는 것이 좋습니다. 단위 테스트는 빠르고 격리되어 있으며, 통합 테스트는 여러 부분을 연결하고, E2E 테스트는 전체 시스템을 검증합니다.
- 리팩터링 시의 안정성: 통합 테스트는 코드 리팩터링 시에도 높은 수준의 안정성을 제공합니다. 내부 구현이 변경되어도 사용자 흐름이 동일하다면 테스트가 여전히 통과해야 합니다.
초기 prop·검색 함수 대역을 사용한 이 테스트만으로 async Server Component, HTTP 전송, DB 연동이 검증되었다고 판단하지 않습니다. 예제 API 모듈도 고정 배열과 지연을 사용하므로 외부 서버 호출과 구분합니다. 범위를 넓힐 때는 실제로 연결한 의존성을 명시합니다.