# 픽스처·인증 재사용·병렬 실행

> 매 테스트마다 같은 준비를 반복하고 있나요? Playwright의 커스텀 픽스처와 저장된 인증 상태로 반복을 없애고, 병렬 실행으로 속도를 끌어올립니다.

**Published:** 2026-07-21 | **Updated:** 2026-07-21

---


<!--
「프론트엔드 테스트 제대로 하기」 시리즈 초안. 본문 코드는 companion repo 실코드 기준으로 확정됨.
- 계획: _notes/series/frontend-testing-series-plan.md
- 이미지 가이드: _notes/image-guides/frontend-testing-playwright-fixtures-images.md
- repo 태그(발행 시 고정): step-13
- 발행 전: 이미지 제작, OG 배경, /translate-post 로 en 번역
-->

> 「프론트엔드 테스트 제대로 하기」 시리즈의 **13편**입니다. [전체 목차 보기](/series/프론트엔드-테스트-제대로-하기/) · [용어집](/posts/frontend-testing-glossary/)

E2E 테스트가 열 개를 넘어가면 두 가지가 아파옵니다. 테스트마다 복붙되는 **준비 코드**, 그리고 순서대로 도느라 **느려지는 스위트**. Playwright의 픽스처(반복 준비를 테스트에 주입하는 장치)와 병렬 실행이 이 둘을 한 번에 해결합니다 — 이 글에서 커스텀 픽스처, 인증 재사용(storageState), 병렬 안전 수칙까지 정리해요.

시리즈로 따라오셨다면 E2E가 슬슬 쌓이기 시작한 딱 그 시점이고, 검색으로 처음 오셨어도 괜찮습니다. 예제는 자급자족하고, 기초가 필요하면 [입문 편](/posts/frontend-testing-playwright-intro/)이 있어요.

> 실습 코드: 이 편의 상태가 [`step-13` 태그](https://github.com/IsaacEryn/frontend-testing-lab/tree/step-13)에 담겨 있습니다(픽스처·Page Object로 준비 코드를 공유한 상태 — 디버깅·axe 같은 뒤 편 코드는 아직 없어요). [StackBlitz에서 바로 열기](https://stackblitz.com/github/IsaacEryn/frontend-testing-lab/tree/step-13)도 됩니다.

군살을 뺄 곳부터 짚고 갑니다.

- 커스텀 픽스처로 준비 코드 공유
- 인증 상태 저장·재사용(storageState) 패턴
- 병렬 실행과 테스트 격리

{{< img src="images/contents/fixtures-auth.png" alt="저장된 인증 상태를 여러 테스트가 재사용하고 병렬로 실행되는 구조를 보여주는 다이어그램" >}}

---

## 커스텀 픽스처

사실 여러분은 픽스처를 이미 쓰고 계셨어요. [입문 편](/posts/frontend-testing-playwright-intro/)부터 테스트마다 받아온 `{ page }` — 그게 Playwright의 **내장 픽스처**입니다. 테스트 시작 전에 깨끗한 탭을 준비해서 건네주고, 끝나면 알아서 치우죠. 연극으로 치면 **무대 스태프**예요. 막이 오르기 전에 소품을 제자리에 놓고, 막이 내리면 정리하는.

오늘 배우는 건 그 스태프에게 **우리 일을 더 가르치는 것**입니다. "접속하고, 로딩을 기다리고"를 매번 쓰는 대신, 준비된 대시보드를 픽스처로 받습니다. (repo `e2e/fixtures.ts`)

```ts
import { test as base, type Page } from '@playwright/test'

class DashboardPage {
  readonly page: Page

  constructor(page: Page) {
    this.page = page
  }

  async goto() {
    await this.page.goto('/')
    await this.page.getByText('Alice Kim').waitFor()  // 목록 로딩까지 끝나야 '준비 완료'
  }

  search(q: string) {
    return this.page.getByRole('searchbox', { name: '검색' }).fill(q)
  }

  row(name: string) {
    // 이름이 포함된 목록 행을 찾는 로케이터 (쓰는 순간 조회)
    return this.page.getByRole('listitem').filter({ hasText: name })
  }
}

export const test = base.extend<{ dashboard: DashboardPage }>({
  dashboard: async ({ page }, use) => {
    const dashboard = new DashboardPage(page)
    await dashboard.goto()      // use 이전 = 준비 (접속 + 로딩 대기)
    await use(dashboard)        // 이 순간 테스트 본문이 실행된다
  },
})
export { expect } from '@playwright/test'
```

(생성자에서 `constructor(readonly page: Page)` 축약을 쓰지 않은 건 의도예요 — 최신 Vite 템플릿의 `erasableSyntaxOnly` 옵션과 충돌하거든요. 데모 repo에서 typecheck가 잡아준 실화입니다.)

구조가 낯설 테니 흐름을 풀어볼게요. `DashboardPage`는 "대시보드 페이지를 다루는 법"을 모아둔 클래스 — 이런 패턴을 **Page Object**라고 부릅니다. 그리고 `base.extend({ dashboard: ... })`가 픽스처 등록이에요. 픽스처 함수의 구조는 샌드위치입니다.

- `use(...)` **이전** — 준비 단계. dashboard를 만들고 `goto()`까지 마칩니다
- `use(dashboard)` — "자, 무대 준비 끝" — 이 순간 테스트 본문이 실행되고, 인자 `{ dashboard }`로 배달됩니다
- `use(...)` **이후** — 정리 단계. (여기선 정리할 게 없어 비어 있지만, DB 연결을 닫는 식의 뒷정리가 이 자리에 옵니다)

{{< img src="images/contents/fixture-sandwich.png" alt="픽스처의 샌드위치 구조 다이어그램 - base.extend로 등록된 픽스처 함수에서 use 호출 이전은 준비 단계로 dashboard 생성과 goto 실행, use(dashboard) 호출 시점에 테스트 본문이 실행되며 dashboard가 인자로 배달되고, use 이후는 정리 단계입니다. 무대 스태프가 막 전에 소품을 놓고 막 후에 정리하는 비유가 곁들여져 있습니다" >}}

테스트 본문은 시나리오에만 집중하게 됩니다. (repo `e2e/search-with-fixture.spec.ts`)

```ts
import { test, expect } from './fixtures'

test('픽스처로 준비를 공유해 검색한다', async ({ dashboard }) => {
  await dashboard.search('bob')

  await expect(dashboard.row('Bob Lee')).toBeVisible()
  await expect(dashboard.row('Alice Kim')).toHaveCount(0)
})
```

접속도, 로딩 대기도 본문에서 사라졌죠. 임포트가 `'@playwright/test'`가 아니라 `'./fixtures'`인 것만 눈여겨봐 주세요 — 우리가 가르친 스태프가 딸린 test를 쓰는 겁니다.

---

## 인증은 한 번만 (패턴)

데모 앱엔 로그인이 없지만, 실무의 필수 패턴이라 짚어둡니다. 전역 셋업에서 한 번 로그인하고 상태를 저장한 뒤, 모든 테스트가 재사용해요.

```ts
// global-setup: 로그인 후 상태 저장
await page.context().storageState({ path: 'playwright/.auth/user.json' })

// playwright.config.ts
use: { storageState: 'playwright/.auth/user.json' }
```

`storageState`가 저장하는 건 **쿠키와 localStorage** — 즉 "로그인됐다"는 증거 일체입니다. 이 파일을 물려받은 테스트는 처음부터 로그인된 상태로 시작해요. 매 테스트 로그인 대비 수십 초씩 아끼고, 로그인 UI가 바뀌어도 테스트 전체가 흔들리지 않습니다(고칠 곳은 전역 셋업 한 군데).

---

## 병렬과 격리

repo 설정의 `fullyParallel: true`로 파일·테스트가 병렬 실행됩니다. 검색 테스트 둘을 실제로 돌려보면:

```bash
npx playwright test -g "검색"

Running 2 tests using 2 workers
  ✓  2 [chromium] › e2e/dashboard.spec.ts:4:1 › 대시보드에서 사용자를 검색한다 (461ms)
  ✓  1 [chromium] › e2e/search-with-fixture.spec.ts:3:1 › 픽스처로 준비를 공유해 검색한다 (465ms)
  2 passed (1.8s)
```

`using 2 workers` — 브라우저 두 대가 **동시에** 돌았습니다. 완료 순서(2번이 먼저)가 파일 순서와 다른 것도 그 증거예요. 테스트가 둘뿐인 지금은 체감이 없지만, 수십 개쯤 쌓이면 이 워커들이 진가를 발휘합니다.

대신 조건이 하나 — 테스트끼리 **상태를 공유하면 안 됩니다.** 병렬 사고는 이런 식으로 나요: 테스트 A가 "항목을 삭제하면 목록이 빈다"를 검증하는 동안, 같은 계정을 쓰는 테스트 B가 그 목록을 읽고 있었다면? B는 영문도 모른 채 실패합니다. 순서대로 돌 땐 멀쩡하다가 병렬에서만 터지니, 최악의 flaky가 되죠. 각 테스트는 자기만의 데이터로 시작하고 끝나야 병렬에서 안전합니다. (전역 변수, 같은 계정의 데이터 수정이 단골 범인이에요.)

---

## 한 장 요약

- 반복되는 준비(접속·로딩 대기·로그인)는 **픽스처**로 — `base.extend`로 등록하면 테스트 인자로 배달됩니다
- 페이지 다루는 법은 **Page Object**(클래스)로 모으면 UI가 바뀌어도 한 곳만 고치면 됨
- 인증은 한 번만: 전역 셋업에서 로그인 → **storageState**로 저장 → 전 테스트가 재사용
- `fullyParallel: true`로 병렬 실행 — 단, 테스트끼리 **상태 공유 금지**(전역 변수·같은 계정 데이터 수정이 단골 범인)
- 픽스처 본문이 비대해지면 신호 — 준비는 픽스처에, 검증은 테스트에

---

## 정리하며

빠르고 깔끔한 E2E가 됐어요. 그래도 테스트는 깨집니다. 다음 편은 원인을 추적하는 디버깅입니다.

테스트 스위트가 5분에서 40초로 줄어드는 순간, 병렬의 신봉자가 됩니다. 단, 공유 상태를 하나라도 남기면 그 40초는 지옥이 되지만요.

> **레벨업**: 픽스처와 병렬화로 E2E 스위트를 빠르고 깔끔하게 유지할 수 있습니다.

> **다음 편**: 트레이스·디버깅·리트라이로 원인 추적

> 낯선 용어가 있었다면 — [용어집](/posts/frontend-testing-glossary/)에 전부 한 줄씩 정리돼 있어요.

