sgkstudio.
엔지니어링

쿠팡 크롤링 차단, 15분 만에 선 벽 — 고정 4.5초 간격이 자초했다

쿠팡 크롤링 차단은 IP 전체가 아니라 카테고리·상품 경로에만 걸렸고, 원인은 사이트가 아니라 요청 속도를 조절하지 않은 저희 스캐너였습니다. 목록 30페이지와 상세 24건, 리뷰 API 24회를 돈 뒤 한 번에 몰아 읽은 뒤 `Access Denied`가 떴고, 속도 조절 모듈을 넣은 뒤 두 번째 차단에서는 60→120→240초를 기다리다 스스로 멈췄습니다.

2026-10-03쿠팡 로켓배송 소싱 스캐너 운영 기록(9월 14일) — 목록 30페이지·상세 24건·리뷰 API 24회 순회 뒤 18시 28분부터 카테고리·상품 경로 Akamai `Access Denied`, 홈 3,972자 정상, 카드 1,308장·후보 24행(목표 30행)같은 날 저녁 속도 조절 모듈 도입과 두 번째 차단 재현 기록 — 35분 대기 후 카드 722장, 상세 6/22에서 차단, 60→120→240초 백오프 후 `BlockDetected`로 중단, 최종 후보 46행스캐너 소스(`scan.py`)의 속도 조절 설정값 — 최소 간격 3.0초·지터 1.5초·5회마다 45초 휴지·예산 40같은 스캐너의 첫 관측 기록 — CDP 방식 통과, 목록 페이지당 카드 60장, 리뷰 API는 상품 상세 페이지 안에서만 200

쿠팡 크롤링 차단은 어떤 모습으로 나타났나

쿠팡 상품 목록을 자동으로 읽어 오는 도구를 돌리다 보면 어느 순간 홈 화면은 열리는데 카테고리와 상품 페이지만 `Access Denied`로 막히는 때가 옵니다. 저희는 9월 14일 오후에 이 벽을 만났고, 원인은 사이트가 아니라 저희 도구의 요청 속도였습니다. 페이지 사이 간격을 4.5초로 고정해 둔 채 한 번에 너무 많이 읽은 점이 화근이었고, 이 글은 그 차단을 재고, 가설을 세우고, 틀린 가설을 버리고, 속도 조절 모듈을 만들어 다시 잰 기록입니다.

먼저 어떤 일을 하던 중이었는지부터 적습니다. 저희는 쿠팡 카테고리 목록에서 로켓배송 판매자에게 제안할 만한 상품 후보를 고르는 소싱 스캐너를 만들고 있었습니다. 후보 조건은 리뷰 100~300개, 가격 9,900~15,900원, 광고 상품 제외입니다. 목록 한 페이지에는 상품 카드가 60장 나오고, 후보로 남는 카드는 그중 극히 일부입니다.

상품 목록을 읽을 때는 WebFetch나 curl 같은 단순 요청이 403으로 막혀서, 사람이 쓰는 Chrome과 같은 브라우저를 원격으로 조종하는 방식(CDP)을 썼습니다. 이 방식은 첫 관측에서 검색·카테고리·상품 상세를 전부 통과했습니다. 그래서 처음에는 문제가 해결됐다고 믿었습니다.

이 글에서 말하는 «차단»은 아래 세 가지가 동시에 나타나는 상태입니다.

  • 카테고리 경로(`/np/categories/*`)와 상품 경로(`/vp/products/*`)가 모두 Akamai(쿠팡 앞단의 봇 방어 서비스)의 `Access Denied` 화면을 냅니다
  • 홈(`www.coupang.com/`)은 평소처럼 열리고 본문이 3,972자 돌아옵니다
  • 같은 컴퓨터, 같은 Chrome, 로그인 없는 상태인데 몇 분 전에는 통과하던 경로입니다

셋째 항목이 중요합니다. 코드도 장비도 그대로인데 결과만 달라졌다면, 도구가 틀렸다기보다 도구가 낸 요청의 양이나 모양이 임계를 넘었다는 뜻이기 때문입니다. 이 글의 나머지는 그 임계가 어디쯤이었는지 재 본 과정입니다.

처음 잰 숫자: 약 55페이지와 API 24회에서 멈췄다

차단 직전까지 한 일은 이렇습니다. 같은 automation Chrome으로 카테고리 목록 30페이지(6개 카테고리 × 5페이지)와 상품 상세 24건, 그리고 상품 상세 안에서 부르는 리뷰 API 24회를 오후 3시대부터 6시 20분대까지 두 번에 나눠 돌렸습니다. 오후 6시 28분부터 카테고리와 상품 경로가 전부 막혔습니다.

첫 번째 순회에서 잰 값 표 — 목록 30페이지(6카테고리×5), 상품 상세 24건, 리뷰 API 24회, 수집한 카드 1,308장, 후보 24행(목표 30행 미달), 차단 후에도 홈은 3,972자 정상
첫 순회는 약 55페이지와 API 24회까지 갔고, 후보는 목표 30행에 6행 모자랐다

제목의 «약 55페이지»는 목록 30페이지와 상세 24건을 더한 값에 가깝습니다. 정확한 한 줄로 환산한 값이 아니라 두 번에 나눠 돈 요청을 대략 합친 수라서, 여기서는 «임계가 대략 이쯤이었다»는 눈금으로만 씁니다. 원문 기록은 이 벽을 «약 55페이지, API 24회, 15분»이라고 요약합니다. 다만 실제 순회는 오후 3시대부터 6시 20분대까지 두 번에 나눠 돌았으므로, 15분은 몰아 읽은 구간의 길이로 읽어야 하고 정확한 환산은 확인하지 못했습니다. 같은 장비, 같은 Chrome에서 몇 십 페이지 안팎에 벽이 섰다는 점이 이 숫자의 핵심입니다.

그 시점에 스캐너는 카드 1,308장을 모았고 후보 24행을 냈습니다. 목표는 30행이었으므로 6행이 모자랐습니다. 후보로 남는 비율이 1.8%라서 30행을 채우려면 카테고리가 8~9개 필요하다는 계산도 이때 나왔습니다. 속도를 낮춰 천천히 가면 이 계산대로 여러 날에 걸쳐 채울 수 있습니다. 반대로 지금 속도를 유지하면 하루에 몇 번이고 같은 벽에 부딪힙니다.

측정 방법도 밝혀 둡니다. 차단 시각은 도구가 페이지 제목에서 `Access Denied`를 읽은 시각이고, 해제 시각은 별도 대기 스크립트의 로그에 남깁니다. 요청 수는 스캐너 로그에서 센 값이라 실제로 Akamai가 센 값과 다를 수 있습니다. 즉 이 글의 임계는 «우리가 센 요청 수»이지 «상대가 정한 한도»가 아닙니다.

처음 세운 가설은 무엇이었나

차단을 만난 직후 가설은 세 가지였습니다. 이 셋을 지우지 않고 그대로 남깁니다. 어느 가설이 맞고 어느 가설이 틀렸는지가 이 글의 본문이기 때문입니다.

  • 가설 1. IP 주소가 통째로 막혔다 — 집 PC의 공인 IP가 쿠팡 쪽 차단 목록에 올랐다
  • 가설 2. 첫 관측의 «통과했다»는 우연이었고, 처음부터 이 방식은 대량 순회에 쓸 수 없었다
  • 가설 3. 페이지 사이 간격 4.5초가 짧아서 생긴 문제이니, 간격만 늘리면 된다

가설 3이 가장 그럴듯했고, 실제로 첫 번째 대응도 여기서 나왔습니다. 당시 정리한 다음 실행 규칙은 이렇습니다. 4.5초로는 부족하니 카테고리 5페이지마다 60초를 쉬고, 한 번 실행에 읽을 페이지는 40페이지까지만 두고, 나머지는 다음 날로 넘깁니다. 상세 페이지로 이동하지 않아도 되도록 리뷰 API를 카테고리 페이지 안에서 바로 부르는 길도 찾아보기로 했습니다. 리뷰 API는 상품 상세 페이지 안에서 부를 때만 200이 돌아오고 카테고리 페이지에서 부르면 403이라서, 이 길을 풀면 상세 이동 24회가 통째로 사라집니다.

여기까지는 «간격을 늘린다»는 처방이라 틀릴 일이 없어 보였습니다. 그런데 이 처방에는 빠진 점이 하나 있었습니다. 처방이 문장으로만 남아 있었고, 스캐너 코드는 그 문장을 읽지 않는다는 점입니다.

가설은 어디서 틀렸나

가설 1은 첫 확인에서 바로 무너졌습니다. 홈 화면은 정상으로 열렸고 본문이 3,972자 돌아왔습니다. IP 전체를 막았다면 홈도 같은 `Access Denied`를 냈어야 합니다. 막힌 곳은 카테고리와 상품 경로뿐이었으므로 경로 단위 차단이라고 결론을 내렸습니다. 같은 컴퓨터로 다른 경로를 계속 쓸 수 있다는 뜻이기도 합니다.

가설 2도 반증됐습니다. 첫 관측이 틀렸다면 처음 30페이지와 상세 24건이 통과하지 못했을 텐데, 실제로는 1,308장을 읽었습니다. 방식이 아니라 한 번에 읽은 양이 문제였습니다. 첫 관측의 «통과했다»는 한 번의 관측이었고, 한 번의 성공은 계속 통과한다는 증거가 아닙니다. 이 점을 정정 기록으로 남겼습니다.

가장 아팠던 것은 가설 3이 절반만 맞았다는 점입니다. 간격이 짧았던 점은 사실이었지만, 더 큰 문제는 같은 문제를 이미 알고 있었는데 스캐너가 여전히 똑같이 4.5초 고정 간격만 쓰고 있었다는 점입니다. 작업 중 이런 지적이 나왔습니다.

예측 불가능한 문제는 아니지 않아?

맞는 말이었습니다. 첫 관측에서 «통과했다»고 적은 시점에 이미 요청이 쌓이면 막힐 수 있다는 점은 예측할 수 있었습니다. 그런데 그 예측을 읽은 사람의 머릿속에만 두었고, 스캐너 코드에는 아무 변화도 없었습니다. 속도를 늦추라는 메모는 다음 스크립트의 요청 간격을 바꾸지 못합니다.

진짜 원인은 무엇이었나

진짜 원인은 둘이 겹친 것이었습니다. 첫째는 요청이 짧은 시간에 몰렸다는 점이고, 둘째는 몰리지 않게 막는 장치가 코드에 없었다는 점입니다. 앞쪽은 상대 사이트의 사정이라 우리가 고칠 수 없습니다. 뒤쪽은 우리 코드의 빈칸이라 고칠 수 있습니다.

그래서 원인을 이렇게 정리했습니다. 간격을 4.5초로 고정한 코드는 요청이 몇 건 쌓였는지 모릅니다. 막혔는지도 모릅니다. 막힌 뒤에도 같은 간격으로 계속 두드리고, 두드릴수록 상대의 경계가 올라갈 수 있습니다. 사람이라면 «여기까지 하고 쉬자»를 하지만, 코드는 끝까지 갑니다.

그렇다면 해야 할 일은 «조심하라»는 문장을 한 줄 더 적는 일이 아닙니다. 매 요청이 반드시 거치는 관문에 규칙을 넣는 일입니다. 읽은 지식은 행동을 막지 못하고, 코드에 들어간 규칙은 막습니다. 이 구분이 이 글에서 가장 값진 결론이라고 생각합니다.

어떻게 고쳤나: 요청마다 거치는 속도 조절 모듈

같은 날 저녁에 속도 조절 모듈(`bot-crawl-pace`)을 새로 만들고, 스캐너가 페이지를 이동하는 함수 `goto()`에 강제로 연결했습니다. 모듈의 핵심은 `Pacer`라는 클래스이고, 네 가지 일을 합니다.

  • 최소 간격과 지터: 요청 사이에 최소 간격을 두고, 간격에 무작위 흔들림을 더합니다
  • 배치 휴지: 일정 횟수마다 길게 쉽니다
  • 요청 예산: 한 번 실행에서 보낼 수 있는 요청 수에 상한을 둡니다
  • 차단 신호 감지: 차단 신호가 연속으로 오면 지수 백오프로 기다린 뒤, 그래도 안 풀리면 `BlockDetected` 예외를 내고 멈춥니다
속도 조절 설정 전후 비교 — 전: 페이지 사이 4.5초 고정, 차단 감지 없음, 예산 없음. 후: 최소 간격 3.0초에 지터 1.5초, 5회마다 45초 휴지, 요청 예산 40, 차단 신호 연속 감지 시 백오프 후 BlockDetected
고정 4.5초 한 줄이 네 가지 규칙으로 바뀌었다

실제 설정값은 최소 간격 3.0초, 지터 1.5초, 5회마다 45초 휴지, 요청 예산 40입니다. 눈여겨볼 점이 둘 있습니다. 처음 적은 규칙은 «5페이지 뒤 60초 휴지»였는데 구현은 45초로 들어갔습니다. 메모와 코드가 어긋났고, 어느 쪽이 더 나은지는 아직 재 보지 않았습니다. 또 예산 40은 앞서 적은 «한 세션 40페이지 상한»과 같은 숫자인데, 이쪽은 메모대로 들어갔습니다.

테스트는 적대적으로 5종을 짰습니다. 정상 흐름, 예산 소진, 연속 차단, 산발적 차단은 발동하지 않음, 그리고 `Access`라는 단어가 단독으로 나와도 오탐하지 않음입니다. 마지막 항목이 필요한 이유는 단어 하나만 보고 차단으로 오인하면 멀쩡한 순회가 멈추기 때문입니다. 5종이 모두 통과했고, 실제로 차단이 걸린 상태에서도 돌려 봤습니다. 차단을 감지하고 60초를 기다린 뒤 0행으로 정상 종료했으며 크래시는 없었습니다.

간격에 무작위 흔들림(지터)을 더한 이유도 적어 둡니다. 정확히 같은 간격으로 요청이 오면 사람이 읽는 흐름과 모양이 다르고, 방어 장비 입장에서는 기계라는 가장 쉬운 신호가 됩니다. 이 설명은 일반 지식이고 쿠팡에서 직접 확인한 값은 아닙니다. 그래서 지터 1.5초가 실제로 차단을 늦추는지는 따로 재지 못했고, 지금은 «해가 되지 않는 안전장치»로만 쓰고 있습니다.

스캐너는 `BlockDetected`와 예산 소진 예외를 받으면 그때까지 모은 행만 쓰고 멈춥니다. 계속 두드리지 않는 태도 자체가 다음 실행을 지키는 보호가 됩니다. 이 설계는 다음 대량 순회 스크립트도 같은 모듈을 쓰도록 의도했습니다. 비슷한 자동화를 업무 자동화 효과를 재는 법에서도 다뤘는데, 거기서도 «규칙을 문장으로 두지 말고 실행 경로에 넣어라»가 결론이었습니다.

재측정하니 두 번째 차단은 어떻게 멈췄나

고친 것이 정말 듣는지는 같은 날 저녁 바로 확인할 기회가 왔습니다. 첫 차단이 풀리기까지 35분을 기다린 뒤, 같은 계열의 카테고리 3개를 마저 돌렸습니다. 목록으로 카드 722장을 읽고 상세 조회로 넘어갔는데, 상세 6건째(6/22)에서 다시 차단 신호가 왔습니다.

이번에는 설계대로 움직였습니다. 60초, 120초, 240초로 두 배씩 늘려 가며 기다렸고, 그래도 풀리지 않자 `BlockDetected`를 내고 스스로 멈췄습니다. 전처럼 크래시하지도, 막힌 채 계속 두드리지도 않았습니다. 최종 후보는 46행으로 목표 30행을 넘겼습니다.

차단 후 대기 시간 증가 선 그래프 — 1차 대기 60초, 2차 120초, 3차 240초로 두 배씩 늘어난 뒤 BlockDetected로 중단
60→120→240초, 두 배씩 기다리다 스스로 멈췄다

재측정 결과를 전후로 놓고 보면 달라진 점은 분명합니다.

고치기 전과 후 — 같은 날 저녁에 잰 값
항목고치기 전(첫 번째 파도)고친 후(두 번째 파도)
페이지 사이 간격4.5초 고정최소 3.0초 + 지터 1.5초, 5회마다 45초 휴지
차단 시 행동끝까지 진행(스캐너 v0)60→120→240초 백오프 후 `BlockDetected`로 중단
차단까지 보낸 상세 조회24건6건
최종 후보24행(목표 30행 미달)46행(목표 상회)
차단 뒤 결과후보 24행에서 멈춤크래시 없이 종료

첫 번째 파도는 목록 30페이지와 상세 24건을 돈 결과이고, 두 번째 파도는 목록 3개 카테고리(카드 722장)를 읽은 뒤 상세 6건째에서 차단 신호가 온 결과입니다.

표에서 눈에 띄는 칸은 «차단까지 보낸 상세 조회» 줄입니다. 첫 번째는 24건에서, 두 번째는 6건에서 차단이 왔습니다. 절대 수치만 보면 속도 조절을 넣은 뒤에 오히려 더 일찍 차단이 온 셈입니다. 이 점을 숨기지 않고 적습니다. 속도 조절 모듈은 차단을 «막는» 장치가 아니라 «걸렸을 때 안전하게 멈추는» 장치라는 점을 이 숫자가 보여 줍니다.

두 파도를 시간 순서로 다시 놓으면 이렇습니다. 첫 번째 파도는 오후 3시대부터 6시 20분대까지 두 번에 나눠 돌렸고 18시 28분에 막혔습니다. 두 번째 파도는 속도 조절 모듈을 넣고 35분을 기다린 뒤 같은 날 저녁에 돌렸습니다. 두 파도 사이에 쉰 시간이 35분이라는 점이 중요합니다. 하루를 쉬지 않고 같은 날 다시 들어갔는데도 목록 3개 카테고리의 카드 722장은 읽혔고, 막힌 곳은 상품 상세 단계였습니다. 목록 경로보다 상품 상세 경로가 먼저 한도에 닿는 듯하지만, 이 역시 두 번의 관측만으로는 단정하지 못합니다.

차단이 온 상세 조회 건수 막대그래프 — 첫 번째 파도 24건, 두 번째 파도 6건
같은 날 두 번째 차단은 첫 번째보다 훨씬 적은 요청에서 왔다

고치다가 따로 발견한 버그는 무엇이었나

차단에서 중간에 멈추게 만들자 새 문제가 드러났습니다. `BlockDetected`로 상세 조회 반복문이 중간에 끊기면, 아직 못 돈 나머지 후보 행에는 `review_total` 같은 키 자체가 없었습니다. 요약 문서를 만드는 단계가 그 키를 읽다가 `KeyError`로 죽었습니다.

CSV 파일은 기본값 처리(`restval`) 덕에 죽지 않고 빈 칸으로 살아남았고, 합본 보고서도 정상이었습니다. 요약 문서만 죽었습니다. 이런 식으로 한 곳은 죽고 한 곳은 살아남는 상태가 가장 발견하기 어렵습니다. 전체가 죽으면 바로 보이지만 일부만 죽으면 «돌아가는 듯한데 결과가 이상하다»로 넘어가기 때문입니다.

고친 방법은 단순합니다. 기본값 `❓`을 반복문에 들어가기 전에 후보 전체에 `setdefault`로 미리 채우고, 요약 문서를 만드는 곳도 `.get()`으로 읽게 바꿨습니다. 반복문이 중간에 끊기는 상황을 흉내 낸 회귀 테스트로 확인했고, 예외는 나오지 않았습니다. 멈추는 기능을 넣으면 멈춘 뒤의 상태도 같이 검사해야 한다는 점을 이번에 배웠습니다.

아직 모르는 것은 무엇인가

한계는 분명하게 적어 둡니다. 이 글의 결론 가운데 확인된 사실과 아직 가설인 내용을 나눕니다.

  • 확인된 사실: 막힌 것은 IP 전체가 아니라 카테고리와 상품 경로이고, 홈은 열립니다
  • 확인된 사실: 고정 간격으로만 도는 코드는 차단을 만나면 계속 두드리거나 죽습니다. 속도 조절 모듈은 두 번째 파도에서 설계대로 스스로 멈췄습니다
  • 미검증 가설: Akamai의 임계는 하루 누적 요청량에 반응하는 듯합니다. 두 번째 차단이 첫 번째보다 훨씬 적은 요청(상세 6건)에서 왔다는 관측이 근거의 전부이고, 요청 수를 바꿔 가며 일부러 재 본 적은 없습니다
  • 아직 안 잰 값: 45초 휴지와 60초 휴지 중 어느 쪽이 더 오래 버티는지, 그리고 예산 40이 적당한 값인지는 한 번씩만 실행해 본 수준입니다

그래서 지금의 운영 규칙은 보수적입니다. 같은 날 재시도보다 다음 날을 먼저 택합니다. 임계가 하루 누적량에 반응한다는 가설이 맞다면 같은 날 다시 시도하면 손해이고, 틀리더라도 하루를 기다리는 비용은 작습니다. 확인되지 않은 가설을 규칙으로 쓰는 이유는 틀렸을 때 잃는 시간이 하루뿐이라서입니다.

리뷰 API를 카테고리 페이지 안에서 바로 부르는 길도 풀지 못했습니다. 이 길이 풀리면 상세 이동 24회가 사라져서 차단 위험이 크게 줄어듭니다. Referer와 쿠키가 403의 원인이라는 추정만 있고 확인하지 못했습니다.

크롤링 자동화를 맡기려면 무엇부터 물어봐야 하나

이 기록은 한 가지 작업의 이야기지만, 외부 사이트를 자동으로 읽는 일을 외주로 맡길 때 확인할 항목은 대부분 겹칩니다. 개발 업체에 견적을 받을 때 아래 네 가지를 물어보시기 바랍니다.

  • 요청 속도를 코드가 강제로 조절하나, 아니면 «천천히 돌리겠다»는 약속뿐인가
  • 차단이 걸리면 어떻게 하나 — 계속 두드리는지, 기다리는지, 멈추는지, 멈춘 뒤 결과 파일이 깨지지 않는지
  • 한 번 성공한 관측을 근거로 «문제없다»고 말하는가, 시간대와 요청량을 달리해 다시 재 봤는가
  • 한 번 실행에 읽을 양의 상한이 있고, 나머지는 다음 날로 넘기는 구조인가

저희는 홈페이지 제작, AI 챗봇, 업무 자동화를 만드는 작은 스튜디오라서 이런 자동화의 설계와 운영 기록을 같이 보고 판단합니다. 비슷한 수집 자동화를 검토하고 계신다면 어느 사이트를 어떤 주기로 읽고 싶은지만 알려 주셔도 위 네 가지 기준으로 먼저 점검해 드릴 수 있습니다. 상담 요청에서 남겨 주시면 됩니다.

같은 방식으로 우리 업무를 재보고 싶다면

이 글에 쓴 계측·판정·자동화는 저희가 실제로 매일 돌리는 방식 그대로입니다. 어떤 작업을 자동화할 수 있고 어디를 사람이 잡아야 하는지, 현재 업무 흐름을 놓고 같이 짚어 드립니다.

새 글이 올라오면 알려 드립니다

이런 계측 기록을 새로 쓰면 메일로 한 통 보냅니다. 광고나 판촉은 보내지 않습니다.