
React 애플리케이션에서 빠르게 나타났다 사라지는 로딩 모달을 테스트할 때 발생하는 타이밍 이슈와 이를 Promise.all을 통해 해결하는 방법을 알아보겠습니다.
먼저 테스트할 검색 컴포넌트의 예시 코드입니다:
// SearchComponent.tsx
import React, { useState } from 'react';
import LoadingModal from './LoadingModal';
interface SearchResult {
id: number;
name: string;
}
export const SearchComponent: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [results, setResults] = useState<SearchResult[]>([]);
const handleSearch = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/search');
const data = await response.json();
setResults(data);
} finally {
setIsLoading(false);
}
};
return (
<div>
<button onClick={handleSearch}>검색</button>
{isLoading && <LoadingModal data-testid="loading-modal" />}
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
// LoadingModal.tsx
import React from 'react';
interface LoadingModalProps {
'data-testid'?: string;
}
const LoadingModal: React.FC<LoadingModalProps> = (props) => {
return (
<div
className="modal-overlay"
{...props}
>
<div className="modal-content">
<div className="spinner" />
<p>검색 중...</p>
</div>
</div>
);
};
export default LoadingModal;
순차적 실행 방식은 다음과 같은 문제점이 있습니다:
// 불안정한 테스트 코드
test('순차 실행 방식의 모달 테스트', async ({ page }) => {
await page.getByRole('button', { name: '검색' }).click();
await expect(page.getByTestId('loading-modal')).toBeVisible();
});
이 방식의 문제점:
Promise.all을 사용하면 이벤트와 상태 변화를 동시에 감지할 수 있습니다:
test('Promise.all을 활용한 안정적인 모달 테스트', async ({ mount, page }) => {
// 컴포넌트 마운트
await mount(<SearchComponent />);
// API 응답 모킹
await page.route('/api/search', async route => {
await new Promise(resolve => setTimeout(resolve, 1000));
await route.fulfill({
status: 200,
body: JSON.stringify([
{ id: 1, name: '결과 1' },
{ id: 2, name: '결과 2' }
])
});
});
// API 응답 감시 설정
const responsePromise = page.waitForResponse(res =>
res.url().includes('/api/search')
);
// 클릭과 모달 표시를 동시에 감지
await Promise.all([
page.getByTestId('loading-modal').waitFor({ state: 'visible' }),
page.getByRole('button', { name: '검색' }).click()
]);
// API 응답 대기
await responsePromise;
// 모달 숨김 및 결과 표시 확인
await expect(page.getByTestId('loading-modal')).toBeHidden();
await expect(page.getByText('결과 1')).toBeVisible();
await expect(page.getByText('결과 2')).toBeVisible();
});
동시성 처리
레이스 컨디션 방지
await Promise.all([
page.getByTestId('loading-modal').waitFor({
state: 'visible',
timeout: 5000
}),
page.getByRole('button', { name: '검색' }).click()
]);
await Promise.all([
page.getByTestId('loading-modal').waitFor({ state: 'visible' }),
page.getByTestId('results').waitFor({ state: 'hidden' }),
page.getByRole('button', { name: '검색' }).click()
]);
Playwright 컴포넌트 테스트를 위한 설정 파일입니다:
// playwright-ct.config.ts
import { defineConfig, devices } from '@playwright/experimental-ct-react';
export default defineConfig({
testDir: './',
timeout: 10000,
use: {
ctPort: 3100,
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
}
],
});
Promise.all을 활용한 테스트는 특히 모달이나 토스트 메시지처럼 빠르게 상태가 변하는 UI 요소를 테스트할 때 매우 효과적입니다. 이를 통해 테스트의 안정성과 신뢰성을 크게 향상시킬 수 있으며, 특히 CI/CD 환경에서 더욱 안정적인 테스트 실행이 가능해집니다.
이러한 방식으로 테스트를 작성하면 깜빡이는 모달과 같은 까다로운 UI 요소도 안정적으로 테스트할 수 있습니다. 특히 Promise.all을 활용한 동시성 처리가 테스트의 신뢰성을 높이는 핵심 요소가 됩니다.