[오픈소스] 안드로이드 Termux 환경에서 Playwright와 Chromium을 완벽 구동하는 엔지니어링 심층 아키텍처 및 라이브러리 개발기 (termux-playwright)
1. 서론: 왜 안드로이드 공기계인가?
현대 데이터 엔지니어링과 AI 모델 파이프라인에서 웹 크롤링과 데이터 수집은 필수적인 인프라입니다. 하지만 클라우드 가상 서버(AWS EC2, GCP)를 24시간 켜두는 것은 매달 상당한 인프라 비용을 발생시킵니다.
반면, 서랍 속에 방치된 안드로이드 공기계는 다음과 같은 놀라운 하드웨어 스펙을 갖추고 있습니다:
그러나 공식 Playwright를 안드로이드의 대표적인 터미널 환경인 Termux(터먹스)에서 실행하려고 하면 수많은 시스템 레벨 크래시와 마주치게 됩니다. 이 문제를 원천 해결하기 위해 밑바닥부터 설계한 termux-playwright 라이브러리의 구조와 핵심 메커니즘을 설명합니다.
2. 기술적 난제: 공식 Playwright가 안드로이드에서 폭발하는 6대 원인
2.1 glibc vs Android Bionic libc 시스템 콜의 근본적 불일치
공식 Playwright(playwright-python)는 데스크톱 Linux 표준인 glibc 기반으로 빌드된 Node.js 드라이버 바이너리를 내려받아 실행합니다. 하지만 안드로이드는 구글이 독자 개발한 경량 C 라이브러리인 Bionic libc를 사용합니다. 이로 인해 데스크톱 바이너리는 심볼 로딩(dlopen) 단계에서 즉시 No such file or directory 또는 Segment Fault를 발생시키며 즉사합니다.
2.2 하드코딩된 플랫폼 검증 차단
Playwright의 핵심 브라우저 제어 드라이버인 coreBundle.js 내부에는 process.platform !== 'android' 검증 로직이 엄격하게 하드코딩되어 있습니다. 안드로이드 환경이 감지되는 순간 드라이버가 스스로 프로세스를 강제 종료합니다.
2.3 C-확장 모듈 빌드 폭탄 (Greenlet Clang OOM 사살)
Playwright 파이썬 클라이언트는 비동기 코루틴 컨텍스트 스위칭을 위해 C-확장 모듈인 greenlet에 의존합니다. 사용자가 pip install playwright를 실행하면 pip는 Bionic 호환 greenlet C소스를 직접 컴파일하려고 시도합니다. 이 과정에서 Clang 컴파일러가 1.2GB 이상의 RAM을 순간적으로 점유하여 안드로이드 커널의 OOM Killer에 의해 파이썬 프로세스가 사살됩니다.
2.4 공유 메모리(/dev/shm) 부재로 인한 SIGBUS 크래시
크로미움은 다중 렌더러 프로세스 간 고속 데이터 교환을 위해 POSIX 공유 메모리(/dev/shm)를 필수로 요구합니다. 하지만 안드로이드 OS는 보안상 /dev/shm 마운트를 지원하지 않습니다. 이로 인해 웹페이지 렌더링 시 크로미움 자식 프로세스들이 Bus Error (SIGBUS)를 뿜으며 일제히 크래시됩니다.
2.5 고아 좀비 프로세스 누수 및 RAM 고갈
파이썬 스크립트가 비정상 종료되거나 예외가 발생할 때, 백그라운드에 생성된 Chromium 메인 프로세스, GPU 프로세스, 렌더러 프로세스들은 종료되지 않고 Android init(PID 1) 프로세스로 입양(Re-parenting)됩니다. 이 좀비 프로세스들이 수십 개씩 누적되어 스마트폰의 RAM을 완전히 잠식합니다.
2.6 안드로이드 12~14+ Phantom Process Killer
안드로이드 12부터 도입된 '팬텀 프로세스 킬러(Phantom Process Killer)'는 백그라운드 앱이 생성한 자식 프로세스의 총합이 32개를 넘어가면 해당 앱 전체를 커널 레벨에서 SIGKILL (signal 9)로 사살합니다. 탭을 몇 개만 띄워도 프로세스 수가 32개를 쉽게 초과하여 크롤러가 의문사하게 됩니다.
3. termux-playwright의 핵심 아키텍처 및 솔루션
3.1 런타임 계층 분리 및 전수조사 총괄표
3.2 디스크 기반 세션 영속 장부 (Persistent Disk Ledger)
기존 메모리 기반 프로세스 관리자는 안드로이드 LMK(Low Memory Killer)나 kill -9로 파이썬이 사살될 경우 추적 장부가 증발하는 치명적인 결함이 있었습니다.
termux-playwright는 세션 시작 시 $TMPDIR/.tp_ledger/{token}.session에 파일로 소유자 PID를 원자적으로 기록합니다. 다음번 크롤러 기동 시, 살아있는 OS PID 테이블을 스캔하여 이전 크래시로 방치된 고아 크로미움 프로세스를 100% 추적하여 사살합니다.
3.3 프로토타입 체인 안전 안티봇 스텔스 (Stealth Evasion)
Cloudflare Turnstile, DataDome, Akamai 등 최신 안티봇 시스템은 Object.defineProperty(navigator, 'webdriver') 수준의 단순 위장을 쉽게 탐지합니다. navigator.hasOwnProperty('webdriver') === true인지 검사하기 때문입니다.
termux-playwright는 프로토타입 체인 자체에서 속성을 삭제(delete Object.getPrototypeOf(navigator).webdriver)하고, 네이티브 C++ 함수 형태의 permissions.query와 window.chrome.runtime 객체를 동적으로 에뮬레이션합니다.
3.4 eMMC 플래시 메모리 수명 보호 (RAM 캐시 강제 주입)
모바일 기기의 eMMC/UFS 저장장치는 잦은 쓰기 작업 시 수명이 급격히 단축됩니다. termux-playwright는 Chromium 기동 시 --disk-cache-dir=/dev/shm, --disk-cache-size=1, --media-cache-size=1 플래그를 자동 주입하여 디스크 I/O를 RAM으로 전환, 기기 수명을 영구 보존합니다.
3.5 안드로이드 14 단일 프로세스 모드 (`single_process=True`)
루팅이나 ADB 권한이 없는 순정 안드로이드 기기에서도 32개 프로세스 한계(Phantom Process Killer)를 우회할 수 있도록 single_process=True 옵션을 제공합니다. 모든 브라우저 탭과 렌더러를 단 1개의 프로세스로 통합 가동합니다.
4. 설치 및 환경 구축
4.1 초간단 1줄 자동 설치 (추천)
pip install termux-playwright && termux-playwright-install
4.2 수동 정석 5단계 설치 파이프라인
1단계: OS 시스템 패키지 설치
pkg update -y && pkg install -y python python-pip python-greenlet chromium nodejs-lts procps termux-api
2단계: 가상환경 생성 (venv 사용 시 필수)
python -m venv --system-site-packages myenv
source myenv/bin/activate
3단계: 파이썬 패키지 설치
pip install termux-playwright
4단계: 핵심 드라이버 원자적 패치
termux-playwright-patch
5단계: 시스템 무결성 자가 진단
termux-playwright-doctor
5. 실전 프로덕션 코드 레시피
레시피 1: Cloudflare Turnstile 우회 스텔스 크롤러
import asyncio
from termux_playwright import async_playwright_termux, launch, setup_stealth_context
async def main():
async with async_playwright_termux() as p:
# 스텔스 엔진 및 크로미움 기동
browser = await launch(p, headless=True, stealth=True)
context = await setup_stealth_context(
browser,
locale="en-US",
timezone_id="America/New_York",
extra_headers={"Accept-Language": "en-US,en;q=0.9"}
)
page = await context.new_page()
await page.goto("https://bot.sannysoft.com", timeout=60000)
title = await page.title()
print(f"탐지 테스트 페이지 타이틀: {title}")
await browser.close()
if __name__ == "__main__":
asyncio.run(main())
레시피 2: 24시간 무인 가동 저전력 데몬 (WakeLock & 리소스 차단)
import asyncio
from termux_playwright import async_playwright_termux, launch, block_heavy_resources
async def run_worker():
while True:
try:
async with async_playwright_termux() as p:
# CPU 절전 방지 WakeLock 및 저메모리 모드 적용
browser = await launch(p, headless=True, low_memory_mode=True, wake_lock=True)
page = await browser.new_page()
# 이미지, 폰트, 미디어 차단으로 트래픽/CPU 70% 절감
await block_heavy_resources(page, images=True, media=True, fonts=True)
await page.goto("https://news.ycombinator.com", timeout=45000, wait_until="domcontentloaded")
title = await page.title()
print(f"데이터 수집 완료: {title}")
await browser.close()
except Exception as e:
print(f"사이클 예외 발생 (자동 복구 중): {e}")
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(run_worker())