Katana는 ProjectDiscovery의 엔드포인트 탐색 크롤러입니다. Go로 작성된 MIT 라이선스 바이너리로, 대상 하나를 넣으면 다음 도구에 넘길 URL과 엔드포인트를 찾아줍니다. 브라우저 없이 돌아가는 HTTP 모드로도, -headless로 Chromium을 띄우는 모드로도 크롤링할 수 있습니다. 공식 가이드는 headless 모드를 더 높은 커버리지 옵션으로 소개하지만, 이 테스트용 환경은 엔드포인트의 종류가 단순한 개수만큼이나 중요하다는 점을 보여줍니다.
작게 만든 사이트에 의도적으로 서로 다른 세 가지 엔드포인트 유형을 넣고, v1.6.1에서 -d 4로 각각 어떤 모드가 무엇을 찾아내는지 측정했습니다. 일반 HTML은 네 가지 구성 모두에서 4/4 링크와 3단계 전체 체인을 모두 찾아냈습니다. 차이는 JavaScript 소스에 노출된 엔드포인트와 런타임 DOM 변경으로만 드러나는 엔드포인트에서 나타났습니다.
이 테스트에서는 headless가 실행 시 DOM에 생성되는 유형을 찾아낸 반면, 표준 모드의 -jc는 두 headless 실행 모두가 놓친 JavaScript 파일 안의 리터럴을 찾아냈습니다. 네 가지 명령 조합 중 어느 것도 이 두 유형을 동시에 모두 커버하지 못했습니다. 범위(scope), 재개(resume), known-files 동작은 그 밖의 실무적 경계를 보여줬습니다.
Katana가 실제로 무엇인지
Katana 크롤러 — GitHub의 projectdiscovery/katana — 는 Go로 작성됐고 MIT 라이선스를 따릅니다. 저는 v1.6.1을 2026년 7월 27일에 테스트했습니다. 아래에 나오는 커버리지와 known-files 결과는 빌드에 따라 달라질 수 있으므로, 버전이 중요합니다.
여기서는 카테고리 구분이 특히 중요합니다. 엔드포인트 탐색 크롤러는 필드 추출기가 아닙니다. 제품명과 가격을 구조화된 JSON으로 뽑아오는 go web crawler를 원한다면, Katana는 완전히 다른 영역입니다. Katana는 /products/1138 같은 경로가 존재한다는 사실은 알려주지만, 그 페이지 안에 무엇이 있는지는 전혀 알려주지 않습니다. 이건 의도된 설계입니다. 추출 기능이 아닌 탐색 기능을 기준으로 평가한다면, 금속 탐지기를 보석 감정 능력으로 평하는 것과 다르지 않습니다.
Katana의 주된 용도는 공격적 보안 리콘과 자동화 파이프라인입니다. STDIN으로 입력을 받고 URL을 내보내 다음 도구에 파이프로 연결하는 방식이죠. 그래서 분명한 주의사항을 먼저 말씀드려야 합니다. 여기서의 모든 측정은 제가 직접 작성한 127.0.0.1의 테스트용 환경에서 수행했습니다. Katana는 반드시 본인이 소유했거나 명시적으로 테스트 권한을 가진 대상에만 사용해야 하며, 그 외에는 안 됩니다. 이 글은 방어를 우회하는 이야기가 아니라, 주어진 명령이 사이트의 엔드포인트 표면을 얼마나 실제로 열거해 주는지에 대한 이야기입니다.
세 가지 모드, 그리고 각각이 볼 수 있는 것
표준 모드는 Go HTTP 클라이언트입니다. HTML을 가져와 파싱하고 href를 따라가지만, 브라우저는 전혀 띄우지 않습니다. 빠르고 가볍지만, JavaScript가 실행된 뒤에만 생기는 요소는 보지 못합니다.
-jc(-js-crawl)는 이 브라우저 없는 경로 위에 JavaScript 파서를 얹습니다. 연결된 .js 파일을 내려받아 소스 안의 URL 형태 문자열 리터럴을 찾아냅니다. 실행은 하지 않고 읽기만 합니다. README에는 더 무겁고 메모리를 많이 쓰는 파서인 -jsl(jsluice)도 설명되어 있지만, 저는 테스트하지 않았으므로 커버리지에 어떤 차이를 만드는지는 말할 수 없습니다.

-headless는 Chromium을 구동해 페이지 스크립트를 실행합니다. 이 테스트 환경에서는 조각 문자열을 합쳐 런타임 DOM에 삽입한 경로를 되찾은 유일한 Katana 모드였습니다. 다만 이 결과가 모든 파서나 미래의 Katana 모드가 같은 것을 찾는다는 뜻은 아닙니다.
그다음은 scope 모델입니다. 실제 환경에 넣기 전에 먼저 몸에 익혀둘 부분은 바로 이것입니다.
| 플래그 | 제어하는 것 | 값 / 기본값 |
|---|---|---|
-fs (field scope) | 어떤 호스트를 대상으로 할지 | dn, rdn, fqdn 또는 사용자 정의 정규식 — 기본값은 rdn |
-cs 및 -cos | 해당 field scope 내부에서 URL을 걸러내는 정규식 | — |
-kf | known files: robots.txt와 sitemap.xml | README에 따르면 최소 depth 3이 필요 |
-d | depth | 기본값 3 |
-resume | 중단된 크롤링을 이어서 실행 | — |
순서는 장식이 아닙니다. 호스트 정규식이 크롤 범위를 넓히는지, 아니면 조용히 비워버리는지를 결정합니다.
설치: 세 가지 방법, 하나의 예외
들어가는 방법은 세 가지이고, 도구체인이 필요한 것은 딱 하나입니다.
| 설치 경로 | 필요 조건 |
|---|---|
소스에서 설치: go install github.com/projectdiscovery/katana/cmd/katana@latest | 명시된 요구사항은 Go 1.25 이상 |
| 릴리스 페이지의 사전 빌드 바이너리 | 도구체인 불필요 |
| Docker 이미지 | 도구체인 불필요 |
제 환경에서는 ~/go/bin/katana에 설치됐고, 실행할 때마다 Current version: v1.6.1이 표시됐습니다. 여기까지는 흔히 기대하는 깔끔한 Go식 이야기입니다. 파일 하나, 런타임 의존성 없음.
예외는 headless입니다. 여기서는 바이너리 외에 브라우저가 별도 전제 조건입니다.
-headless 실행 위치 | 필요한 것 |
|---|---|
| 내 머신 | Katana가 이미 설치된 Chromium을 자동 감지했고, 브라우저 빌드나 경로는 따로 기록하지 않았음 |
| 프로젝트의 Ubuntu 안내에 따른 기본 서버 | headless가 동작하기 전에 apt install google-chrome-stable 필요 |
| Docker 경로 | -system-chrome로 headless 실행 |
빈 서버에서는 이 편의가 사라집니다. -headless를 명령줄에 넣는 순간, 바이너리만이 아니라 브라우저도 예산에 포함해야 합니다.
CI에서 돌릴 경우 알아둘 사소하지만 중요한 점 하나가 있습니다. Katana는 시작할 때 GitHub로 버전 확인 요청을 보냅니다. -duc는 이를 막아줍니다. 노트북에서는 별일 아니지만, 네트워크가 분리된 환경이나 rate limit이 있는 러너에서는 매 실행마다 원치 않는 왕복 요청이 됩니다. 제 시간 측정은 모두 -duc를 넣어, 숫자가 전화홈이 아니라 크롤링 자체를 반영하도록 했습니다.
어떻게 테스트했나
세 종류의 엔드포인트를 골랐고, 의도적으로 모드별 차이가 드러나게 만들었습니다. 모든 것은 로컬 테스트 서버에 있고, 크롤을 시작하기 전에 정답 목록을 먼저 적어 뒀습니다. 즉, recall은 Katana가 출력한 것과 무관하게 고정된 기준 집합을 대상으로 계산했습니다.
- A 유형 — 일반 HTML.
/page/a,/page/b,/page/c, 그리고 세 단계 체인/depth/1 → /depth/2 → /depth/3. 어떤 크롤러든 잡아야 하는 것들입니다. - B 유형 — JavaScript 파일 리터럴.
/api/js-endpoint-7과/api/js-endpoint-8은 연결된/static/app.js안의 문자열 리터럴로만 존재합니다. JS를 읽기만 한다면 브라우저 없이도 볼 수 있습니다. - C 유형 — 런타임 DOM 전용. 조각 문자열 (
'endpoint' + (6 * 7))을 런타임에 합쳐 스크립트가 DOM에 주입하는 경로입니다./runtime-only/endpoint42라는 문자열은 서버가 보내는 어떤 바이트에도 연속해서 나타나지 않습니다. HTML에도 없고, JS 소스에도 없습니다. 실행해야만 드러납니다.
여기에 robots.txt, 다른 곳에는 전혀 없는 <loc> 엔드포인트 두 개를 담은 sitemap.xml, 500을 반환하는 경로, 끊어진 링크, 그리고 다른 호스트명에 있는 두 번째 서버를 가리키는 범위 밖 링크도 추가했습니다.
도구도 중요합니다. 이 서버는 실제로 무엇이 요청됐는지를 카운트하므로, scope와 resume에 대한 주장은 Katana의 stdout이 아니라 실제 요청 기록에 기반합니다. 원하시면 원시 실행 결과는 benchmark repo에 있습니다.
아무도 수치화하지 않는 커버리지 차이

-d 4에서 모드별 엔드포인트 유형 매트릭스는 다음과 같습니다.
| 모드 | HTML 링크 (A) | depth 체인 (A) | JS 파일 리터럴 (B) | 런타임 DOM (C) |
|---|---|---|---|---|
| standard | 4/4 | 3/3 | 0/2 | 찾지 못함 |
standard -jc | 4/4 | 3/3 | 2/2 | 찾지 못함 |
-headless | 4/4 | 3/3 | 0/2 | 찾음 |
-headless -jc | 4/4 | 3/3 | 0/2 | 찾음 |
마지막 두 열을 함께 보면 문제가 바로 드러납니다. B 유형은 오직 한 구성, 즉 standard 모드의 -jc에서만 찾았습니다. C 유형은 둘뿐입니다. 두 headless 실행 모두에서 찾았습니다. 두 열을 동시에 통과한 행은 없습니다. 전체 매트릭스는 discovery-summary.json에 있으며, 계산된 필드 headless_jc_covers_both 값은 false입니다.
실무적으로는 “커버리지를 높이려면 그냥 headless를 쓰면 된다”는 말이 이 테스트에서는 불완전했습니다. headless는 standard 결과 위에 B 유형을 더해주지 않았고, 대신 B를 놓친 채 C를 되찾았습니다. 이 테스트 환경에서 심은 모든 유형을 커버하려면 두 번 크롤하고 합쳐야 했습니다.
katana -u https://target.example -jc -d 4 -silent -o pass-jc.txt
katana -u https://target.example -headless -d 4 -silent -o pass-headless.txt
sort -u pass-jc.txt pass-headless.txt > endpoints.txt
-headless -jc 조합은 제가 가장 먼저 upstream에 설명을 듣고 싶은 부분입니다. headless 실행에 JavaScript 파서를 더해도 아무것도 추가되지 않았습니다. B 유형은 계속 0/2였습니다. 새로 재현해도 같은 결과였습니다. 저는 이 동작을 보고하는 것이지 메커니즘을 완전히 추적했다고 주장하는 것은 아닙니다. 브라우저 경로가 왜 JS 파일 리터럴에 기여하지 않는지 Katana 내부를 계측해 보지는 않았습니다. 그러니 이것은 진단이 아니라 재현 가능한 관찰이자 GitHub 이슈로 보기 좋습니다. (덧붙이면, v1.6.1의 macOS ARM에서 -hl -jc 조합은 return code 0으로 깔끔하게 끝났습니다. 예전에는 항상 그렇지 않았습니다.)
공식 문서는 headless가 더 좋은 커버리지를 제공한다고 설명합니다. 그리고 이 테스트에서는 런타임 렌더링 클래스에 대해서 실제로 그랬습니다. 하지만 문서에는 이처럼 소스 리터럴과 런타임 DOM을 가르는 차이는 명시되어 있지 않았습니다. 따라서 이 매트릭스는 여러분의 대상 유형에 대해 두 경로를 모두 시험해 보라는 뜻이지, 보편적인 분류 체계를 제시하는 것은 아닙니다.
headless가 걸리는 시간 비용
유휴 상태의 머신에서 모드별로 순차 실행을 세 번씩 측정했습니다.
| 모드 | p50 | 최소–최대 | 평균 |
|---|---|---|---|
| standard | 13.08초 | 13.07–13.17초 | 13.11초 |
-headless | 66.82초 | 66.78–67.68초 | 67.09초 |
이건 5.1배 차이입니다. 범위도 전혀 겹치지 않습니다. 가장 느린 standard 실행(13.17초)조차 가장 빠른 headless 실행(66.78초)보다 53초 이상 빨랐습니다(cost-summary.json). 이건 측정 오차가 아닙니다.
13초 수치에 대해 한 가지 주의할 점이 있습니다. 제 테스트 환경에는 500 경로와 끊어진 링크가 일부러 포함되어 있고, standard 모드는 둘 다 기본 -timeout 10의 재시도 꼬리를 기다립니다. 빠른 모드를 유리하게 보이도록 timeout을 조정하지 않았기 때문에, timeout을 맞춘 standard 실행이라면 격차를 줄이기보다 오히려 더 벌릴 가능성이 큽니다.
이 비율은 로컬 처리 용량 신호이지, 실제 운영 환경의 예측치는 아닙니다. 실제 대상은 지연, 실패, 스크립트 작업, 스케줄링이 모두 다르며, 이 테스트는 기본 timeout 꼬리를 포함합니다. 측정된 5.1배 차이를 기준으로 headless에 별도 예산과 대상 subset이 필요한지 판단하고, 그 계획은 대표적인 승인 대상 호스트에서 다시 벤치마크하세요.
범위는 유지됐지만, 한 플래그는 조용히 아무 일도 하지 않았다
scope 테스트에는 두 서버를 사용했습니다. 하나는 127.0.0.1의 기본 서버, 다른 하나는 다른 포트의 localhost에서만 접근 가능한 경로를 서빙하는 서버였습니다. 따라서 그 경로가 히트했다는 것은 범위 밖 호스트가 실제로 fetch됐다는 증거이지, 단순히 문자열로 출력된 것이 아닙니다.
| 구성 | 범위 밖 호스트를 fetch했나? | 두 번째 서버의 히트 수 |
|---|---|---|
기본값 (-fs rdn) | 아니오 | 0 |
-fs fqdn | 아니오 | 0 |
-cs localhost | 아니오 | 0 |
| `-fs '(127.0.0.1 | localhost)'` | 예 |
scope는 좋은 소식입니다. 기본값에서는 Katana가 제자리를 지켰고, 범위를 넓히려면 명시적인 조치가 필요했습니다. 남의 인프라를 향해 쓰는 도구라면 이게 맞는 기본값입니다.
흥미로운 행은 -cs localhost입니다. 이것은 두 번째 호스트로 크롤을 넓히지 않았고, URL도 하나도 내보내지 않았습니다. -cs는 field scope 내부를 필터링하는데, field scope는 여전히 기본 호스트였기 때문에 정규식이 아무것도 매칭하지 못했고, 크롤은 에러 대신 빈 결과를 반환했습니다. 포함시키고 싶었던 호스트를 scope 정규식에 적어 넣었는데 출력 파일이 텅 비어 있어서 한참 바라본 적이 있다면, 그게 바로 이 메커니즘입니다(scope-summary.json). 호스트를 추가하려면 -fs를 설정하세요. 이미 가진 호스트 안에서 더 좁히려면 -cs/-cos를 쓰면 됩니다.
resume는 플래그 이름이 주는 인상보다 더 거칠다
README에는 이 플래그가 -resume string resume scan using resume.cfg라고 적혀 있습니다. 보통은 작업 디렉터리에 파일 하나가 떨어지는 것처럼 읽히죠. 하지만 그렇지 않았습니다. 제 머신에서는 체크포인트가 ~/.config/katana/resume-<xid>.cfg에 저장됐습니다. 문서에 경로가 명시되어 있지 않아서, 문서가 아니라 직접 측정한 값입니다.
그 안에 무엇이 들어 있는지가 더 중요한 놀라움이었습니다. 파일에는 InFlightUrls 맵이 있었는데, 딱 하나만 들어 있었습니다. 방문 완료 집합도 아니고, frontier도 아니고, seed URL 하나뿐이었습니다. 그래서 3초 뒤 SIGINT로 크롤을 끊고 다시 resume했을 때 이렇게 됐습니다.
| 실행 | 서로 다른 경로 수 |
|---|---|
| 전체 기준선 크롤 | 11 |
| 인터럽트 전까지 fetch된 수 | 10 |
| resume 실행이 다시 fetch한 수 | 이미 끝난 10개를 포함한 전체 11개 |
resume는 같은 최종 엔드포인트 집합에 도달했으므로 기능적으로는 문제 없습니다. 하지만 체크포인트의 단위는 URL이 아니라 입력 seed별입니다. 메모리 내 dedupe 필터는 저장되지 않기 때문에, 단일 seed 크롤을 resume하면 그 seed를 처음부터 다시 크롤하게 됩니다(resume-summary.json). Katana에 500개의 호스트 목록을 넣는다면, 완전히 끝난 호스트를 resume가 절약해 줄 가능성이 있습니다. 다만 그런 멀티-seed 동작은 상태 저장 방식으로부터 추론되는 것이고, 저는 single-seed만 측정했습니다. 아주 큰 사이트 하나를 깊게 파고드는 경우라면, resume가 절약해 주는 것은 시간이 아니라 정합성입니다.
known files: 요청은 했지만, 떨어졌다

-kf all -d 3는 실제로 두 파일을 모두 요청했습니다. robots.txt와 sitemap.xml이 서버 히트 로그에 기록됐습니다. 하지만 그 sitemap의 <loc> 요소에 있던 엔드포인트 2개 중 0개만 복구했습니다. recall은 0.0입니다.
이걸 한계라고 말하기 전에 제 실수인지 확인하려고 여러 변형을 시도했습니다. 결과는 모두 같았습니다.
| 시도한 변형 | sitemap <loc> 엔드포인트 복구 수 |
|---|---|
-kf all | 0/2, recall 0.0 |
-kf sitemapxml | 0/2, recall 0.0 |
-kf robotstxt | 0/2, recall 0.0 |
| depth 3 | 0/2, recall 0.0 |
| depth 4 | 0/2, recall 0.0 |
| depth 5 | 0/2, recall 0.0 |
-jc 추가 | 0/2, recall 0.0 |
/sitemap.xml에 직접 seed | 0/2, recall 0.0 |
문서에 적힌 요구사항 — -kf를 쓰고, 적어도 세 단계 깊이로 가라 — 는 매번 만족했습니다. 플래그를 빼먹은 이야기가 아닙니다.
실무적으로 먼저 내릴 결론은 이것입니다. 이 IP-literal 테스트 환경에서는 known files를 요청했다고 해서 그 안의 <loc> URL이 자동으로 크롤에 합류한다고 가정하면 안 됩니다. recall을 확인하거나, 그 URL을 직접 추출해서 seed로 넣으세요.
v1.6.1 코드 경로는 이 관찰과 일관되지만, 실행 중에는 계측하지 않았습니다. v1.6.1의 sitemapxml.go에서는 NewNavigationRequestURLFromResponse가 RootHostname이 채워지지 않은 상태에서 <loc> 탐색 요청을 만듭니다. 그다음 요청은 ValidateScope에 도달합니다. v1.6.1의 scope.go에서는 IP-literal 분기에서 URL host와 비어 있는 root를 비교하게 되고, 그 결과 URL이 거부될 수 있습니다. 별도의 scope 테스트에서 사용한 사용자 정의 -fs '(127.0.0.1|localhost)' 명령은 다른 scope 분기를 탔기 때문에, 그것은 원천 코드로 예측 가능한 우회책이지, -kf의 실측 워크어라운드는 아닙니다. 다만 이 호스트에서는 known-files 클라이언트의 간헐적 dial 문제 때문에 확인 시도가 막혔고, 따라서 보고 결과는 여전히 0/2입니다.
실제로 운영 환경에서 제가 오늘 당장 할 일은 이렇습니다. sitemap을 직접 가져와 <loc> URL을 추출한 뒤, 그것들을 seed 목록으로 Katana에 넘깁니다. 셸 두 줄이면 되고, scope 검증은 관여하지 않습니다.
한 가지는 정확히 문서대로 동작했고, 한 문장으로 짚을 만합니다. 500 경로와 끊어진 링크는 fetch되고, 로그에 기록되고, 그냥 지나쳤습니다. 모든 browserless 실행은 return code 0으로 끝났습니다. 첫 번째 오류에서 죽는 크롤러는 무인 상태에서 쓸모가 없고, Katana는 그렇지 않습니다.
배포 전, 대상별 커버리지 점검
이 테스트용 매트릭스는 본인의 승인 대상에 대해 시험하는 템플릿으로 가장 유용합니다. Katana를 돌리기 전에 엔드포인트 유형을 먼저 정의하세요. 일반 링크, 연결된 스크립트 안의 리터럴, 실행 후에만 생성되는 경로, known-file 항목은 시작점으로 충분히 합리적인 네 가지 분류입니다. 각 유형마다 작은 정답 샘플을 유지하세요. 이런 사전 목록이 없으면, 더 큰 stdout 파일이 더 나은 커버리지처럼 보이더라도 사실 특정 유형이 사라졌을 수 있습니다.
browserless 경로와 headless 경로를 먼저 각각 따로 측정하세요. 정확한 명령, Katana 버전, 브라우저 빌드, return code, 출력 결과를 저장하세요. 줄 수를 비교하지 말고 엔드포인트 집합을 정규화해 diff를 내세요. 샘플에서 standard -jc가 독자적으로 추가하는 것이 없다면 headless 전용 정책으로 충분할 수 있습니다. 하지만 여기처럼 두 집합이 갈라진다면 두 패스를 분리해서 수집한 뒤 합치는 편이 낫습니다. 둘 다 한 명령에 넣었다고 해서 대상별 diff로 확인되기 전까지 합집합과 같다고 가정하면 안 됩니다.
Katana 출력 밖의 증거로 scope를 검증하세요. 제외돼야 하는 호스트에 canary URL을 두고 그 서버의 요청 로그를 보세요. 크롤 범위를 넓혀야 하는 경우라면 의도한 두 번째 호스트도 테스트하세요. 여기서 -cs localhost 실행은 field scope를 넓히지 못했기 때문에 출력이 비어 있었고, 사용자 정의 -fs '(127.0.0.1|localhost)' 호출은 두 번째 서버에 실제로 접속했습니다. 정규식은 한 글자만 달라도 표현만 바뀌는 것이 아니라 동작 자체가 바뀔 수 있으므로, 정확한 표현을 기록하는 것이 중요합니다.
resume와 known files는 discovery recall과 분리해서 테스트하세요. resume의 경우 대표 seed를 몇 페이지 뒤에서 중단시키고, 생성된 체크포인트 경로를 저장한 뒤, 이미 끝난 URL이 얼마나 다시 fetch되는지 세세요. -kf의 경우 robots/sitemap을 실제로 요청했는지와, 심은 <loc> URL이 정말 큐에 올라갔는지 둘 다 확인해야 합니다. 이 둘은 서로 다른 주장입니다. 이 테스트 환경에서는 파일은 fetch되었지만 두 sitemap 엔드포인트는 빠져 있었기 때문에, 요청 로그와 엔드포인트 출력 둘 다 있어야 경계를 볼 수 있었습니다.
마지막으로, 유휴 상태의 머신에서 순차 실행으로 로컬 비용 기준선을 만든 뒤, 대표 대상에서 다시 반복하세요. 배수만 보지 말고 최소, 최대, 중앙값을 함께 보관하세요. 여기서의 5.1배 수치는 이 테스트 환경의 실패와 timeout 동작을 포함합니다. 이는 headless에 별도 예산이 필요하다는 뜻이지, 실제 운영 인벤토리가 얼마나 걸릴지를 말해주지는 않습니다.
장단점
장점:
- 모든 모드에서 일반 HTML에 대해 완벽한 recall — 4/4 링크와 전체 3/3 depth 체인, 별도 설정 없이 가능.
-jc는 브라우저 없이도 실제로 동작함: 연결된 JS 파일의 문자열 리터럴에서 2/2 엔드포인트 복구, 브라우저 비용 없음.-headless만이 런타임에 조립된 엔드포인트를 찾아냄 — 본질적으로 소스 파싱만으로는 보이지 않는 유형.- scope 기본값은 보수적임. 범위 밖 호스트는 기본값,
-fs fqdn,-cs모두에서 fetch되지 않음. - 단일 Go 바이너리, MIT 라이선스, 사전 빌드 및 Docker 이미지, 파이프라인 친화적인 입출력.
- 실패에 강함: 500과 끊어진 링크가 크롤을 멈추지 않음.
단점:
- 단일 실행으로는 JS 파일 엔드포인트와 런타임 DOM 엔드포인트를 모두 커버하지 못함. 완전한 커버리지를 위해서는 두 번 실행하고 합쳐야 함.
-jc는-headless아래에서는 아무 기여도 하지 않았음 — 모든 headless 실행에서 B 유형 0/2.- headless는 wall time을 5.1배 더 소모함(66.82초 vs 13.08초 p50, 범위 겹침 없음).
-resume는 seed 내에서 이미 끝난 페이지를 다시 크롤함. 엔드포인트 집합은 복원하지만, 경과 시간은 저장하지 못함.- known files는 robots.txt와 sitemap.xml을 요청했지만, IP 대상에서 sitemap
<loc>엔드포인트 2개 중 0개만 회수함. - headless는 결국 Chromium이 박혀 있어야 조용히 동작함. “하나의 바이너리” 이야기는 브라우저까지 가지 못함.
- 탐색만 가능. 구조화 추출도, 콘텐츠 변환도, 필드 스키마도 없음.
미테스트 항목이므로 이 수치들이 커버하지 않는 것: -jsluice, -d 1/-d 2의 depth 컷오프 테스트, multi-seed resume, 자동 폼 채우기, 그리고 실제 JavaScript가 많은 보호된 운영 사이트들. 모든 숫자는 macOS arm64 한 대에서 로컬 테스트 환경을 대상으로 측정했습니다.
누구에게 맞고, 누가 건너뛰어야 하는가
당신의 일이, 건드릴 권한이 있는 인프라의 엔드포인트 인벤토리를 만드는 것이라면 Katana는 파이프라인 형태가 적절합니다. STDIN/STDOUT 연결, 배포 가능한 바이너리, 브라우저 없이/브라우저 기반 두 모드까지 갖췄습니다. 이 테스트 환경에서는 두 번 합치는 작업이 심은 유형들을 모두 찾는 데 필요했습니다. 대상마다 둘 다 필요한지는 대표 페이지를 통해 확인해야 합니다.
주소가 아니라 데이터를 원한다면 건너뛰세요. Katana는 제품 테이블을 주지 않습니다. 제품이 있을 법한 URL을 줄 뿐이고, 추출은 다른 도구가 합니다. 한 번의 명령으로 끝나야 하는 상황이라면 그것도 맞지 않을 수 있습니다. 두 번 돌리고 합치는 과정은 파이프라인에서는 괜찮지만, 프롬프트 앞에서는 번거롭습니다. 그리고 탐색 대상이 IP일 때 sitemap의 <loc> 엔드포인트에 기대고 있다면, 출력물을 그대로 믿기 전에 실제로 무엇을 받고 있는지 검증하세요. 적어도 제 테스트 환경에서는 그 경로가 아무것도 돌려주지 않았습니다.
대안과 추출 경계
Katana는 무료이고, MIT 라이선스이며, 자체 호스팅됩니다. 탐색, 모드 선택, 브라우저 배포, 결과 병합을 모두 사용자가 책임지는 구조입니다.
오픈소스 범위에서 비교할 만한 도구는 언어보다 작업 기준으로 보는 편이 유용합니다. Colly는 또 다른 Go 옵션이지만, 직접 콜백을 작성해 컴파일하는 라이브러리이며 JavaScript 렌더링은 전혀 하지 않습니다. Crawl4AI는 실제 브라우저를 돌리고 LLM 파이프라인용 Markdown을 생성해, 출력 자체가 완전히 다릅니다. 여러 도구를 한꺼번에 비교하고 있다면, 우리의 오픈소스 스크래퍼 총정리가 카테고리를 나란히 정리해 줍니다.
고지 사항: Thunderbit는 발행사의 제품이며, 이 Katana 테스트 환경에서는 검증하지 않았습니다. Thunderbit는 관리형 추출 카테고리에 속해 페이지를 텍스트나 구조화된 레코드로 바꾸는 데 쓰이며, 승인된 대상의 엔드포인트 표면을 열거하는 역할은 아닙니다. 실제 워크플로우에서 두 카테고리를 함께 쓸 수는 있지만, 이 리뷰가 제공하는 근거는 Katana의 탐색 동작에 한정됩니다.
결론
Katana는 권한이 있는 대상의 엔드포인트 목록이 산출물이고, 그 대상들에서 모드별 커버리지를 검증할 수 있을 때 쓰세요. 이 테스트 환경에서는 standard -jc가 심어둔 JavaScript 파일 리터럴을 찾아냈고, headless는 런타임 DOM 엔드포인트를 찾아냈습니다. 테스트한 browserless 모드들은 그 런타임 경로를 찾지 못했습니다. 기본 scope는 두 번째 호스트를 fetch하지 않았고, browserless 실행은 500과 끊어진 링크를 지나쳤습니다.
운영상의 주의점도 분명합니다. 섞인 엔드포인트 유형에서는 두 번 돌려 합쳐야 할 수 있고, headless는 로컬 wall time의 약 다섯 배가 들며, 단일 seed resume는 이미 끝난 경로를 다시 가져오고, known-files recall은 IP 대상에서 0/2였습니다. 이것들은 v1.6.1의 테스트 결과이지, 모든 사이트에 대한 보장은 아닙니다. 하지만 프로덕션 평가에서 반복해야 할 점검 항목을 정의하기에는 충분합니다.
웹 데이터 추출용 Thunderbit 사용해 보기 Get Started Free
자주 묻는 질문
Katana는 resume 파일을 어디에 저장하나요? 그리고 resume하면 이미 크롤한 페이지를 건너뛰나요?
체크포인트는 플래그 도움말이 암시하는 작업 디렉터리의 resume.cfg가 아니라 ~/.config/katana/resume-<xid>.cfg에 저장됐습니다. 그리고 이미 끝난 페이지를 건너뛰지도 않습니다. 파일에는 진행 중인 seed URL만 저장되기 때문에, resume한 단일 seed 크롤은 이미 끝난 10개를 포함해 기준선의 11개 경로를 다시 가져왔습니다. 최종 엔드포인트 집합은 같지만, 절약된 시간은 없습니다.
왜 -kf all은 sitemap.xml은 요청하면서 안의 URL은 크롤하지 않았나요?
IP 대상에서는 플래그를 잘못 쓴 문제라기보다 scope 검증 경계와 일치하는 결과입니다. v1.6.1 코드에서 Katana의 sitemap 파서는 각 <loc> 요청을 만들 때 root hostname을 이어받지 않고, 그 뒤 IP-literal 호스트에 대한 DNS-scope 체크가 해당 URL을 거부할 수 있습니다. 다만 저는 실행 중에 그 메커니즘을 계측하지는 않았습니다. 여러 플래그, depth, seeding 변형에서도 recall은 0으로 유지됐습니다. 사용자 정의 -fs 호스트 정규식은 다른 검증 분기를 타며, 소스가 예측하는 수정책이긴 하지만 제 머신에서는 -kf와 함께 확인하지 못했습니다. 따라서 미검증으로 봐야 합니다. 오늘 당장 가장 신뢰할 방법은 <loc> URL을 직접 추출해 Katana의 seed로 넣는 것입니다.
Katana 커버리지 테스트를 보고할 때 무엇을 남겨야 하나요?
정확한 Katana 버전과 명령을 기록하세요. -fs 표현은 바이트 단위까지 동일하게 남기세요. 실행 전에 엔드포인트 유형을 정의하세요. stdout뿐 아니라 서버 측 히트 로그도 함께 보관하세요. 그리고 측정된 동작과 소스 기반 가설을 구분하세요. headless 실행의 경우 브라우저 빌드도 기록해야 합니다. 이번 테스트는 그 기록을 남기지 않았으므로 재현성에 한계가 있습니다.
-jc, -headless, 아니면 둘 다 써야 하나요?
필요한 엔드포인트 유형에 따라 고르세요. 이 테스트 환경에서는 standard -jc가 JavaScript 파일에 저장된 리터럴을 찾았고, headless는 런타임 DOM에 삽입된 엔드포인트를 찾았습니다. 어느 모드도 둘을 혼자 다 커버하지 못했으므로, 섞인 대상에서는 두 번 실행 후 중복 제거가 가장 타당한 선택이었습니다.
하나의 실패한 URL이 크롤 전체를 멈추나요? 이 통제된 실행에서는 그렇지 않았습니다. Katana는 500 응답과 끊어진 링크 이후에도 계속 진행했고, 나머지 도달 가능한 경로를 돌려줬습니다. 다만 이것이 프로덕션에서의 오류 회계까지 대신해 주는 것은 아닙니다. 실패 요청 로그를 보관하고 허용 가능한 실패율을 정해, 부분 성공한 크롤을 완전한 커버리지로 착각하지 않도록 하세요.


