C.W.K.
Stream
Lesson 02 of 04 · published

Storage State — 한 번 로그인, 영원히 재사용

~16 min · playwright-advanced, auth, storage-state

Level 0테스트 호기심
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"매 테스트마다 로그인하는 건 auth 세금 50번 내는 거."

테스트별 로그인 비용

전형적 로그인 흐름이 5-15초 걸려 — 로그인 페이지로 navigate, 이메일 fill, 비밀번호 fill, submit 클릭, redirect 기다림, 대시보드 기다림. 매 테스트가 그걸로 시작하면, 50 테스트 suite 가 순수 로그인에만 4-12분 추가. 세 브라우저 곱하면 CI 예산을 auth 로 다 쓴 거.

해법: 한 번 로그인, 결과 브라우저 state (쿠키, localStorage, sessionStorage) 캡처하고 다른 모든 테스트에 주입. 각 테스트가 이미 로그인된 상태로 시작. 로그인 흐름 자체는 여전히 작동하는지 검증하는 전용 테스트 ONE 받아.

Setup Project 패턴

Playwright 의 project 의존성 모델이 완벽히 맞아:

  1. 다른 모든 거 전에 도는 setup project 정의.
  2. Setup spec 이 로그인 흐름 통해 ONCE navigate 하고 storage state 를 파일에 저장 (보통 playwright/.auth/user.json).
  3. 다른 project 들이 dependencies: ['setup']use: { storageState: 'playwright/.auth/user.json' } 선언.
  4. 그 project 의 모든 테스트가 그 storage state 이미 로드된 채 시작 — 이미 로그인됨.

앱이 multiple role 필요하면 (admin, member, anonymous) role 당 setup 하나 생성하고 project 가 맞는 state 파일 고르게 해.

Auth state 가 가는 곳

playwright/.auth/ 가 관례적 디렉토리. .gitignore 에 추가 — 파일이 진짜 세션 토큰 포함. 매 테스트 setup 이 파일을 fresh 쓰니까, run 사이 절대 stale 안 함.

로그인 흐름은 한 번 테스트. 로그인이 가능하게 하는 거는 여러 번 테스트. 50 테스트 auth-required suite 가 50 로그인-흐름 테스트 포함하면 안 돼. 로그인 흐름을 집중된 spec 하나로; 다른 모든 건 로그인 작동했다 가정하고 로그인 후 기능 테스트.

API 기반 로그인 — 더 빠름

앱이 auth 토큰 반환하는 API endpoint 가지면 (대부분 현대 앱이 그래), UI 로그인 전적으로 skip 가능:

  1. Playwright 의 request fixture 통해 POST /api/auth/login.
  2. 결과 쿠키 / localStorage 아이템을 context.addCookies()context.addInitScript() 통해 설정.
  3. Storage state 저장.

이게 초 단위 대신 ~100ms 걸려. Trade-off: 진짜 로그인 UI 우회, 그래서 로그인 흐름이 그 UI 가 여전히 작동하는지 확인하는 전용 테스트 필요.

테스트별 로그아웃

어떤 테스트는 로그아웃 상태로 시작 필요 — 로그인 흐름 테스트, 공개 페이지 테스트. 그것들엔 테스트별이나 파일별로 project 설정 오버라이드: test.use({ storageState: { cookies: [], origins: [] } }) 가 그 파일에 빈 auth state 줘. 공개 테스트용 Playwright project 는 storageState 전혀 선언 안 해야.

Code

Setup spec — 한 번 돌고, auth 저장·typescript
// e2e/global.setup.ts — runs first, saves auth state for everyone else
import { test as setup, expect } from '@playwright/test';

const USER_AUTH_FILE = 'playwright/.auth/user.json';

setup('authenticate as standard user', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Wait for the post-login landing page so the cookie is actually set.
  await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();

  // Save cookies + localStorage to the file.
  await page.context().storageState({ path: USER_AUTH_FILE });
});
Project setup — 체인된 의존성 + project 별 state·typescript
// playwright.config.ts — projects that consume the saved state
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    // Setup project — runs first, no auth itself
    {
      name: 'setup',
      testMatch: /global\.setup\.ts/,
    },

    // Authenticated project — depends on setup, loads the saved state
    {
      name: 'chromium',
      dependencies: ['setup'],
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
    },

    // Public project — no auth (for landing page, login flow, etc.)
    {
      name: 'chromium-public',
      testMatch: /\.public\.spec\.ts/,
      use: { ...devices['Desktop Chrome'] },
      // No storageState — these tests are logged out.
    },
  ],
});
저장된 state 쓰는 테스트 (또는 명시적으로 안 쓰는)·typescript
// Authenticated test — starts logged in already
import { test, expect } from '@playwright/test';

test('shows the dashboard for the logged-in user', async ({ page }) => {
  await page.goto('/dashboard');
  // No login dance — the storage state from setup is already loaded.
  await expect(
    page.getByRole('heading', { name: /welcome back/i })
  ).toBeVisible();
});

// e2e/landing.public.spec.ts — runs in the chromium-public project
import { test as publicTest, expect } from '@playwright/test';

publicTest('landing page shows Sign in for anonymous users', async ({ page }) => {
  await page.goto('/');
  await expect(page.getByRole('link', { name: 'Sign in' })).toBeVisible();
});
API 기반 auth — UI 로그인보다 빠름·typescript
// e2e/global.setup.ts — API-based auth (much faster)
import { test as setup, expect } from '@playwright/test';

const USER_AUTH_FILE = 'playwright/.auth/user.json';

setup('authenticate via API', async ({ request, page }) => {
  // Hit the login API directly — ~100ms instead of ~5s
  const response = await request.post('/api/auth/login', {
    data: {
      email: process.env.TEST_USER_EMAIL,
      password: process.env.TEST_USER_PASSWORD,
    },
  });
  expect(response.ok()).toBeTruthy();

  // The API set a Set-Cookie header on the response. Now save the context state.
  await page.context().storageState({ path: USER_AUTH_FILE });
});

External links

Exercise

기존 E2E suite 가져와 (없으면 예제 로그인 흐름 setup). Storage-state 패턴 구현: 로그인하고 state 저장하는 setup project, 그걸 쓰는 authenticated project. Suite 런타임 전후 측정 — auth-required 테스트 10개만 있는 suite 도 눈에 띄는 drop 봐야. 보너스: setup 의 API 기반 변형 추가하고 UI 로그인보다 얼마나 빠른지 봐.
Hint
Authenticated 테스트가 'expected logged-in state, got login page' 로 실패하면, storageState 파일이 안 써졌거나 (setup project 가 조용히 실패) 파일 path 가 매칭 안 함. --project=setup 으로 setup 만 돌리고 파일 존재 확인, 그 다음 authenticated 테스트 하나 돌려.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.