로컬에선 되는데 서버에선 안 되는 헤드리스 크롤러, 원인 좁히기

로컬에선 되는데 서버에선

결론부터 말씀드리면, 로컬에선 되는데 서버에선 안 되는 크롤러 문제는 거의 항상 “코드가 틀렸다”가 아니라 “환경이 다르다”에서 생깁니다. 브라우저 바이너리에 필요한 리눅스 라이브러리가 없거나, 서버의 데이터센터 IP가 차단당하거나, 화면이 없는 환경에서 폰트·언어 설정이 달라서 셀렉터가 어긋나는 세 가지 경우가 대부분을 차지합니다. 이 글에서는 Playwright와 Puppeteer 기준으로, 로컬과 서버의 차이를 하나씩 좁혀가는 순서를 정리합니다.

로컬 환경과 서버 환경, 실제로 뭐가 다른지부터 확인해야 합니다

로컬 PC는 GUI가 있고, 폰트가 풍부하게 깔려 있고, 집이나 회사의 일반 IP로 접속합니다. 반면 서버(특히 클라우드 VM이나 Docker 컨테이너)는 화면 출력 장치가 없고, 리눅스 배포판 최소 설치본이라 공유 라이브러리와 폰트가 빠져 있는 경우가 많습니다.

여기에 서버의 아웃바운드 IP는 AWS, GCP, 네이버클라우드 같은 데이터센터 대역이라서, 대상 사이트가 이 IP 대역 자체를 통째로 차단하거나 추가 인증을 요구하는 일도 흔합니다. 같은 코드, 같은 Node.js 버전이라도 이 세 가지 조건이 다르면 결과가 갈립니다.

서버에서 가장 많이 걸리는 두 가지 — 의존성 누락과 리소스 제한

Playwright나 Puppeteer가 내려받는 크로미움은 리눅스 서버에서 libnss3, libatk-bridge2.0-0, libgbm1 같은 공유 라이브러리가 없으면 브라우저 프로세스가 아예 뜨지 않습니다. 이때 에러 메시지는 “타임아웃”이나 “Target closed”처럼 모호하게 나와서, 코드 문제로 오해하기 쉽습니다.

어두운 터미널 화면을 바라보며 코드를 디버깅하는 개발자

Playwright는 이 문제를 해결하려고 의존성 자동 설치 명령을 공식 제공합니다.

npx playwright install --with-deps chromium

이 명령은 운영체제별로 필요한 패키지를 자동으로 잡아주는데, 자세한 지원 OS 목록과 도커 환경에서의 설정법은 공식 문서에 정리돼 있습니다. 또 하나는 메모리입니다. 저가형 서버 인스턴스는 1~2GB 메모리라 크로미움 탭 하나가 500MB 이상 쓰면 OOM(Out of Memory)으로 프로세스가 죽는데, 이 경우도 로그는 “크롤러가 멈췄다”로만 보입니다.

스크린샷은 찍히는데 셀렉터를 못 찾는 이유가 뭘까요?

브라우저 자체는 잘 뜨는데 원하는 데이터를 못 가져오는 경우도 많습니다. 이때는 “브라우저 문제”가 아니라 “페이지 내용이 다르다”로 봐야 합니다. 대표적으로 대상 사이트가 헤드리스 브라우저 접속을 감지해서 캡챠나 빈 페이지를 돌려주는 경우가 있습니다.

확인하는 방법은 간단합니다. 콘솔 로그와 페이지 본문을 그대로 출력해보는 겁니다.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.on("console", lambda msg: print("CONSOLE:", msg.text))
    page.on("pageerror", lambda exc: print("PAGEERROR:", exc))
    page.goto("https://example.com", wait_until="networkidle")
    page.screenshot(path="debug.png")
    print(page.content()[:300])
    browser.close()

로컬에서 돌리면 <div class="product-list">... 같은 정상 HTML이 나오는데, 서버에서 똑같은 코드를 돌리면 Access Denied 문구나 빈 <body></body>만 찍히는 경우가 실제로 자주 발생합니다. 이러면 코드 수정이 아니라 접속 방식(IP, User-Agent, 요청 간격) 조정이 필요한 상황입니다.

데이터센터 서버 랙과 파란 조명

원인을 좁히는 순서를 이 체크리스트대로 밟아 보세요

증상이 비슷해도 원인은 다를 수 있어서, 아래 표 순서대로 하나씩 제거해가는 편이 시간을 아낍니다.

확인 순서 점검 내용 확인 방법
1 브라우저 바이너리 실행 가능 여부 npx playwright install --with-deps 재실행 후 로그 확인
2 메모리/CPU 제한으로 인한 강제 종료 dmesg | grep -i oom 또는 컨테이너 리소스 로그 확인
3 페이지 응답 자체가 다른지 page.content()를 로컬·서버 양쪽에서 출력해 diff
4 IP 차단/캡챠 여부 서버 IP로 브라우저에서 직접 접속해 동일 증상 재현
5 타임존·로케일 차이로 날짜 파싱 오류 서버 TZ 환경변수와 로케일 패키지 설치 여부 확인

이 중 1~2번은 의존성·리소스 문제라서 설치 명령이나 인스턴스 스펙을 올리면 해결되지만, 4번(IP 차단)은 코드를 아무리 고쳐도 안 풀리는 경우가 있습니다. 이때는 요청 빈도를 낮추거나 프록시를 거치는 방식으로 바꿔야 하는데, 이 접근은 응답 속도가 느려지고 비용이 추가로 든다는 트레이드오프가 있습니다.

결국 콘솔 로그와 스크린샷 한 장이 원인을 가장 빨리 말해줍니다

로컬에선 되는데 서버에선 안 되는 상황을 만나면, 코드를 다시 읽기 전에 서버에서 찍은 스크린샷과 page.content() 출력을 먼저 확보하는 편이 훨씬 빠릅니다. 빈 화면이면 의존성이나 리소스 문제, 캡챠나 차단 페이지가 보이면 네트워크·탐지 문제, 레이아웃은 있는데 셀렉터가 안 맞으면 로케일·폰트 문제로 갈라서 접근할 수 있습니다.

이 세 갈래만 구분해도 디버깅 시간이 크게 줄어듭니다. 다음 크롤러를 서버에 올릴 때는 배포 직후 반드시 위 표의 1~3번 항목부터 서버 콘솔에서 직접 돌려보시길 권합니다.

Prometheus 카디널리티가 터질 때, 레이블은 이렇게 골라야 합니다

Leave a Comment