
Playwright로 로그인 세션을 재사용할 때 가장 먼저 부딪히는 문제는 storageState()로 저장한 쿠키 파일을 그대로 믿어버리는 것입니다. 쿠키에는 만료 시각이 분명히 남아있지만, Playwright는 그 세션이 서버 쪽에서 이미 무효화됐는지까지는 알려주지 않습니다. 이 글에서는 로그인 세션을 저장하고 재사용할 때 실제로 쓰는 코드와, 파일 안의 만료 시각을 직접 검사하는 방법, 그리고 검사를 건너뛰면 어떤 일이 생기는지를 순서대로 정리합니다.
storageState()로 로그인 세션을 파일에 저장하는 방법
Playwright의 BrowserContext에는 storageState()라는 메서드가 있습니다. 이 메서드를 호출하면 현재 컨텍스트가 가진 쿠키와, 각 origin별 localStorage 값을 하나의 JSON 스냅숏으로 뽑아낼 수 있습니다. 로그인 폼을 자동으로 입력하고 로그인이 끝난 직후에 이 스냅숏을 파일로 저장해두면, 다음 실행부터는 로그인 과정을 건너뛸 수 있습니다.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com/login');
await page.fill('#username', 'myid');
await page.fill('#password', 'mypassword');
await page.click('#login-button');
await page.waitForURL('https://example.com/dashboard');
// 쿠키 + localStorage를 state.json에 저장
await context.storageState({ path: 'state.json' });
await browser.close();
이렇게 생성된 state.json을 열어보면 cookies 배열과 origins 배열이 들어있습니다. origins는 도메인별 localStorage를 담는 자리인데, JWT 기반 로그인을 쓰는 서비스라면 토큰이 쿠키가 아니라 여기에 저장돼 있는 경우가 많습니다. 자세한 스키마는 공식 문서의 스토리지 상태 가이드에 나와 있습니다.
저장한 세션을 다음 실행에서 재사용하는 방법
저장해둔 state.json을 다시 쓸 때는 browser.newContext()를 호출하면서 storageState 옵션에 파일 경로를 넘기면 됩니다. 이 옵션이 켜지면 새 컨텍스트가 만들어지자마자 쿠키와 localStorage가 그대로 복원되므로, 로그인 페이지를 다시 거치지 않고 바로 보호된 페이지로 이동할 수 있습니다.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext({ storageState: 'state.json' });
const page = await context.newPage();
await page.goto('https://example.com/dashboard');
console.log(page.url());
// https://example.com/dashboard (로그인 페이지로 리다이렉트되지 않음)

@playwright/test로 테스트를 짜는 경우라면 playwright.config.ts의 use.storageState에 같은 경로를 지정해두는 방식도 많이 씁니다. 다만 이 방식은 컨텍스트가 생성되는 시점에만 쿠키를 심어줄 뿐이라서, 테스트가 오래 도는 동안 세션이 만료돼도 Playwright가 따로 알려주지는 않습니다. 재사용할 때 이 부분을 놓치면 뒤에서 다룰 문제가 그대로 재현됩니다.
state.json 안의 쿠키, 만료 시각은 어떻게 확인하나요?
쿠키 객체에는 expires라는 필드가 있는데, 이 값은 유닉스 타임(초 단위)입니다. 브라우저가 세션 쿠키로 취급하는 항목, 즉 만료 시각 없이 브라우저 종료 시 사라지는 쿠키는 expires가 -1로 표시됩니다. 이 필드를 현재 시각과 직접 비교하면 파일을 열어보지 않고도 만료된 쿠키를 걸러낼 수 있습니다.
import fs from 'fs';
const state = JSON.parse(fs.readFileSync('state.json', 'utf-8'));
const now = Date.now() / 1000;
const expired = state.cookies.filter(
(c) => c.expires !== -1 && c.expires < now
);
if (expired.length > 0) {
console.log('만료된 쿠키:', expired.map((c) => c.name));
} else {
console.log('아직 유효한 쿠키만 있습니다.');
}
실행 결과는 이런 식으로 나옵니다.
만료된 쿠키: [ 'session_id', 'auth_token' ]
로그인 세션을 자동화 스크립트에서 재사용할 때는 이 검사를 컨텍스트를 만들기 전에 돌려서, 만료된 쿠키가 하나라도 있으면 로그인 절차를 다시 태우는 식으로 분기하는 편이 안전합니다.
쿠키 만료 시각만 믿으면 조용히 실패합니다
쿠키의 expires 값이 아직 남아있어도 서버가 그 세션을 이미 무효화했을 수 있습니다. 관리자가 강제로 로그아웃시켰거나, 서버가 세션 스토어(레디스 등)에서 해당 세션을 지운 경우가 대표적입니다. 이때 Playwright는 아무 오류도 내지 않습니다. newContext({ storageState })는 그냥 쿠키를 브라우저에 심어줄 뿐이고, 실제로 로그인 상태인지는 페이지를 열어서 서버 응답을 봐야만 알 수 있습니다.
JWT를 localStorage에 저장하는 서비스라면 문제가 하나 더 있습니다. state.json의 origins 항목에는 토큰 문자열만 들어있고, 쿠키처럼 expires 필드가 따로 붙어있지 않습니다. 만료 여부를 확인하려면 토큰 자체를 디코딩해서 exp 클레임을 읽어야 합니다.

function isJwtExpired(token) {
const payloadBase64 = token.split('.')[1];
const payload = JSON.parse(
Buffer.from(payloadBase64, 'base64').toString('utf-8')
);
return payload.exp * 1000 < Date.now();
}
이 방식은 표준 base64url 인코딩에서 -와 _를 +, /로 바꿔주지 않으면 토큰에 따라 디코딩이 깨질 수 있다는 점을 감안해야 합니다. 쿠키 기반 세션과 토큰 기반 세션을 같은 코드로 검사하려 하면 이런 세부 차이 때문에 둘 중 하나는 놓치기 쉽습니다.
만료 감지 뒤에는 재로그인까지 이어지게 짜보세요
가장 실용적인 방식은 쿠키 만료 검사와 실제 접속 확인을 같이 두는 것입니다. state.json의 expires로 1차 필터링을 하고, 실제로 보호된 페이지에 들어가서 로그인 페이지로 리다이렉트되는지를 확인하는 2차 검사를 붙이면 서버 쪽 세션 무효화까지 잡아낼 수 있습니다.
const context = await browser.newContext({ storageState: 'state.json' });
const page = await context.newPage();
await page.goto('https://example.com/dashboard');
if (page.url().includes('/login')) {
console.log('세션이 만료됨 → 재로그인 진행');
await page.fill('#username', 'myid');
await page.fill('#password', 'mypassword');
await page.click('#login-button');
await page.waitForURL('https://example.com/dashboard');
await context.storageState({ path: 'state.json' }); // 갱신
} else {
console.log('기존 세션 재사용 성공');
}
이 패턴은 쿠키 검사만으로는 잡히지 않던 서버 측 만료까지 커버하지만, 매번 실제 페이지 이동이 한 번 더 들어가기 때문에 스크립트 실행 시간이 그만큼 늘어난다는 트레이드오프가 있습니다. 크롤링 주기가 아주 짧거나 요청 수가 많은 환경이라면, 쿠키 파일의 expires 검사만으로 대부분을 걸러내고 실제 접속 확인은 일정 주기마다만 돌리는 절충안도 고려할 만합니다.
expires가 남아있는데도 왜 로그아웃 상태로 뜨나요?
앞서 다룬 대로 쿠키의 expires는 브라우저 쪽 만료 시각일 뿐, 서버가 세션을 유효하다고 보장하는 값이 아닙니다. 세션 스토어를 쓰는 백엔드는 관리자가 세션을 강제 종료하거나, 같은 계정으로 다른 기기에서 로그인해 기존 세션을 밀어내는 정책을 두는 경우가 흔합니다. 이런 무효화는 쿠키 파일 어디에도 기록되지 않으므로, 앞 절에서 소개한 실제 페이지 접속 확인을 빼놓으면 이 상황을 걸러낼 방법이 없습니다.
Playwright로 로그인 세션을 재사용할 때는 결국 storageState()로 저장한 파일이 "쿠키가 아직 안 지났다"는 것만 보장하지, "서버가 여전히 이 세션을 인정한다"는 것까지는 보장하지 않는다는 점을 기준으로 코드를 짜는 편이 안전합니다. 쿠키 만료 검사로 1차로 걸러내고, 실제 접속으로 최종 확인하는 두 단계를 같이 두는 것부터 적용해보시기 바랍니다.
