| name | storybook-visual-testing |
| description | Storybook 10 + @storybook/test-runner + Playwright toHaveScreenshot로 자체 호스팅(외부 SaaS 의존 없는) 시각 회귀 테스트 셋업 — preVisit/postVisit 훅, axe a11y 자동 검증, baseline 운영 정책, 모노레포 멀티 Storybook |
Storybook Visual Testing — 자체 호스팅 시각 회귀
소스: https://storybook.js.org/docs/releases/migration-guide | https://github.com/storybookjs/test-runner | https://playwright.dev/docs/test-snapshots
검증일: 2026-08-11
대상 버전: Storybook 10.5.x · @storybook/test-runner 0.x · Playwright v1.59.x
언제 사용하는가
- Storybook으로 문서화된 컴포넌트들을 외부 SaaS(Chromatic·Percy) 없이 자체 CI에서 시각 회귀로 잠그고 싶을 때
- 디자인 시스템·UI 라이브러리에서 컴포넌트 단위 픽셀 회귀를 PR마다 검증할 때
- 보안·비용 이슈로 스크린샷을 외부 서비스에 업로드할 수 없을 때
- 모노레포에서 여러 Storybook(예: MUI 기반 / Radix 기반)을 동시에 시각 검증할 때
언제 사용하지 않는가
- 페이지 단위 사용자 흐름 검증 →
frontend/e2e-testing 스킬 (Playwright E2E)
- Storybook 기본 설치·CSF 3 스토리 작성·Controls·play function →
frontend/storybook 스킬
- 크로스 브라우저 픽셀 회귀가 핵심 가치인 대규모 디자인 시스템 → Chromatic 같은 SaaS가 운영 비용 대비 효율적
- CI 인프라(셀프호스팅 러너 또는 Docker 이미지) 없이 macOS/Windows 로컬에서 baseline을 만들어 운영 → 픽셀 차이로 깨짐
1. Storybook 10 핵심 변화
자체 호스팅 시각 회귀 셋업은 Storybook 10 기준으로 작성한다. 9.x 이하와는 ESM 요구사항·Node 버전이 다르다.
Node.js 요구사항 (강제)
| 항목 | 요구 버전 |
|---|
| Node.js | 20.19+ 또는 22.12+ |
| 이유 | require(esm) 기본 지원 (플래그 없이) |
주의: 공식 마이그레이션 가이드 기준 **20.19+ 또는 22.12+**다. 일부 블로그·커뮤니티 글에서 "20.16+ / 22.19+"로 적힌 경우가 있는데, Storybook 공식 docs(storybook.js.org/docs/releases/migration-guide) 기준은 20.19+/22.12+가 정확하다. CI 이미지(Docker, GitHub Actions)도 이 버전 이상으로 강제해야 한다.
- uses: actions/setup-node@v7
with:
node-version: '22.12'
ESM-only 패키징
Storybook 10부터 패키지가 ESM 전용으로 배포된다. 결과:
.storybook/main.{js,ts}, preview.{js,ts}는 유효한 ESM이어야 한다 (CommonJS module.exports = {} 사용 금지, export default {} 사용)
- 프로젝트
package.json에 "type": "module"이 없는 CommonJS 프로젝트라도 .storybook/main.ts 자체는 ESM 문법을 사용
- 설치 크기 약 29% 감소, dist 코드 비-minify로 디버깅 용이
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx|mdx)'],
framework: '@storybook/react-vite',
addons: [
'@storybook/addon-docs',
'@storybook/addon-a11y',
],
};
export default config;
주의: @storybook/addon-essentials / @storybook/addon-interactions를 addons에 남겨두면 안 된다 — v9에서 제거되어 controls·actions·backgrounds·viewport·toolbars·measure·outline·highlight·인터랙션 패널이 모두 코어에 내장됐다. 남아 있으면 Storybook 시작 시 에러가 난다. 패키지 이동 대응표 전체는 frontend/storybook 스킬의 "v8 → v10 마이그레이션 노트" 참조.
Storybook 9 → 10 차이 요약
| 항목 | Storybook 9 | Storybook 10 |
|---|
| Node 요구 | 18+ / 20+ | 20.19+ / 22.12+ |
| 패키지 형식 | CJS + ESM 듀얼 | ESM only |
| main/preview | CJS·ESM 모두 가능 | ESM 필수 |
| essentials 애드온 | v9에서 제거 (코어 내장) | addons에 남기면 에러 |
| a11y addon | @storybook/addon-a11y 별도 패키지 | 여전히 별도 패키지 (Recommended 설치에 기본 포함) |
| 자동 마이그레이션 | — | npx storybook@latest upgrade |
주의: 10.4~10.5의 추가 사항(Vite 8 / Next.js 16.2 지원 등)은 시각 테스트 셋업에 직접 영향 없음. 현재 최신 안정은 10.5.x다.
2. @storybook/test-runner 설치·설정
@storybook/test-runner는 Jest를 러너로, Playwright를 브라우저 자동화 엔진으로 쓰는 도구다. 각 *.stories.tsx 파일을 spec으로 변환해 헤드리스 브라우저에서 실행한다.
주의: Vite 기반 Storybook의 경우 공식 문서가 Vitest addon 사용을 권장하기 시작했다. 하지만 Playwright 기반 시각 회귀(픽셀 비교) 워크플로는 여전히 @storybook/test-runner + Playwright의 toHaveScreenshot이 가장 표준적이다. 본 스킬은 이 조합을 다룬다.
설치
npm install -D @storybook/test-runner @playwright/test
npx playwright install --with-deps chromium
package.json 스크립트
{
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test-storybook": "test-storybook",
"test-storybook:ci": "concurrently -k -s first -n SB,TEST \"npm:storybook -- --ci\" \"wait-on tcp:6006 && test-storybook --url http://127.0.0.1:6006\"",
"test-storybook:static": "concurrently -k -s first -n SB,TEST \"npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && test-storybook --url http://127.0.0.1:6006\""
}
}
| 스크립트 | 용도 |
|---|
test-storybook | 로컬 storybook dev 실행 중에 사용 |
test-storybook:ci | dev 서버를 직접 띄워 함께 실행 (개발용) |
test-storybook:static | CI 권장 — build-storybook 후 정적 서빙으로 가장 빠르고 결정적 |
test-runner.ts 설정 (.storybook/test-runner.ts)
import type { TestRunnerConfig } from '@storybook/test-runner';
const config: TestRunnerConfig = {
async setup() {
},
async preVisit(page, context) {
},
async postVisit(page, context) {
},
};
export default config;
주의: preRender / postRender 훅은 deprecated다. 신규 코드는 preVisit / postVisit만 사용한다.
훅 시그니처:
async preVisit(page: Page, context: { id; title; name }): Promise<void>
async postVisit(page: Page, context: { id; title; name }): Promise<void>
3. axe-playwright로 a11y 자동 검증
postVisit에 axe를 끼워 넣으면 모든 스토리 렌더링 직후 자동으로 접근성 검사가 돌아간다. 시각 회귀와 같은 파이프라인에서 a11y까지 한 번에 잡을 수 있다.
npm install -D axe-playwright
import type { TestRunnerConfig } from '@storybook/test-runner';
import { getStoryContext } from '@storybook/test-runner';
import { injectAxe, checkA11y, configureAxe } from 'axe-playwright';
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page, context) {
const storyContext = await getStoryContext(page, context);
if (storyContext.parameters?.a11y?.disable) return;
await configureAxe(page, {
rules: storyContext.parameters?.a11y?.config?.rules,
});
await checkA11y(page, '#storybook-root', {
detailedReport: true,
detailedReportOptions: { html: true },
axeOptions: storyContext.parameters?.?.,
});
},
};
config;
@storybook/addon-a11y는 개발 중 a11y 패널로 위반을 표시해줄 뿐이다. CI 파이프라인에서 a11y를 빌드 게이팅으로 강제하려면 여전히 axe-playwright + postVisit 조합이 가장 명시적이다.
4. Playwright toHaveScreenshot로 베이스라인 캡처
Storybook은 iframe.html?id={story-id}&viewMode=story로 단일 스토리를 격리 렌더링한다. 이 페이지를 Playwright로 찍어 baseline 비교한다.
패턴 A: test-runner의 postVisit에서 직접 스크린샷
import type { TestRunnerConfig } from '@storybook/test-runner';
import { waitForPageReady } from '@storybook/test-runner';
import { expect } from '@playwright/test';
const config: TestRunnerConfig = {
async postVisit(page, context) {
await waitForPageReady(page);
const root = page.locator('#storybook-root');
await expect(root).toHaveScreenshot(`${context.id}.png`, {
animations: 'disabled',
maxDiffPixelRatio: 0.01,
});
},
};
export default config;
패턴 B: 별도 Playwright 프로젝트에서 iframe.html 직접 방문
복잡한 시나리오(다중 뷰포트, 다중 테마)는 Playwright 테스트 파일로 빼는 게 깔끔하다.
import { test, expect } from '@playwright/test';
import storyIndex from '../../storybook-static/index.json';
const stories = Object.values(storyIndex.entries).filter(
(e: any) => e.type === 'story'
);
for (const story of stories) {
test(`visual: ${story.title} - ${story.name}`, async ({ page }) => {
await page.goto(`/iframe.html?id=${story.id}&viewMode=story`);
await page.waitForLoadState('networkidle');
await expect(page.locator('#storybook-root')).toHaveScreenshot(
`${story.id}.png`,
{ animations: 'disabled' }
);
});
}
toHaveScreenshot 핵심 옵션
| 옵션 | 의미 | 권장값 |
|---|
animations | 'disabled' 시 CSS 애니메이션·트랜지션 정지 | 'disabled' |
mask | Locator 배열, 해당 영역을 핑크색으로 덮음 (시간/광고 등 동적 영역) | 동적 영역만 |
stylePath | 스크린샷용 CSS 주입(Shadow DOM 관통) | 동적 요소 숨김 |
threshold | YIQ 색공간 차이 허용치(0=엄격, 1=느슨) | 0.2 (기본) |
maxDiffPixels | 절대 허용 픽셀 수 | 작은 컴포넌트 100~500 |
maxDiffPixelRatio | 전체 대비 허용 비율(0~1) | 0.01 (1%) |
fullPage | 페이지 전체 캡처 | 컴포넌트 범위면 false |
clip | 캡처할 직사각형 영역 | 일부만 비교할 때 |
maxDiffPixels와 maxDiffPixelRatio는 함께 쓰면 둘 중 하나만 만족해도 통과한다(OR). 보수적으로 가려면 한쪽만 사용.
풀페이지 vs 컴포넌트 영역
await expect(page.locator('#storybook-root')).toHaveScreenshot();
await expect(page).toHaveScreenshot({ fullPage: true });
동적 영역 마스킹
await expect(page.locator('#storybook-root')).toHaveScreenshot('card.png', {
mask: [
page.getByTestId('current-time'),
page.getByTestId('user-avatar-id'),
],
});
또는 stylePath로 CSS 주입:
[data-testid="current-time"],
[data-testid="ad-slot"] {
visibility: hidden !important;
}
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
}
5. Baseline 운영 정책
환경 일관성 — 가장 중요
Playwright는 baseline 파일명에 플랫폼이 자동 포함된다: card-chromium-darwin.png, card-chromium-linux.png. 다른 OS에서 만든 baseline은 호환되지 않는다. 폰트 렌더링·서브픽셀 위치·안티앨리어싱이 OS별로 미묘하게 달라 1px 차이로 깨진다.
규칙:
- Baseline 생성·검증은 동일 환경에서
- 권장: GitHub Actions Linux 러너 또는 Playwright 공식 Docker 이미지
- macOS 개발자 로컬에서 만든 PNG를 git에 commit하지 않는다
- Docker 이미지 강제 (선택지)
docker run --rm -v $(pwd):/work -w /work \
mcr.microsoft.com/playwright:v1.59.1-jammy \
npx playwright test --update-snapshots
- Chromium만 사용 — Firefox/WebKit은 픽셀이 또 달라 baseline 3배가 됨. 컴포넌트 시각 회귀는 chromium 단일 채널로 충분
Baseline 갱신 워크플로
npx playwright test --update-snapshots
test-storybook -- --update-snapshots
PR 룰: baseline .png 변경은 반드시 같은 PR에 코드 변경과 함께 commit. 리뷰어가 코드 diff와 시각 diff를 함께 본다.
Threshold 권장값
| 상황 | 권장 설정 |
|---|
| 디자인 시스템 컴포넌트 (엄격) | maxDiffPixelRatio: 0.001 (0.1%) |
| 일반 UI 컴포넌트 | maxDiffPixelRatio: 0.01 (1%) |
| 폰트·아이콘 변동이 잦음 | maxDiffPixels: 500 + threshold: 0.3 |
| Linux Docker 환경 동일 | 0~minimal threshold (가장 엄격) |
전역 설정은 playwright.config.ts:
export default defineConfig({
expect: {
toHaveScreenshot: {
animations: 'disabled',
maxDiffPixelRatio: 0.01,
},
},
});
상세 레퍼런스 (예제·고급 패턴·흔한 실수) → references/REFERENCE.md