로그인의 흐름은 자주 바뀔 수 있다
캡차, OTP, SSO 같은 요소가 섞이면 자동화가 불안정해진다
그래서 사람 손으로 한 번 로그인을 하고, 이후에는 인증 상태를 저장하고 재사용하는 방법을 사용했다
전체 흐름
🧑 사람이 한번 GUI 브라우저로 수동 로그인을 하고 세션을 JSON 으로 저장한다
🤖 실행 시 저장된 JSON 을 Context 에 로드해서 로그인 상태로 시작한다
폴더 구조
project/
├── scripts/
│ └── save_login_session.py 👈 사람 손으로 로그인해서 세션 저장
├── keywords/
│ ├── login_keywords.py 👈 저장된 세션을 불러오는 Python 키워드
│ └── main_keywords.py 👈 페이지 접속 등 기능 실행
├── resource/
│ └── common.resource 👈 Robot 에서 사용할 Keyword 래퍼
├── tests/
│ └── mail.robot 👈 진짜 테스트 케이스
├── logged_in.json 👈 저장된 로그인 세션 (자동 로그인에 사용)
save_login_session.py 로 로그인 과정을 수행하고, 로그인 상태를 logged_in.json 파일에 저장한다
수동 로그인 & 세션 저장 코드
GUI 로 Browser 를 띄워 사람이 직접 로그인하고 Storage State 를 저장한다
# scripts/save_login_session.py
import asyncio
from playwright.async_api import async_playwright
SESSION_PATH = "logged_in.json"
async def save_login_session():
async with async_playwright() as p:
# GUI 열림 (headless=False)
browser = await p.chromium.launch(headless=False)
context = await browser.new_context()
page = await context.new_page()
print("🌐 웹페이지 접속 중...")
await page.goto("https://www.webpage.com")
print("🔐 로그인 버튼 클릭")
await page.get_by_role("button", name="로그인").click()
print("\n👉 수동으로 로그인하세요. 로그인 완료 후 터미널에 엔터를 눌러주세요.")
input()
# 로그인 세션 저장
await context.storage_state(path=SESSION_PATH)
print(f"\n✅ 로그인 세션 저장 완료: {SESSION_PATH}")
await browser.close()
아래 명령어를 실행하면 logged_in.json 파일이 자동으로 생성되고, 파일 안에 세션이 저장된다
python scripts/save_login_session.py
logged_in.json 파일의 구조는 아래와 같다
{
"cookies": [...],
"origins": [...]
}
cookies 에는 로그인 유지 (사용자 세션 유지) 에 필요한 쿠키 목록이 들어있고,
origins 에는 도메인 별로 저장된 localStorage 스냅샷 목록이 들어간다
저장된 세션 불러오기
저장된 Storage State 를 불러와 로그인된 컨텍스트를 생성한다
이 함수는 코루틴 객체를 반환하므로, 호출 측에서 await 로 받아 사용할 수 있다
# keywords/login_keywords.py
from playwright.async_api import async_playwright
SESSION_PATH = "logged_in.json"
def load_logged_in_context():
async def run():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(storage_state=SESSION_PATH)
return browser, context
return run() # 코루틴 객체를 반환해서 호출측에서 await로 받는다
저장된 세션으로 메일 화면 진입
한 번 호출로, 열고 이동하고 확인하고 닫기까지 끝낼 수 있도록 안쪽에서 코루틴을 실행까지 보장한다
# keywords/mail_keywords.py
from login_keywords import load_logged_in_context
def open_mail_page():
async def run():
browser, context = await load_logged_in_context()
page = await context.new_page()
await page.goto("https://www.naver.com")
await page.get_by_role("link", name="메일").click()
await page.wait_for_url("https://mail.naver.com/*")
assert "메일" in await page.title()
await browser.close()
import asyncio
try:
# 현재 스레드에 이벤트 루프가 없으면
# 여기서 새 루프를 만들어 run()을 끝날 때까지 실행
return asyncio.run(run())
except RuntimeError:
# 이미 이벤트 루프가 돌고 있는 환경이면
# 여기서 실행하면 충돌하니 코루틴만 넘기고 호출자가 await 하게 함
return run()
비로그인으로 열고 싶을 때
Storage State 를 지정하지 않으면 비로그인 상태로 동작한다
context = browser.new_context() # storage_state 없음
Resource 에서 Keyword 래핑하기
Resource 에서 Keyword 를 한 번 더 감싸면, 내부 Playwright API 나 구현이 바뀌어도 유지보수가 쉬워진다
*** Settings ***
Library ../keywords/login_keywords.py
Library ../keywords/mail_keywords.py
Library ../src/main_page_test_case.py
*** Keywords ***
...
Access to Mail w Login
[Documentation] 로그인 한 상태로 메일 화면 진입
[Arguments] ${url}
Access Mail w Login ${url}
Robot Framework 로 실행하기
Test Suite 는 Resource 의 Wrapping Keyword 를 그대로 호출한다
Test Setup/ Teardown 으로 공통 초기화/ 정리를 자동화하면 케이스마다 보일러플레이트를 없앨 수 있다
*** Settings ***
Resource ../resource/web_access.resource
Resource ../resource/main_page.resource
Test Setup Initialize Environment
Test Teardown Close Environment
*** Test Cases ***
...
Check the Mail w Login
Access Mail w login https://www.webpage.com
실행 결과

주의할 점
logged_in.json 은 민감한 정보가 들어갈 수 있으니 반드시 .gitignore 에 추가한다
저장된 세션은 사이트 정책에 따라 만료될 수 있으니 실패 시 저장 스크립트를 다시 실행한다
저장과 사용 환경의 브라우저 버전, User-Agent, 타임존이 크게 다르면 세션이 거부될 수 있으니 가능하면 동일하게 맞춘다
'TIL > Playwright' 카테고리의 다른 글
| Playwright 로 API Test 해보기 w/ OpenAPI (0) | 2025.06.30 |
|---|---|
| Playwright Python: Tutorial #5 - API Testing (0) | 2025.02.28 |
| Playwright Python: Tutorial #4 - Browser Context (0) | 2025.02.27 |
| Playwright Python: Tutorial #3 - Test Generator (0) | 2025.02.24 |
| Playwright Python: Tutorial #2 - Pytest (0) | 2025.02.21 |