
Playwright 테스트나 스크레이퍼가 인증이 필요할 때, 매번 처음부터 로그인할 이유는 거의 없다. 흔한 방법은 한 번 로그인한 뒤 storageState로 쿠키와 관련 브라우저 상태를 저장하고, 이후 실행에서 그 상태를 불러와 같은 세션을 재사용하는 것이다. 하지만 저장된 storageState 파일이 있다고 로그인이 영원히 유효한 것은 아니다. 세션은 만료될 수도, 서버가 폐기할 수도, 처음부터 제대로 로드되지 않을 수도 있다.
자신의 계정을 자동화하고 일상 브라우저에 이미 로그인해 있다면 다른 선택지가 있다. 그 기존 브라우저 세션을 바로 재사용하는 것이다. ego (lite)는 필요한 로그인 상태를 이미 가진 브라우저에서 AI 에이전트가 일하게 하면서, 각 작업을 각자의 Space에 격리한다. 작업이 인증 코드, QR 로그인, 또는 사람이 필요한 다른 단계에 도달하면 MFA를 자동화하려 하지 말고 보이는 브라우저에서 이어받으면 된다.
어느 쪽이든 목표는 같다. 자동화가 실제로 유효한 인증 세션을 갖고 있는지 확인하는 것이다. 갑자기 로그인 페이지로 떨어지는 실행, 401을 반환하는 API, 나타나지 않는 로그인 후 요소는 탐색, 권한, 또는 셀렉터 문제처럼 세 가지 다른 문제로 보일 수 있지만, 모두 같은 깨진 세션을 가리킬 수 있다. 아래 절에서는 재현 가능한 로컬 환경으로 Playwright에서 인증 상태를 저장하고, 재사용하고, 확인하고, 갱신하는 방법을 보여 준다.
깨진 Playwright 로그인이 어떻게 보이는지
픽스처 실행에서 폐기된 세션 하나가 세 증상을 한꺼번에 냈다. 서버 측 세션을 지운 뒤, 저장된 상태를 재사용하는 context는 대시보드를 가리키는 next 파라미터와 함께 로그인 페이지로 리다이렉트됐고, 인증된 JSON 엔드포인트 프로브는 401을 반환했으며, 대시보드 테이블 대기는 설정된 3초 후 타임아웃됐다.
공개 the-internet 로그인에서도 같은 실패 유형은 bounce입니다. 살아있는 세션 없이 /secure를 열면 /login으로 떨어지고 빨간 플래시 You must login to view the secure area가 나옵니다. 아래 콜아웃의 URL 검사가 이것입니다. 브라우저는 locator가 기대한 페이지가 아니라 로그인 폼에 있습니다. 이 스크린샷이 그 bounce입니다. Logout이 storageState 파일을 지웠다는 증거가 아닙니다. the-internet Logout은 현재 창 세션을 끝냅니다. 디스크에 이미 쓴 파일은 쿠키 자체가 죽을 때까지 /secure를 다시 열 수 있습니다.

세 번째 증상이 비싼 쪽이다. 로그인 후 요소에서 locator가 타임아웃됐다고 해서 locator가 틀린 것은 아니다. 보고 있는 페이지가 가정한 페이지가 아니라는 뜻이다. 위 실행에서 테이블이 렌더되지 않은 이유는 브라우저가 로그인 폼에 머물러 있었기 때문이다. 이를 셀렉터 문제로 취급하면 한 번도 깨지지 않은 쿼리를 고치게 된다.
인증 실패 뒤의 네 가지 근본 원인
인증 실패는 네 가지 원인으로 모이고, 고치는 방법도 각각 다르다. 고치기 전에 분류해야 수정이 wait 문 한 줄 추가가 되지 않는다.
1. 상태가 한 번도 로드되지 않음
storageState 없이 만든 context는 이전 실행이 어땠든 처음부터 로그아웃 상태다. 잘못된 시점에 파일을 저장하거나, 상대 경로가 다른 곳으로 해석되거나, 상태가 의존 프로젝트로 전달되지 않는 project에서 setup을 실행해도 결과는 같다. 신호는 새 context가 실패한 것과 똑같이 동작한다는 점이다. 둘 다 로그아웃이다.
2. Cookie는 있지만 죽어 있음
세션 Cookie는 존재하고 올바른 도메인과 플래그를 가져도 서버가 거부할 수 있다. 세션 Cookie에는 자체 수명이 있고, 서버는 재배포, 비밀번호 변경, 유휴 타임아웃을 포함해 언제든 폐기할 수 있다. Cookie의 expires 필드는 힌트이지 보장이 아니다. 서버 측 세션이 더 먼저 죽을 수 있다. 픽스처가 명시적인 revoke 호출로 재현하는 경우가 바로 이것이다.
3. context 사이의 격리
Cookie와 localStorage는 브라우저가 아니라 브라우저 context에 속한다. 한 context에서 상태를 저장하고 설정이 다른 context가 공유하기를 기대하면 조용히 실패한다. 병렬 worker도 같다. worker마다 자기 context가 있으므로 각자 상태 파일이나 각자 로그인 단계가 필요하다. 이 격리는 기능이다. 한곳에서는 되는 세션이 다른 곳에서는 사라진 것처럼 보이는 이유이기도 하다.
4. SSO 리다이렉트 체인
싱글 사인온에서는 들어가려는 앱이 세션을 가진 origin인 경우가 거의 없다. 로그인은 다른 도메인의 ID 제공자로 리다이렉트되고, 제공자가 자체 Cookie를 설정한 뒤 code나 Token과 함께 제어가 앱으로 돌아온다. 체인이 끝나기 전에 상태를 저장하면 세션의 일부만 잡고 IdP Cookie를 놓치며, 다음 실행에서 같은 리다이렉트가 재현된다. 신뢰할 수 있는 패턴은 최종 앱 URL과 인증 마커를 기다린 뒤에 저장해, 체인에 관여한 모든 origin을 상태 파일에 싣는 것이다.
한 번 로그인하고 상태를 올바르게 저장하기
한 번 로그인하는 흐름은 짧다. 이동하고, 인증하고, 인증된 상태를 확인한 다음 storageState를 파일에 쓴다. 확인 단계가 잘 되는 상태 파일과 가끔 안 되는 상태 파일을 가른다.
import { chromium } from "playwright";
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://app.example.com/login");
await page.fill("input[name=email]", process.env.APP_EMAIL);
await page.fill("input[name=password]", process.env.APP_PASSWORD);
await page.click("button[type=submit]");
// Wait for the authenticated destination, not just for the click to land.
await page.waitForURL("**/dashboard");
// Verify before saving: an authenticated element plus a 200 from the API.
await page.waitForSelector("#revenue-table");
const probe = await context.request.get("https://app.example.com/api/summary");
if (probe.status() !== 200) throw new Error("login did not take");
await context.storageState({ path: "playwright/.auth/user.json" });
await browser.close();픽스처 실행에서 이 단계는 로그인 페이지를 연 뒤 상태 파일을 쓰기까지 119밀리초가 걸렸다. 파일에는 Cookie 하나, 서버가 발급한 세션 식별자가 들어 있었고 프로브는 200을 반환했다. 이런 숫자는 픽스처 고유의 성질이지만, 옮길 수 있는 것은 검사의 형태다. 목적지 URL을 확인하고, 보이는 인증 요소를 확인하고, 인증된 엔드포인트를 확인한 뒤에야 저장한다.
아래 headed 실행은 fixture 대시보드가 아니라 the-internet.herokuapp.com입니다. 119밀리초는 fixture 숫자입니다. 스크린샷은 공개 로그인에서도 같은 저장 형태를 보여 줍니다. 인증 후 /secure, 보이는 Logout, /tmp/pw-auth.json을 확인하는 Claude입니다.

테스트 스위트에서는 같은 생각이 파일을 만드는 setup project와 이를 소비하는 의존 project로 나타난다. 이것이 공식 인증 가이드가 문서화한 패턴이다. 스크립트와 스크레이퍼에서는 위 순서가 전체 흐름이다. 어느 쪽이든 이 파일은 자격 증명이다. 살아있는 세션 Cookie가 들어 있으므로 비밀번호와 같이 다루고 버전 관리에 넣지 않는다.
테스트와 스크레이퍼에서 저장한 상태 재사용하기
재사용은 context를 만들 때 한 줄을 바꾸는 일이다. 스크립트에서는 파일을 context에 넘긴다. Playwright 테스트 스위트에서는 project별 또는 테스트 파일별로 설정해 모든 worker가 같은 로그인 상태에서 시작하게 한다.
// Script: create the context from the saved state.
const context = await browser.newContext({
storageState: "playwright/.auth/user.json",
});
// Test: use the state for this file (or configure it per project).
test.use({ storageState: "playwright/.auth/user.json" });픽스처 실행에서 저장된 파일로 만든 새 context는 로그인 단계 없이 6밀리초 만에 보호된 대시보드에 도달했고, 인증 프로브는 예상 페이로드와 함께 200을 반환했다. 흥미로운 숫자는 6밀리초가 아니다. 흐름에서 바뀐 것은 context 상태의 출처뿐이라는 점이다.
재사용 스크린샷도 같은 공개 사이트입니다. 새 headed context가 /tmp/pw-auth.json을 불러오고 로그인 폼을 건너뛴 뒤, 페이지에 이미 Logout이 있는 /secure를 열었습니다. 6밀리초는 여전히 fixture 시간입니다.

상태 파일이 무엇을 담고 무엇을 담지 않는지 알아 둘 가치가 있다. context가 건드린 모든 origin의 Cookie와 origin별 localStorage 항목은 들어간다. IndexedDB, session storage, service worker 상태는 들어가지 않는다. Token을 IndexedDB에 두는 앱은 따라서 추가 캡처 단계나 재로그인이 필요하다. storageState가 이를 커버하리라 기대하는 것이 유령 실패의 흔한 원인이다. Cookie 자체는 도메인, 경로, 플래그의 지배를 받으며 설명은 MDN Cookie 가이드와 Set-Cookie 레퍼런스에 있다.
만료되거나 폐기된 세션 감지하기
가장 싼 감지기는 비싼 작업 전에 보내는 인증된 요청이다. 대부분의 앱에는 현재 사용자, 개수, 설정 객체를 돌려주는 작은 엔드포인트가 있고, 그 상태 코드가 세션의 상태 코드다. 요청은 밀리초가 들고, 스크랩 중간에 실패를 발견하면 실행 전체가 든다.
async function sessionIsAlive(context) {
const probe = await context.request.get(
"https://app.example.com/api/summary",
{ failOnStatusCode: false },
);
return probe.status() === 200;
}
if (!(await sessionIsAlive(context))) {
await loginAndSaveState(context);
}Cookie의 expires 필드만 믿으면 안 된다. 그것은 브라우저가 Cookie 전송을 멈춰야 하는 시점이지, 서버가 받기를 멈추는 시점이 아니다. 서버 측 폐기는 물어보기 전까지 보이지 않는다. 픽스처의 revoke 호출이 바로 그 상황이다. Cookie는 아직 있고 형태도 맞았지만 서버는 401을 답했다.
자동으로 다시 인증하기
자동 재인증은 작은 상태 기계다. 감지하고, 다시 로그인하고, 저장한 상태를 갱신하고, 실패한 단계를 한 번 다시 실행한 뒤 확인한다. 상한이 중요하다. 루프에서 조용히 재인증하는 워크플로는 잘못된 비밀번호, 계정 잠금, 바뀐 로그인 페이지를 가릴 수 있고, 그 과정에서 트래픽을 기꺼이 만든다.
async function withSession(page, context, task) {
if (!(await sessionIsAlive(context))) {
await loginAndSaveState(context, page); // refresh file after login
}
try {
return await task();
} catch (error) {
if (!(await sessionIsAlive(context))) {
await loginAndSaveState(context, page); // one retry, then fail loudly
return await task();
}
throw error;
}
}픽스처에서 감지기가 401을 봤고, 재로그인과 확인이 99밀리초에 끝났으며, 갱신된 상태 파일은 다시 세션 Cookie를 정확히 하나 갖고 있었다. 실행은 보호된 대시보드에서 끝났고 테이블이 보이며 세 행이 렌더됐다. 이 세 가지 확인 없이 끝나는 재인증은 끝난 것이 아니다.
여러 worker가 상태 파일 하나를 공유하면 모두가 같은 순간에 만료를 알아채고 재로그인을 경합한다. 처방은 single-flight 조율이다. 첫 worker가 파일을 갱신하고 나머지는 기다리거나, 각 worker가 자기 복사본을 갱신한다. 메커니즘은 여러 에이전트가 같은 브라우저 프로필을 상대로 돌 때 나타나는 세션 지속성 문제와 같다.
여러 계정과 병렬 실행
격리는 context 단위이므로 규칙은 계정 또는 역할마다 상태 파일 하나, worker마다 context 하나, 공유하는 가변 세션은 없다는 것이다. 픽스처에서 두 병렬 로그인은 서로 다른 세션 Cookie 두 개를 만들었고, 두 프로브는 같은 밀리초에 200을 반환했으며, 어느 context도 상대 세션을 보지 못했다. 깨졌을 때의 실패 모드가 미묘하므로 이 성질은 의도적으로 지켜야 한다. 두 계정이 서로의 상태를 덮어쓰고 테스트가 잘못된 사용자로 통과한다.
같은 규칙은 실제 브라우저에도 나타납니다. Space 두 개는 context 두 개입니다. 하나는 새 탭에서 idle로 두고, 다른 하나는 로그인한 Airbnb 세션을 유지할 수 있으며, headed 창 하나처럼 쿠키를 공유하지 않습니다. 핵심은 격리입니다. 개요는 둘을 한눈에 보는 방법일 뿐입니다.

인증이 실제로 적용됐는지 확인하기
검증은 세 가지 검사이며, 하나라도 건너뛰면 워크플로가 조용히 깨진 채로 남는다. 첫 번째는 긍정이다. 로그인 후에만 존재하는 요소가 있다. 두 번째는 부정이다. 로그인 폼이 없으므로 둘 다 있는 페이지는 통과하지 못한다. 세 번째는 데이터 검사로, 오래된 페이지나 캐시된 페이지에서는 실패한다. 실행마다 바뀌어야 하는 값 같은 것이다.
await page.waitForSelector("#revenue-table"); // positive
await expect(page.locator("#login-form")).toHaveCount(0); // negative
const probe = await context.request.get("/api/summary"); // data
assert.equal(probe.status(), 200);
assert.ok((await probe.json()).q3Revenue);픽스처의 재사용 실행에서 세 가지가 모두 통과했다. 테이블이 보였고, 로그인 폼 개수는 0이었으며, JSON 페이로드에 예상 필드가 있었다. 셋이 합쳐도 몇 밀리초면 되고, 작동하는 세션과 깨진 세션의 차이를 추론에서 사실로 바꾼다. 여전히 이상한 실행을 위한 더 많은 디버깅 기법은 Playwright 디버그 가이드에 있고, 모범 사례 페이지는 주변 습관을 다룬다. 어떤 wait를 쓸 가치가 있는지도 포함한다.
세션이 일상 브라우저에 있을 때
Playwright storageState 파일은 내가 소유하는 것이다. 계정이 픽스처이고 비밀번호가 CI secrets에 있으며 기계 앞에 사람이 없을 때 맞는 도구다. 로그인이 내 것일 때는 틀린 도구다. SSO, 하드웨어 키, 푸시 프롬프트, 일상 Chrome에서 이미 따뜻하게 유지하는 세션이다. 그때의 일은 로그인을 다시 조립하는 것이 아니다. 이미 갖고 있는 브라우저를 빌리는 것이다.
ego (lite)가 그 빌림이다. 에이전트는 이미 쓰는 로그인된 탭을 열고, 마우스를 빼앗지 않는 Space에서 작업을 이어가며, 두 번째 요소나 결제 확인이 사람을 필요로 하면 멈춘다. 세션이 디스크의 JSON 파일이 되는 일은 없다. 핵심은 이것이다. 개인 로그인은 브라우저에 남아야 하며, 나중에 CI가 실수로 커밋할 storageState 경로를 타면 안 된다.

두 도구를 따로 둔다. 무인 테스트 계정은 storageState에 남긴다. Playwright 인증 가이드가 설명하는 대로다. 개인 대시보드는 보이는 브라우저에 남긴다. ego (lite)는 MFA를 클릭하지 않고, 결제를 확인하지 않으며, 자격 증명을 수집하지 않는다. 이러한 정지점은 Space 문서와 빠른 시작에 문서화돼 있다. 현재 제품 버전은 0.5.0.32다(changelog, 2026-09-12). 더 새로운 빌드를 인용하기 전에 changelog와 GitHub 저장소를 다시 확인한다.
FAQ
storageState를 저장한 뒤에도 Playwright가 로그인 페이지로 떨어지는 이유는?
storageState 이후에도 로그인 페이지로 떨어지는 이유는 파일을 너무 일찍 저장했거나, context가 한 번도 로드하지 않았거나, 서버가 Cookie를 폐기했기 때문이다. 먼저 파일의 Cookie를 보고, 그다음 context 옵션을 본다. 둘 다 맞아 보이면 탐색 버그가 아니라 죽은 세션으로 취급한다.
세션 Cookie는 storageState를 거쳐도 살아남나?
살아남는다. 파일은 만료 유무와 관계없이 Cookie를 기록하고 origin별 localStorage도 기록한다. 살아남지 않는 것은 브라우저가 그 구조 밖에 두는 것이다. IndexedDB, session storage, 캐시, service worker다.
저장한 Playwright 로그인은 얼마나 가나?
서버가 그 세션을 계속 받아들이는 동안이다. 그것은 서버의 결정이고 언제든 뒤집을 수 있다. Cookie의 만료일은 상한이지 약속이 아니다. 오래 둔 상태 파일은 프로브가 아니라고 할 때까지 낡은 것으로 취급한다.
병렬 worker가 storageState 파일 하나를 공유할 수 있나?
읽을 수는 있고, 각 worker의 context는 격리된다. 문제는 한 worker가 재로그인 후 파일을 갱신하는 동안 다른 worker가 실행 중일 때 생긴다. 각 worker에 자기 복사본을 주거나, 갱신을 직렬화해 한 번에 로그인 하나만 일어나게 한다.
Playwright에서 OAuth나 SSO로 어떻게 인증하나?
ID 제공자의 Cookie를 유지할 context에서 전체 리다이렉트 체인을 한 번 끝까지 마치고, 앱의 최종 URL에 도달한 뒤에만 상태를 저장한다. 제공자가 상호작용 단계를 요구하면 같은 실행에서 손으로 한 번 하고, 이후에는 그 상태를 재사용한다.
MFA와 일회용 코드는 어떻게 다뤄야 하나?
저장해 둔 코드로 두 번째 요소를 자동화해서는 안 된다. 정직한 패턴은 결과가 저장된 세션이 되는 일회성 사람 단계이거나, 내가 소유한 브라우저에서 이미 인증된 세션이다. OTP 비밀이나 SMS 코드를 상태 파일 옆에 두면 보안 통제가 부채가 된다.
storageState를 버전 관리에 커밋해도 안전한가?
안전하지 않다. 파일에는 살아있는 세션 Cookie가 들어 있다. 작업 디렉터리에 두고 git에서는 무시하며, CI에서는 시크릿 저장소에서 주입한다. 한 번이라도 커밋된 적이 있다면 세션이 유출된 것으로 보고 폐기한다.
만료된 세션을 자동화가 다루는지 의도적으로 어떻게 테스트하나?
픽스처의 revoke 엔드포인트처럼 서버 측에서 세션을 무효화하는 테스트 훅을 넣고, 재사용 경로를 그 위에 실행한다. 리다이렉트, 프로브의 401, 복구를 어서트한다. 이 한 테스트가 그렇지 않으면 프로덕션에서만 타는 코드 경로를 커버한다.
앱이 Token을 IndexedDB에 저장하면?
storageState는 이를 나르지 않으므로 두 길 중 하나를 쓴다. 로그인 후 페이지 스크립트로 Token을 캡처해 재사용 시 다시 주입하거나, Token을 발급하는 API로 각 실행 안에서 인증한다. 전자가 더 빠르고, 후자는 앱 내부에 덜 결합된다.
상태를 재사용하지 않고 매번 다시 인증해야 하나?
일회용 테스트 계정을 쓰는 CI에서는 실행마다 재인증하는 편이 더 안전한 기본값이다. 안정적인 세션을 상대로 자주 도는 작업에서는 재사용에 프로브와 상한 있는 재로그인을 더하는 쪽이 더 싸고 추론하기 쉽다. 둘 다 정당하다. 확인하지 않은 재사용은 정당하지 않다.
로컬에서는 로그인이 되고 CI에서는 실패하는 이유는?
대개 CI의 상태 파일이 없거나 더 오래됐거나, 시계가 다르거나, 로그인 페이지가 새 IP에 다른 변형을 주기 때문이다. 같은 헤드리스 설정, viewport, 상태 파일로 재현하고 스위트 실행 전에 세션을 프로브한다. 실패는 코드 차이가 아니라 환경인 경우가 많다.
storageState 파일 하나로 계정 두 개를 쓸 수 있나?
없다. 합치려 하면 마지막에 쓴 Cookie가 남는 경향이 있다. 계정 또는 역할마다 파일을 하나씩 쓰고, 속한 신원으로 이름을 붙인 뒤, 각 context에 맞는 것을 넘긴다.