Playwright Component Testing 완벽 가이드 — 컴포넌트 단위 테스트

Playwright Component Testing 완벽 가이드 — 컴포넌트 단위 테스트

이 글의 핵심

Playwright Component Testing은 E2E와 단위 테스트 사이에서 컴포넌트를 실제 브라우저 엔진으로 마운트해 검증하는 방식입니다. 이 글에서는 프레임워크별 설정, 상호작용·스냅샷·MSW·CI까지 한 번에 정리합니다.

이 글의 핵심

Playwright Component Testing(CT)은 단일 컴포넌트를 실제 브라우저에서 마운트하고, Playwright의 Locator·Assertion으로 상태와 스타일까지 검증하는 테스트 방식입니다. 전체 앱을 띄우는 E2E보다 범위가 좁고, DOM만 흉내 내는 jsdom 기반 테스트보다 브라우저 동작에 가깝습니다.

이 글에서는 핵심 개념, React·Vue·Svelte별 설정, 마운트와 사용자 상호작용, 스냅샷·비주얼 회귀, MSW(Mock Service Worker), CI/CD, 그리고 Cypress CT와의 비교까지 실무 관점에서 정리합니다.


1. 왜 컴포넌트 테스트인가

프론트엔드 테스트는 보통 다음 층으로 나뉩니다.

구분대표 도구강점한계
단위·통합(가상 DOM)Vitest, Jest + Testing Library빠르고 피드백이 즉각적레이아웃·실제 CSS·브라우저 차이 반영이 어려움
컴포넌트(실제 브라우저)Playwright CT, Cypress CT픽셀·스크롤·포커스 등 브라우저 특성 반영E2E보다는 가볍지만 순수 단위보다 느림
E2EPlaywright, Cypress사용자 여정 전체 검증느리고 플레이키(flaky)에 민감

Playwright CT는 “한 화면의 한 덩어리”에 집중합니다. 예를 들어 폼 한 블록, 모달, 데이터 테이블 행 단위처럼 재사용 컴포넌트의 회귀를 막기에 적합합니다. 반면 라우팅·결제·권한 플로우 전체는 E2E가 여전히 필요합니다.

공식 문서에서도 컴포넌트 테스트는 실험적(experimental) 기능으로 명시되어 있으며, 시맨틱 버전을 엄격히 따르지 않을 수 있으므로 버전 고정·릴리즈 노트 확인이 중요합니다.


2. 핵심 개념

2.1 마운트(Mount)와 Locator

Playwright CT의 중심은 mount 픽스처입니다. 테스트 코드에서 JSX/Vue/Svelte 컴포넌트를 넘기면, 내부적으로 테스트용 HTML 셸(playwright/index.html의 루트)에 붙습니다. 반환값은 그 컴포넌트 루트를 가리키는 Locator에 가깝게 동작하여, click, fill, screenshot 등 기존 Playwright API를 그대로 씁니다.

즉, “컴포넌트 = 축소된 페이지”로 두고 E2E와 동일한 사고로 assertion을 구성할 수 있습니다.

2.2 진입 스크립트 playwright/index.*

번들러(Vite 등)로 컴포넌트 테스트 앱을 띄울 때, 전역 스타일·테마·i18n·MSW 같은 “앱 전체에서 한 번만 필요한 것”은 playwright/index.tsx(또는 index.ts)에서 주입합니다. 프로덕션 main.tsx와 최대한 동일하게 맞추면 환경 편차로 인한 헛실패를 줄일 수 있습니다.

2.3 제약 사항

문서에 따르면, mount에 복잡한 Node 객체·살아 있는 콜백을 그대로 넘기는 방식은 제한됩니다. 테스트에서는 직렬화 가능한 props간단한 이벤트 핸들러 위주로 설계하는 것이 안전합니다. 내부 상태가 복잡하면 래퍼 컴포넌트로 테스트 전용 진입점을 두는 패턴이 흔합니다.


3. 시작하기: 공통 흐름

프로젝트에 처음 붙일 때는 공식 스캐폴딩을 권장합니다.

npm init playwright@latest -- --ct

생성되는 핵심 파일은 대략 다음과 같습니다.

  • playwright/index.html#root(또는 문서가 요구하는 id)가 있는 엔트리 HTML
  • playwright/index.tsx — 전역 Provider, 스타일, MSW 등
  • playwright-ct.config.ts — CT 전용 Playwright 설정

실행 스크립트는 보통 package.jsontest-ct 형태로 추가됩니다.

npm run test-ct

이후 프레임워크에 맞는 패키지를 선택해 테스트 파일에서 import합니다.


4. React 설정

4.1 패키지

npm install -D @playwright/experimental-ct-react @playwright/test

4.2 테스트 예시

// Button.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';

test('클릭 시 라벨이 바뀐다', async ({ mount }) => {
  const component = await mount(<Button label="전송" />);
  await component.getByRole('button', { name: '전송' }).click();
  await expect(component.getByRole('button')).toHaveText('완료');
});

설명: mount로 트리를 올린 뒤, 반환된 Locator 기준으로 역할·텍스트 위주의 쿼리를 쓰면 접근성과 안정성이 좋아집니다. E2E에서 권장하는 패턴과 동일합니다.

4.3 Vite 연동

Playwright는 컴포넌트 번들에 Vite를 사용합니다. 기존 앱의 vite.config를 그대로 재사용하지는 않을 수 있으므로, 공식 문서대로 playwright-ct.config.tsuse.ctViteConfig에 경로 별칭(@/ 등)·플러그인을 복사해 맞춥니다. 플러그인을 직접 나열하기 시작하면 프레임워크 플러그인(예: @vitejs/plugin-react)도 함께 선언해야 하는 점에 유의합니다.


5. Vue 설정

5.1 패키지

npm install -D @playwright/experimental-ct-vue @playwright/test

5.2 테스트 예시

// Counter.spec.ts
import { test, expect } from '@playwright/experimental-ct-vue';
import Counter from './Counter.vue';

test('증가 버튼', async ({ mount }) => {
  const component = await mount(Counter, {
    props: { initial: 0 },
  });
  await component.getByRole('button', { name: '증가' }).click();
  await expect(component.locator('[data-testid="count"]')).toHaveText('1');
});

설명: Vue는 mount의 두 번째 인자로 props·slots 등을 넘기는 패턴이 문서화되어 있습니다. SFC 단위로 끊어 테스트할 때는 props를 최소화하고, 스토어·라우터는 Provider 또는 테스트 더블로 치환하는 편이 디버깅에 유리합니다.


6. Svelte 설정

6.1 패키지

npm install -D @playwright/experimental-ct-svelte @playwright/test

6.2 테스트 예시

// Alert.spec.ts
import { test, expect } from '@playwright/experimental-ct-svelte';
import Alert from './Alert.svelte';

test('닫기 클릭 시 사라진다', async ({ mount }) => {
  const component = await mount(Alert, {
    props: { message: '저장됨' },
  });
  await expect(component.getByText('저장됨')).toBeVisible();
  await component.getByRole('button', { name: '닫기' }).click();
  await expect(component.getByText('저장됨')).toBeHidden();
});

설명: Svelte도 Vue와 유사하게 컴포넌트와 props를 넘깁니다. Svelte 5 룬 등 최신 문법을 쓸 경우 번들러·플러그인 버전을 프로덕션과 맞추는 것이 중요합니다.


7. 마운트와 상호작용

7.1 사용자 이벤트

click, dblclick, fill, press, hover, focus 등은 E2E와 동일합니다. 자동 대기 덕분에 타이밍 이슈를 수동 waitForTimeout으로 풀기보다, 역할·레이블 기반 Locator와 assertion을 조합하는 편이 유지보수에 낫습니다.

7.2 beforeMount로 주입

테스트마다 다른 Context·Provider·전역 스토어 초기값이 필요하면, 프레임워크별 beforeMount으로 래핑하는 방식을 사용합니다. 한 컴포넌트가 여러 테마를 지원한다면, 테스트 케이스별로 테마 Provider만 갈아끼우는 패턴이 깔끔합니다.

7.3 비동기 데이터

데이터 패칭 컴포넌트는 MSW로 응답을 고정하거나, props로 이미 받은 데이터만 렌더링하는 “프레젠테이션 컴포넌트”로 쪼개 CT 대상을 좁히는 전략이 효과적입니다. 그렇지 않으면 네트워크·캐시 타이밍 때문에 flaky가 생기기 쉽습니다.


8. 스냅샷과 비주얼 회귀 테스트

8.1 스크린샷 assertion

Playwright의 toHaveScreenshot()픽셀 단위 비교에 쓰입니다. 컴포넌트 단위에서는 해당 Locator 범위만 캡처해 노이즈를 줄입니다.

test('카드 비주얼 회귀', async ({ mount }) => {
  const component = await mount(<PricingCard plan="pro" />);
  await expect(component).toHaveScreenshot('pricing-pro.png');
});

설명: 최초 실행 시 기준 이미지가 생성되고, 이후 변경 시 diff가 납니다. OS·폰트·서브픽셀 차이가 있으므로 CI에서는 동일한 Docker 이미지·폰트 설치를 맞추거나, 허용 오차 옵션을 조정해야 합니다.

8.2 DOM 스냅샷

텍스트 구조만 보고 싶다면 스크린샷 대신 스냅샷 matcher(프로젝트에서 허용하는 경우)나, 접근성 트리 기반 assertion을 조합하기도 합니다. 다만 UI 라이브러리 업그레이드 시 스냅샷이 대량으로 깨질 수 있어 팀 규칙이 필요합니다.

8.3 운영 팁

  • 작은 컴포넌트에 남용하면 이미지 자산이 폭증합니다. 디자인 토큰이 바뀌면 광범위하게 영향을 받는 컴포넌트 위주로 쓰는 것이 좋습니다.
  • 스토리북(Storybook)과 병행할 때는 동일 컴포넌트·동일 상태를 기준으로 어디까지 CT로 할지 역할을 나눕니다.

9. MSW와 목 데이터

9.1 왜 MSW인가

실제 API를 두면 속도·안정성·데이터 민감도 문제가 생깁니다. MSW는 브라우저에서 요청을 가로채 고정된 JSON을 돌려주므로, 컴포넌트 테스트와 잘 맞습니다.

9.2 권장: router 픽스처와 MSW 핸들러

Playwright 문서에서는 컴포넌트 테스트에 router 픽스처를 두고, router.use(...)에 MSW http 핸들러를 넘기는 방식을 안내합니다. 프로덕션에서 쓰는 handlers 배열을 그대로 재사용하면 앱과 테스트의 목 계약이 일치하기 쉽습니다.

import { http, HttpResponse } from 'msw';
import { handlers } from '../src/mocks/handlers';

test.beforeEach(async ({ router }) => {
  await router.use(...handlers);
});

test('목 데이터로 렌더', async ({ mount }) => {
  const component = await mount(<UserCard userId="1" />);
  await expect(component.getByText('테스트')).toBeVisible();
});

test('한 번만 다른 응답', async ({ mount, router }) => {
  await router.use(
    http.get('/api/user', () => HttpResponse.json({ name: '에러 케이스' })),
  );
  const component = await mount(<UserCard userId="1" />);
  await expect(component.getByText('에러 케이스')).toBeVisible();
});

설명: test.beforeEach에서 공통 핸들러를 깔고, 개별 테스트에서만 router.use로 덮어쓰면 시나리오 분기가 명확합니다. MSW를 msw/browsersetupWorkerplaywright/index.tsx에서 시작하는 팀도 있으나, 공식 예시는 router 기반이 많으므로 도입 시 한 가지 방식으로 통일하는 것이 좋습니다.

9.3 beforeMount와 Provider·라우터

라우터·테마·Pinia 등 마운트 전 래핑playwright/index.tsx에서 @playwright/experimental-ct-react/hooksbeforeMount로 처리합니다. hooksConfig를 타입으로 두고 mount의 두 번째 인자로 넘기면, 테스트마다 스토어 초기값 등을 바꿀 수 있습니다.

// playwright/index.tsx (개념 예시)
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import { BrowserRouter } from 'react-router-dom';

export type HooksConfig = { enableRouting?: boolean };

beforeMount<HooksConfig>(async ({ App, hooksConfig }) => {
  if (hooksConfig?.enableRouting) {
    return (
      <BrowserRouter>
        <App />
      </BrowserRouter>
    );
  }
});

9.4 Playwright page.route와의 선택

page.route()·router.route()로 응답을 직접 조작할 수도 있습니다. MSW 핸들러는 앱·스토리북과 공유하기 좋고, 인라인 route는 빠른 실험에 유리합니다. 팀 표준을 하나로 정하면 혼선이 줄어듭니다.


10. CI/CD 통합

10.1 기본 명령

CI에서는 보통 헤드리스 모드로 실행합니다.

npx playwright test --reporter=html

컴포넌트 테스트 전용 스크립트가 있다면 그것을 호출합니다. 리포트·트레이스 아티팩트를 저장하면 실패 시 원인 파악이 빨라집니다.

10.2 브라우저 설치

CI 이미지에 Playwright 브라우저 바이너리가 없으면 설치 단계가 필요합니다.

npx playwright install --with-deps chromium

리눅스에서는 시스템 의존성 누락으로 실패하는 경우가 많아 --with-deps가 문서에서 자주 권장됩니다.

10.3 병렬·샤딩

Playwright는 워커 병렬 실행이 강점입니다. 대규모 스위트는 --shard로 잡을 나눠 여러 CI 워커에서 돌리는 방식을 쓸 수 있습니다. 컴포넌트 테스트가 수천 개로 불어나면 스토리 단위 분할과 함께 검토합니다.

10.4 스냅샷 승인 워크플로

디자인 변경이 잦다면 스냅샷 업데이트 PR을 별도로 두고, 시각적 diff 리뷰를 디자이너와 공유하는 프로세스가 안전합니다.


11. Cypress vs Playwright Component Testing

항목Cypress CTPlaywright CT
API 일관성Cypress 전용 체인E2E와 동일한 Playwright API
멀티 브라우저제한적(주로 Chromium 중심 구성)Chromium/WebKit/Firefox 등 선택 용이
생태계·레퍼런스오래됨, 예제 많음상대적으로 신규, 실험적 표기
디버깅타임 트래블 등 독자 UXTrace Viewer·Codegen 등 Playwright 도구
학습 비용Cypress만 익히면 됨E2E를 이미 Playwright로 쓰면 이점 큼

정리: 이미 Playwright로 E2E를 운영 중이면 도구·설정·리포트 통합 측면에서 CT까지 Playwright로 가져가기 쉽습니다. 반면 Cypress에 깊이 최적화된 팀이라면 Cypress CT가 학습 곡선이 완만할 수 있습니다. 결정은 기술 스택 단일화 vs 기존 자산 트레이드오프로 보는 것이 현실적입니다.


12. 트러블슈팅 요약

  • import 경로/별칭 오류: playwright-ct.config.ts의 Vite 설정과 tsconfig paths를 동기화합니다.
  • 스타일이 비어 있음: 글로벌 CSS·테마 Provider가 playwright/index.tsx에 빠졌는지 확인합니다.
  • 폰트·픽셀 diff: CI와 로컬의 폰트 렌더링 차이를 의심하고, 허용치·Docker 기준을 통일합니다.
  • MSW 미적용 요청: 핸들러 누락·base URL 불일치·HTTPS 혼용을 점검합니다.

13. 맺음말

Playwright Component Testing은 실제 브라우저에서 컴포넌트 계약을 검증하는 강력한 도구입니다. 다만 실험적 기능이라 버전 정책을 주시하고, 전역 진입 스크립트·MSW·CI 환경을 프로덕션에 가깝게 맞추는 것이 성공적인 도입의 열쇠입니다. E2E·단위 테스트와 역할을 나누고, 팀에 맞는 비주얼·네트워크 목 전략을 문서화해 두면 유지보수 부담을 크게 줄일 수 있습니다.