namoman.com

디지털 정원과 실험실

Playwright를 이용한 웹 자동화 세션 영구 보존 및 실시간 검증 알고리즘

Playwright를 활용한 웹 자동화 작업을 진행하다 보면 가장 답답한 순간이 바로 ‘세션 유지’와 ‘봇 감지(Bot Detection)’를 통과하는 단계다. 매번 스크립트를 실행할 때마다 새롭게 로그인을 거쳐야 한다면 자동화의 의미가 크게 퇴색될 수밖에 없습니다. 특히 최근에는 클라우드플레어(Cloudflare) 같은 웹 방화벽이 강력해져서 단순한 Headless 브라우저 접근은 즉각적으로 차단당하기 일쑤죠. 이 문제를 근본적으로 해결하기 위해 ‘세션 영구 보존’ 및 ‘실시간 검증 알고리즘’ 구축 방법을 정리해 보았습니다.

영구 사용자 프로필 (Persistent Profile)의 강력함

일반적으로 Playwright나 Selenium을 기본 시크릿 모드나 임시 컨텍스트로 실행하면, 브라우저 프로세스가 종료될 때 쿠키와 로컬 스토리지가 즉시 증발합니다. 이를 해결하기 위해 도입하는 것이 바로 ‘영구 사용자 프로필(Persistent Context)’입니다. 지정된 로컬 디렉터리에 실제 크롬 프로필 데이터를 고스란히 누적하고 저장하는 방식이죠.

launch_persistent_context API를 사용하면 일회성 가상 환경이 아니라, 지정된 폴더 내부에 물리적인 Sqlite 및 LevelDB 데이터베이스가 생성됩니다. 사용자가 최초 한 번만 수동으로 로그인하면서 ‘자동 로그인’을 체크해 두면, 이후 브라우저가 닫히더라도 웹사이트 측에서 발급한 장기 유효 쿠키가 안전하게 하드디스크에 보존됩니다. 다시 스크립트를 실행했을 때 번거로운 로그인 화면을 완벽하게 건너뛸 수 있게 됩니다.

봇 감지 우회와 세션 고착화 기법

세션을 수집하고 저장할 때 가장 주의할 점은 웹사이트의 봇 차단 시스템을 조용히 회피하는 것입니다. 스크립트가 실행되는 것을 들키지 않으려면 브라우저 시작 인수(args)에 --disable-blink-features=AutomationControlled를 주입하여 브라우저 조작 플래그를 꺼야 해요. 또한, 페이지가 로드되기 전 자바스크립트 주입 API(add_init_script)를 통해 자동화 시스템의 고유 시그니처인 navigator.webdriver 속성을 undefined로 덮어씌워 봇 필터를 통과해야 합니다.

주의할 점은 로그인이 완료되었다고 해서 즉각적으로 브라우저 프로세스를 종료하면 안 된다는 점입니다. 리다이렉션으로 유입된 쿠키와 세션 데이터가 디스크에 물리적으로 완전히 기록(Flush) 되기 전에 프로그램이 닫혀버리면 데이터가 유실될 위험이 높거든요. 따라서 사용자의 엔터 입력 등으로 로그인 완료를 감지한 직후에는 반드시 3초 이상의 강제 지연 대기 시간을 두어 파일 기록 완결성을 보장해야 한다.

무인 환경에서의 실시간 세션 검증 알고리즘

저장된 쿠키가 만료되었는지 백그라운드에서 빠르게 검증하는 로직도 자동화 시스템의 핵심 요소입니다. 이 과정에서도 디도스(DDoS) 방어나 웹 방화벽 우회가 필요합니다. 보안이 철저한 사이트는 Headless 모드(무인 화면 숨김)를 켜두면 GPU 가속 여부나 픽셀 렌더링 부재를 판별해 봇으로 간주하고 보안 확인 페이지로 강제 이탈시킵니다.

이를 방지하기 위해 세션 검증 시에도 headless=False 상태로 잠깐 브라우저를 띄우는 유인 기동 방식을 사용합니다. 이후 회원 전용 페이지(예: 마이페이지)로 강제 접속을 시도하여 경로가 어떻게 변하는지 살피면 됩니다. 만약 로그인 페이지로 튕겨 나간다면 세션이 만료된 것이고, 정상적으로 회원 대시보드 요소가 렌더링된다면 생존한 것으로 판정할 수 있죠. 렌더링 동기화를 위해 wait_for_load_state("domcontentloaded")를 활용해 DOM 요소가 뜰 때까지 기다린 뒤 전역 자바스크립트 변수를 평가하는 것이 가장 확실하고 안정적인 방법입니다.

추가적으로 세션이 끊긴 상태에서 접근 시 “회원만 이용 가능합니다” 같은 alert 알림창이 뜨면서 전체 자바스크립트 스레드가 영원히 정지(Blocking)되는 사태를 막아야 합니다. 사전에 context.on("dialog") 이벤트 리스너를 매핑해 두고, 시스템에 팝업창이 뜨자마자 dialog.dismiss()를 비동기로 호출해 무력화하는 방어 코드 작성이 필수적입니다. 이러한 디테일이 뒷받침되어야 중간에 멈추지 않는 진정한 자동화 파이프라인이 완성됩니다.

# 🔑 Playwright를 이용한 웹 자동화 세션 영구 보존 및 실시간 검증 알고리즘

브라우저 자동화 도구(Playwright, Puppeteer, Selenium 등)를 사용하여 정기적으로 웹 서비스에 데이터를 업로드하거나 제어하는 스크립트를 빌드할 때 마주치는 가장 큰 장벽은 **'로그인 세션 유지'**와 **'보안 확인(웹 방화벽/봇 감지)'**입니다.

기본 시크릿 모드로 실행하면 브라우저가 꺼질 때 쿠키와 로컬 스토리지가 즉시 휘발되므로, 주기적인 반복 실행 작업 시 매번 로그인을 거쳐야 하는 심각한 번거로움이 따릅니다.

이 가이드에서는 **영구 사용자 프로필(Persistent Context)** 방식을 활용해 로그인 세션을 로컬 하드디스크에 영구 고착화하고, 실시간으로 세션 생존 여부를 무인 진단하며, 사이트의 자동화 봇 감지를 완벽하게 우회하는 범용적이고 체계적인 웹 자동화 아키텍처를 소개합니다.

---

## 1. 💡 핵심 아키텍처: 영구 사용자 프로필 (Persistent Profile)

Playwright의 `launch_persistent_context` API를 활용하여 지정된 디렉터리에 실제 사용자의 크롬 프로필 데이터를 고스란히 영구 누적 저장합니다.

```
[로컬 프로젝트 경로] 
└── automation_project/
    └── browser_profile/   <-- 쿠키, 세션 스토리지, 로컬 스토리지, 캐시 데이터가 실제 웹 브라우저처럼 보존됨
```

### 왜 이 방식인가?
* 일회성 게스트/시크릿 브라우저가 아닌, 물리적인 Sqlite 및 LevelDB 기반 데이터베이스가 하드에 남는 **독립된 전용 브라우저 프로필**이 생성됩니다.
* 로그인 시 "로그인 유지" 또는 "자동 로그인" 체크박스를 활성화하면, 브라우저가 종료되더라도 사이트 측에서 발급한 **장기 유효 쿠키가 자동으로 하드디스크에 기록**되어 다음 실행 시 로그인 단계를 건너뜁니다.

---

## 2. 📝 세션 획득 및 영구 저장 알고리즘 (Session Capture)

세션을 최초로 안전하게 수집하고 로컬 데이터베이스에 고착화하여 반영구 쿠키 환경을 셋팅하는 과정입니다.

```mermaid
graph TD
    A[세션 저장 스크립트 실행] --> B[browser_profile 폴더 바인딩]
    B --> C[Stealth 옵션 주입 및 User-Agent 위장]
    C --> D[유인 모드 headless=False 브라우저 가동]
    D --> E[대상 웹서비스 로그인 페이지 접속]
    E --> F[사용자 수동 로그인 완료 대기]
    F --> G[사용자 Enter 입력 감지]
    G --> H[★ 3초 세션 안정화 대기 - 비동기 데이터 플러시 완료]
    H --> I[브라우저 정상 종료 및 영구 보존 완료]
```

### ⚙️ 봇 감지 우회 및 안정화 구현 핵심
1. **스텔스 우회 (Stealth Bypass)**:
   * 브라우저 시작 인수(`args`)에 `--disable-blink-features=AutomationControlled`를 주입해 브라우저가 조작 상태임을 알리는 플래그를 비활성화합니다.
   * 페이지 객체가 로드되기 직전 전역 스크립트 주입 API(`add_init_script`)를 호출하여 자동화 시스템의 시그니처인 `navigator.webdriver` 객체를 `undefined`로 숨겨 차단 필터를 통과합니다.
2. **세션 동기화 지연 (3초 버퍼)**:
   * 사용자가 로그인 완료 후 엔터를 눌러 프로세스를 종료하는 즉시 브라우저를 닫으면, 직전 리다이렉션으로 들어온 쿠키 세션 정보가 디스크에 물리적으로 기록되지 못하고 유실될 위험이 높습니다.
   * 엔터 입력 감지 직후 **3초간 강제 지연 대기**(`wait_for_timeout(3000)`)를 두어 LevelDB 파일들의 데이터 플러시(Flush) 완결성을 보장합니다.

---

## 3. 🔍 실시간 세션 검증 알고리즘 (Session Verification)

저장된 영구 프로필 속 쿠키가 여전히 유효한 로그인 세션인지 무인(백그라운드) 상태에서 3초 만에 검사해 내는 핵심 진단 알고리즘입니다.

```mermaid
graph TD
    A[세션 진단 스크립트 실행] --> B[browser_profile 폴더 로드]
    B --> C[웹 방화벽 우회를 위한 headless=False 유인 기동]
    C --> D[로그인 회원만 접속 가능한 회원 전용 페이지 강제 접속]
    D --> E{리다이렉트 주소 및 타이틀 분석}
    E -->|로그인 페이지로 이탈| F[결과: 세션 만료 - 재로그인 필요]
    E -->|정상 유지| G[★ DOM 안정화 대기 - 필수 요소 감시]
    G --> H[페이지 내부 회원 플래그 변수/요소 평가]
    H --> I{로그인 회원 정보가 존재하는가?}
    I -->|Yes| J[✅ 결과: 세션 생존 - 성공]
    I -->|No| K[❌ 결과: 비회원 상태 - 세션 유실]
```

### ⚙️ 안정적인 무인 진단 구현 핵심
1. **디도스/웹 방화벽(Cloudflare 등) 우회**:
   * headless=True(무인 모드) 실행 시 보안이 강한 사이트들은 브라우저의 GPU 가속 여부나 화면 픽셀 매핑 부재를 간파하여 **"보안 확인(DDoS 방어)"** 화면으로 강제 이탈시킵니다.
   * 이를 해결하기 위해 백그라운드 대신 **`headless=False` (유인 모드)**로 브라우저를 잠깐 띄워 스크립트 봇 방지벽을 감쪽같이 즉시 통과시킵니다.
2. **DOM 렌더링 동기화 및 JS 변수 평가**:
   * 페이지 접속 후 `wait_for_load_state("domcontentloaded")`를 호출하고, 회원 전용 화면의 특정 UI 엘리먼트가 완전히 보일 때까지 대기합니다.
   * 렌더링이 완료된 직후 브라우저 자바스크립트 엔진 스레드를 호출해 글로벌 변수나 DOM의 데이터 플래그를 읽어와 최종적인 로그인 상태를 판정합니다.
3. **알림창(Alert Dialog) 블로킹 가로채기**:
   * 세션이 만료된 상태에서 회원 전용 페이지에 가려고 하면 브라우저의 모달 팝업(`alert("회원만 접근 가능합니다")`)이 나타나 전체 자바스크립트 스레드를 멈춰버리는 경우가 많습니다.
   * 사전에 `context.on("dialog")` 이벤트를 매핑하여 시스템에 팝업창이 뜨는 즉시 `dialog.dismiss()`를 비동기 호출해 알림창을 가볍게 넘어가도록 설계해야 프로세스의 멈춤이 발생하지 않습니다.

---

## 4. 📝 범용 템플릿 코드 구현 (Generic Template)

### 1) 세션 영구 저장 스크립트 (`save_session.py`)
```python
import asyncio
import os
from playwright.async_api import async_playwright

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PROFILE_DIR = os.path.join(BASE_DIR, "browser_profile")

async def run():
    async with async_playwright() as p:
        print("영구 크롬 프로필 디렉터리를 연결하여 실행합니다...")
        
        # 봇 차단을 우회하기 위한 스텔스 옵션 적용 및 프로필 디렉터리 매핑
        context = await p.chromium.launch_persistent_context(
            user_data_dir=PROFILE_DIR,
            headless=False,
            args=[
                "--disable-blink-features=AutomationControlled",
                "--no-sandbox"
            ],
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            viewport={"width": 1280, "height": 800}
        )
        
        # navigator.webdriver 변조를 통한 봇 체크 우회
        await context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
        
        page = context.pages[0] if context.pages else await context.new_page()
        
        # 대상 웹 서비스의 로그인 주소로 이동
        await page.goto("https://example.com/login")
        
        print("\n[안내] 열린 브라우저 창에서 수동 로그인을 완료해 주세요.")
        print("⚠️ 중요: 반드시 '자동 로그인'이나 '로그인 상태 유지' 체크박스를 켜주세요.")
        input("로그인을 완수하고 메인 화면으로 이동했다면, 터미널에서 [Enter] 키를 누르세요...")
        
        print("쿠키 세션 안정화를 위해 3초 대기합니다...")
        try:
            await page.wait_for_timeout(3000)
        except Exception:
            pass
            
        print(f"[성공] 로그인 정보 및 영구 쿠키가 '{PROFILE_DIR}' 폴더에 저장되었습니다.")
        
        try:
            await context.close()
        except Exception:
            pass

if __name__ == "__main__":
    asyncio.run(run())
```

### 2) 실시간 세션 검증 스크립트 (`check_session.py`)
```python
import os
import asyncio
from playwright.async_api import async_playwright

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PROFILE_DIR = os.path.join(BASE_DIR, "browser_profile")

async def test_session():
    if not os.path.exists(PROFILE_DIR):
        print("[결과] browser_profile 폴더가 없습니다. save_session.py를 먼저 실행해 주세요.")
        return

    async with async_playwright() as p:
        # 보안 확인 페이지(봇 우회)를 무력화하기 위해 headless=False(유인 브라우저)로 설정합니다.
        context = await p.chromium.launch_persistent_context(
            user_data_dir=PROFILE_DIR,
            headless=False,
            args=["--disable-blink-features=AutomationControlled", "--no-sandbox"],
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            viewport={"width": 1280, "height": 800}
        )
        await context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
        
        # 비회원 리다이렉트용 모달 팝업 차단 리스너 매핑
        async def handle_dialog(dialog):
            print(f"[알림창 가로채기] 메시지: {dialog.message}")
            await dialog.dismiss()
        context.on("dialog", handle_dialog)
        
        page = context.pages[0] if context.pages else await context.new_page()
        
        try:
            print("세션 상태 실시간 진단 중...")
            # 회원만 들어갈 수 있는 마이페이지 혹은 마크 폼 페이지로 접속 시도
            await page.goto("https://example.com/mypage")
            await page.wait_for_load_state("domcontentloaded")
            
            # 리다이렉트되어 로그인 화면으로 다시 튕겼는지 경로 대조
            if "login" in page.url:
                print("[결과] 세션이 완전히 만료되었습니다! 로그인이 필요합니다.")
            else:
                # 회원 전용 대시보드나 회원 아이콘 요소가 렌더링될 때까지 안전 대기
                try:
                    await page.wait_for_selector(".member-dashboard", timeout=3000)
                except Exception:
                    pass
                
                # 브라우저 전역 변수나 쿠키 등을 평가하여 로그인 여부 최종 판단
                is_logged_in = await page.evaluate("typeof someLoginFlag !== 'undefined' ? someLoginFlag : ''")
                
                if is_logged_in:
                    print("[결과] 세션이 정상 생존 중이며 회원 정보가 확인됩니다!")
                else:
                    print("[결과] 세션 분석 불가 (로그인 상태를 확인할 수 없습니다).")
                    
        except Exception as e:
            print(f"[오류] 검증 도중 에러가 발생했습니다: {e}")
        finally:
            await context.close()

if __name__ == "__main__":
    asyncio.run(test_session())
```

---

## 5. 📚 참고 문서 및 자원 (References)

* **Playwright Persistent Contexts**:
  * [Playwright Python API - launch_persistent_context Reference](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context)
* **Bypassing Headless Detection**:
  * [W3C WebDriver Specification - The Webdriver Property](https://www.w3.org/TR/webdriver/#interface)
* **Chromium Command Line Arguments**:
  * [List of Chromium Command Line Switches](https://peter.sh/experiments/chromium-command-line-switches/)

출처

  • Playwright 공식 문서 API 참조 (Python launch_persistent_context)
  • W3C WebDriver Specification (The Webdriver Property)
  • List of Chromium Command Line Switches (Peter Beverloo)