Katana는 ProjectDiscovery의 엔드포인트 탐색 크롤러입니다. Go로 작성된 MIT 라이선스 바이너리로, 대상에 대해 실행하면 URL과 엔드포인트를 반환해 다음 도구로 넘길 수 있습니다. 브라우저 없이 동작하는 HTTP 모드로 크롤링할 수도 있고, Chromium을 구동하는 -headless 모드로도 실행할 수 있습니다. 공식 안내는 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 웹 크롤러를 원한다면, Katana는 완전히 다른 영역입니다. Katana는 /products/1138 같은 경로가 존재한다는 사실은 알려줄 수 있지만, 그 페이지의 내용이 무엇인지에 대해서는 아무 말도 하지 않습니다. 그건 설계 의도입니다. 이를 추출기처럼 평가한다면, 금속탐지기의 보석 감정 능력을 따지는 것과 비슷합니다.
주 무대는 공격 보안 정찰과 자동화 파이프라인입니다. STDIN으로 넣고 URL을 STDOUT으로 빼서 다음 도구에 넘기는 방식입니다. 그래서 분명한 전제를 먼저 밝힙니다. 여기의 모든 측정은 제가 직접 만든 127.0.0.1 로컬 테스트 환경에서 수행했습니다. Katana는 본인 소유이거나 명시적으로 테스트 권한을 받은 대상에만 사용해야 하며, 그 외에는 안 됩니다. 이 글은 누구의 방어를 우회하는 이야기가 아니라, 주어진 명령이 사이트의 엔드포인트 표면을 얼마나 정확히 열거하는지에 관한 이야기입니다.
세 가지 모드, 그리고 각각이 볼 수 있는 것
표준 모드는 Go HTTP 클라이언트입니다. HTML을 가져와서 파싱하고, href를 따라가며, 브라우저는 전혀 띄우지 않습니다. 빠르고 가볍지만, JavaScript가 실행된 뒤에야 생기는 요소는 보지 못합니다.
-jc(-js-crawl)는 브라우저 없이 동작하는 경로에 JavaScript 파서를 덧붙입니다. 연결된 .js 파일을 내려받아 URL처럼 생긴 문자열 리터럴을 소스에서 추출합니다. 실행은 하지 않고, 읽기만 합니다. README에는 -jsl(jsluice)도 더 무겁고 메모리를 더 쓰는 파서로 설명되어 있지만, 저는 테스트하지 않았으므로 커버리지에 변화를 주는지에 대해서는 말할 수 없습니다.

-headless는 Chromium을 구동하고 페이지 스크립트를 실행합니다. 이 테스트 환경에서는 조각을 조립해 런타임 DOM에 삽입하는 경로를 복원해 낸 유일한 Katana 모드였습니다. 다만 이 결과가 모든 파서나 미래의 Katana 모드가 같은 내용을 복원할 수 있다는 뜻은 아닙니다.
그리고 실전에 넣기 전에 먼저 몸에 익혀둘 범위 모델이 있습니다.
| 플래그 | 제어하는 것 | 값 / 기본값 |
|---|---|---|
-fs (field scope) | 어떤 호스트를 포함할지 | dn, rdn, fqdn, 또는 사용자 정의 정규식 — 기본값은 rdn |
-cs 및 -cos | 해당 field scope 내부에서 URL을 걸러내는 정규식 | — |
-kf | known files: robots.txt와 sitemap.xml | README는 최소 깊이 3이 필요하다고 설명 |
-d | 깊이 | 기본값 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 실행 위치 | 필요한 것 |
|---|---|
| 내 머신 | 이미 설치된 Chromium을 Katana가 자동으로 감지했으며, 브라우저 빌드나 경로는 따로 지정하지 않음 |
| 프로젝트 문서의 Ubuntu 안내처럼 빈 서버 | headless를 쓰기 전에 apt install google-chrome-stable 필요 |
| Docker 경로 | -system-chrome와 함께 headless 실행 |
빈 서버에서는 이런 편의가 사라집니다. -headless를 명령줄에 넣는 순간, 바이너리뿐 아니라 브라우저 예산도 함께 고려해야 합니다.
CI에서 돌릴 때 알아두면 좋은 작은 점 하나도 있습니다. Katana는 시작 시 GitHub로 버전 확인 요청을 보냅니다. -duc는 이 동작을 끕니다. 노트북에서는 큰 의미가 없지만, 네트워크가 끊긴 환경이나 rate limit이 걸린 러너에서는 매 실행마다 원치 않는 왕복 요청이 생깁니다. 제 시간 측정은 모두 -duc를 넣어, 홈 호출이 아니라 크롤링 자체를 측정했습니다.
어떻게 테스트했나
모드를 분리해서 보여주기 좋도록 의도적으로 고른 세 종류의 엔드포인트를 사용했습니다. 모든 대상은 로컬 테스트 서버에 있고, ground truth는 크롤링을 시작하기 전에 기록했습니다. 따라서 recall은 Katana가 우연히 출력한 값이 아니라 고정된 정답 집합을 기준으로 측정됩니다.
- 클래스 A — 일반 HTML.
/page/a,/page/b,/page/c, 그리고 3단계 체인/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, sitemap.xml, 그 안에만 존재하는 <loc> 엔드포인트 2개, 500을 반환하는 경로, 끊어진 링크, 그리고 다른 호스트명에 있는 두 번째 서버를 가리키는 out-of-scope 링크도 넣었습니다.
측정 도구도 테스트 환경만큼 중요합니다. 이 서버는 실제로 무엇이 요청되었는지를 카운트하므로, scope와 resume에 대한 결론은 Katana의 stdout이 아니라 실제 hit 기록에 근거합니다. 원시 실행 결과는 benchmark repo에 커밋되어 있으니, 계산이 맞는지 직접 확인할 수 있습니다.
아무도 수치화하지 않는 커버리지 분기

-d 4 기준으로, 모드별 엔드포인트 클래스 매트릭스는 다음과 같습니다.
| 모드 | HTML 링크 (A) | 깊이 체인 (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는 오직 한 구성, 즉 -jc가 붙은 표준 모드에서만 발견됐습니다. 클래스 C는 오직 두 구성, 즉 두 번의 headless 실행에서만 발견됐습니다. 두 열 모두에 히트가 있는 행은 없습니다. 전체 매트릭스는 discovery-summary.json에 있고, 계산된 필드 headless_jc_covers_both는 false입니다.
실무적으로 말하면, “커버리지를 높이려면 그냥 headless를 쓰라”는 말은 이 테스트에서는 충분하지 않았습니다. headless는 표준 결과에 클래스 B를 더해 주지 않았고, 대신 클래스 C를 복원하면서 클래스 B는 놓쳤습니다. 이 테스트에 심어 둔 모든 클래스의 엔드포인트를 다 잡으려면 두 번 크롤링하고 결과를 합쳐야 했습니다.
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 조합은 제가 업스트림에 가장 묻고 싶은 부분입니다. headless 실행에 JavaScript 파서를 더해도 아무 변화가 없었습니다. 클래스 B는 계속 0/2였고, 새로 재현해도 마찬가지였습니다. 저는 현상을 보고하는 것이지 메커니즘을 규명했다고 주장하는 것이 아닙니다. 왜 브라우저 경로가 JS 파일 리터럴에는 기여하지 않는지 내부를 계측해 보지는 않았습니다. 따라서 이건 원인 분석이 아니라 재현 가능한 관찰이자 GitHub 이슈감으로 보셔야 합니다. (덧붙이면, macOS ARM의 v1.6.1에서는 -hl -jc 조합이 종료 코드 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 재시도 꼬리를 두 경우 모두 그대로 기다립니다. 빠른 모드를 유리하게 보이도록 타임아웃을 조정하지 않았기 때문에, 타임아웃을 튜닝한 standard 실행이라면 격차는 줄어들기보다 오히려 더 벌어졌을 가능성이 큽니다.
이 비율은 로컬 용량 신호이지, 운영 환경 예측치가 아닙니다. 실제 대상은 지연, 실패, 스크립트 작업량, 스케줄링이 모두 다르며, 이 테스트는 기본 타임아웃 꼬리를 포함합니다. 측정된 5.1배 차이를 기준으로 headless에 별도 예산과 대상 하위집합이 필요한지 판단한 뒤, 대표적인 승인된 호스트에서 그 계획을 다시 벤치마크하세요.
범위는 지켜졌지만, 한 플래그는 조용히 아무 일도 하지 않았다
범위 테스트에는 두 서버가 사용되었습니다. 하나는 127.0.0.1의 મુખ્ય(primary) 서버이고, 다른 하나는 다른 포트의 localhost로 접근 가능한 두 번째 서버입니다. 그리고 그 두 번째 서버에만 존재하는 경로를 하나 제공했습니다. 따라서 그 경로에 hit가 있다는 것은 out-of-scope 호스트가 실제로 요청되었다는 뜻이지, 단순히 출력만 됐다는 뜻이 아닙니다.
| 구성 | 범위 밖 호스트가 요청됐나? | 두 번째 서버의 hit 수 |
|---|---|---|
기본값(-fs rdn) | 아니오 | 0 |
-fs fqdn | 아니오 | 0 |
-cs localhost | 아니오 | 0 |
| `-fs '(127.0.0.1 | localhost)'` | 예 |
범위 통제는 좋은 소식입니다. 기본값에서는 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라고 설명합니다. 그래서 작업 디렉터리에 resume.cfg가 떨어지는 것처럼 읽힙니다. 하지만 그렇지 않습니다. 제 환경에서는 체크포인트가 ~/.config/katana/resume-<xid>.cfg에 기록됐습니다. 문서에 경로가 없기 때문에, 문서가 아니라 실제 측정값입니다.
더 중요한 놀라움은 파일 내용입니다. 그 안에는 InFlightUrls 맵이 있었고, 들어 있는 항목은 정확히 하나, 시드 URL뿐이었습니다. 방문 완료 집합도, 프런티어도 아니었습니다. 그래서 제가 크롤을 3초 뒤 SIGINT로 중단한 뒤 resume했을 때 이런 일이 벌어졌습니다.
| 실행 | 고유 경로 수 |
|---|---|
| 전체 baseline 크롤 | 11 |
| 중단 전까지 가져온 수 | 10 |
| resume 실행에서 다시 가져온 수 | 이미 완료된 10개를 포함한 전체 11개 |
resume는 최종 엔드포인트 집합을 동일하게 복원했으므로, 기능이 깨진 것은 아닙니다. 다만 체크포인트의 단위는 입력 시드별이지 URL별이 아닙니다. 메모리의 dedupe 필터는 저장되지 않기 때문에, 단일 시드 크롤을 재개하면 그 시드를 처음부터 다시 크롤합니다(resume-summary.json). 만약 Katana에 500개 호스트 목록을 넣는다면, resume는 완전히 끝난 호스트들을 줄여 줄 것입니다. 그런 멀티 시드 동작은 상태 저장 방식상 그렇게 되는 것이지만, 저는 단일 시드만 측정했습니다. 아주 큰 하나의 사이트를 깊게 파고들 때는, resume가 시간을 절약해 주는 게 아니라 정합성을 보장해 줍니다.
known files: 요청은 했지만, 이후 버려졌다

-kf all -d 3는 실제로 두 파일을 모두 요청했습니다. robots.txt와 sitemap.xml이 서버 hit 로그에 찍혔습니다. 그런데 그 sitemap의 <loc> 요소에 적힌 엔드포인트 2개는 0/2만 복원했습니다. 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에 직접 시드 | 0/2, recall 0.0 |
문서에 적힌 요구사항 — -kf를 쓰고 최소 3단계 이상 깊게 가라는 조건 — 은 매번 충족했습니다. 즉, 플래그를 빼먹은 이야기는 아닙니다.
의사결정에 유용한 결론은 먼저 나옵니다. 이 IP 리터럴 테스트 환경에서는 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 리터럴 분기가 URL host와 비어 있는 root를 비교해 이를 거부할 수 있습니다. 별도의 scope 테스트에서 사용한 커스텀 -fs '(127.0.0.1|localhost)' 명령은 다른 scope 분기로 들어갔으므로, 이것은 소스가 예측하는 구조적 대안이지 -kf의 실측 우회책은 아닙니다. 제 머신에서는 known-files 클라이언트의 간헐적인 dialing 문제 때문에 확인 시도가 막혔고, 따라서 보고된 결과는 0/2로 남아 있습니다.
실제로 운영 환경에서 제가 할 일은 당분간 분명합니다. sitemap을 직접 가져와서 <loc> URL을 뽑고, 그 URL들을 Katana의 seed 목록으로 넘길 것입니다. 셸 두 줄이면 되고, scope 검증은 개입하지 않습니다.
한 가지는 문서 그대로, 그리고 칭찬받아 마땅하게 동작했습니다. 500 경로와 끊어진 링크는 요청되었고, 로그에 남았고, 문제 없이 건너뛰어졌습니다. 모든 browserless 실행은 종료 코드 0으로 끝났습니다. 첫 오류에서 죽는 크롤러는 무인 운용에 쓸 수 없고, Katana는 그렇지 않습니다.
배포 전에 대상별 커버리지 점검을 하라
이 테스트의 매트릭스는 자신의 승인된 대상에 대한 테스트 템플릿으로 가장 유용합니다. Katana를 돌리기 전에 엔드포인트 클래스를 정의하세요. 일반 링크, 연결된 스크립트 안의 리터럴, 실행 후에만 생성되는 경로, known-file 항목은 시작점으로 충분히 좋은 네 가지 범주입니다. 각 클래스마다 작은 정답 샘플을 남겨 두세요. 그런 사전 목록이 없으면, stdout 파일이 더 커 보인다는 이유만으로 커버리지가 좋아졌다고 착각하기 쉽습니다. 클래스 하나가 사라졌는데도 말이죠.
먼저 browserless 경로와 headless 경로를 각각 따로 측정하세요. 정확한 명령, Katana 버전, 브라우저 빌드, 종료 코드, 출력 결과를 저장합니다. 라인 수를 비교하지 말고, 엔드포인트 집합을 정규화한 뒤 diff를 보세요. 표본에서 standard -jc가 독자적인 기여를 하지 않는다면 headless 전용 정책으로도 충분할 수 있습니다. 하지만 여기처럼 집합이 갈라진다면 두 실행을 분리해 유지하고, 수집 뒤에 합치세요. 두 플래그를 한 명령에 넣는다고 해서 대상별 diff가 증명되기 전까지는 그 결과가 합집합과 같다고 가정하면 안 됩니다.
Katana 출력 밖의 증거로 scope를 검증하세요. 제외되어야 하는 호스트에 canary URL을 두고 그 서버의 요청 로그를 확인합니다. 크롤이 넓어져야 한다면 의도한 두 번째 호스트도 함께 시험하세요. 여기의 -cs localhost 실행은 content-scope 필터가 field scope를 넓히지 않아서 빈 출력을 만들었고, 커스텀 -fs '(127.0.0.1|localhost)' 호출은 두 번째 서버에 실제로 접근했습니다. 정규식은 문자 하나만 달라도 표시 방식이 아니라 매칭 방식이 바뀔 수 있으므로, 정확한 표현식을 기록하는 것이 중요합니다.
resume와 known files는 discovery recall과 별개로 테스트하세요. resume의 경우 대표 seed를 몇 페이지 진행한 뒤 중단하고, 생성된 체크포인트 경로를 저장한 다음, 이미 완료된 URL이 몇 개 다시 요청되는지 세세요. -kf의 경우 robots/sitemap이 실제 요청됐는지와, 심어 둔 <loc> URL이 정말 스케줄됐는지를 각각 확인해야 합니다. 이 둘은 서로 다른 주장입니다. 이 테스트에서는 파일은 가져왔지만 sitemap 엔드포인트 2개는 빠져 있었으므로, request 로그와 endpoint 출력 둘 다 있어야 경계가 보였습니다.
마지막으로, 유휴 상태에 가까운 머신에서 연속 실행을 통해 로컬 비용 기준선을 세우고, 이후 대표 대상에서 반복하세요. 배수만 적지 말고 최솟값, 최댓값, 중앙값도 보존하세요. 여기의 5.1배 수치는 이 테스트의 실패와 timeout 동작을 포함합니다. 이는 headless에 별도 예산이 필요하다는 뜻이지, 실제 운영 인벤토리가 얼마나 걸릴지 말해 주는 값은 아닙니다.
장단점
장점:
- 모든 모드에서 일반 HTML에 대해 완전한 recall — 4/4 링크와 3/3 깊이 체인 전체를 별도 설정 없이 수집.
-jc는 브라우저 없이도 실제로 작동함: 연결된 JS 파일의 문자열 리터럴에서 2/2 엔드포인트 복원, 브라우저 비용 없음.-headless만이 런타임에 조립된 엔드포인트를 찾아냄 — 소스 파싱으로는 구조적으로 보이지 않는 유형.- scope 기본값이 보수적임. out-of-scope 호스트는 기본값,
-fs fqdn,-cs모두에서 요청되지 않음. - 단일 Go 바이너리, MIT 라이선스, 사전 컴파일 빌드와 Docker 이미지 제공, 파이프라인형 I/O.
- 실패에 강함: 500과 dead link가 있어도 크롤이 멈추지 않음.
단점:
- 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이 있어야 조용히 동작함. “바이너리 하나”라는 이야기는 브라우저에서 끝납니다.
- 발견(discovery)만 담당함. 구조화 추출도, 콘텐츠 변환도, 필드 스키마도 없습니다.
테스트하지 않았으므로 이 수치에 포함되지 않는 항목: -jsluice, -d 1/-d 2의 별도 깊이 컷오프 테스트, 멀티 시드 resume, 자동 폼 채우기, 그리고 실제 JavaScript-heavy 또는 보호된 운영 사이트. 모든 수치는 로컬 테스트 환경을 대상으로 한 한 대의 머신(macOS arm64) 기준입니다.
누구에게 적합하고, 누구는 건너뛰어야 하나
당신의 일이 접근 권한이 있는 인프라의 엔드포인트 인벤토리를 만드는 것이라면, Katana는 파이프라인 형태가 잘 맞습니다. STDIN/STDOUT 연결, 배포 가능한 바이너리, 그리고 browserless와 browser-backed 두 모드가 있기 때문입니다. 이 테스트의 심어진 클래스에서는 두 번 실행 후 합치는 방식이 필요했습니다. 실제 대상에서도 두 번이 필요한지는 대표 페이지로 확인해야 합니다.
주소가 아니라 데이터를 원한다면 건너뛰세요. Katana는 제품 표를 주지 않습니다. 제품이 있을 법한 URL만 줍니다. 그리고 추출은 다른 도구가 맡습니다. 한 번의 명령으로 완전하길 원해도 마찬가지입니다. 두 번 돌리고 합치는 방식은 파이프라인에서는 괜찮지만, 프롬프트에서 바로 쓰기엔 번거롭습니다. 또한 IP를 대상으로 하면서 sitemap의 <loc> 엔드포인트에 의존한다면, 결과를 믿기 전에 실제로 무엇이 나오는지 검증하세요. 제 테스트에서는 그 경로가 아무것도 돌려주지 않았습니다.
대안, 그리고 추출의 경계
Katana는 무료이고 MIT 라이선스이며 자체 호스팅 가능합니다. 발견(discovery), 모드 선택, 브라우저 배포, 결과 병합을 모두 여러분 쪽에서 관리합니다.
오픈 소스 안에서의 비교는 언어보다 작업 기준으로 보는 것이 유용합니다. Colly는 또 다른 Go 옵션이지만, 자체 콜백으로 컴파일해 쓰는 라이브러리이며 JavaScript 렌더링은 전혀 하지 않습니다. Crawl4AI는 실제 브라우저를 실행하고 LLM 파이프라인용 Markdown을 생성하는데, 출력 성격이 완전히 다릅니다. 여러 도구를 동시에 검토한다면, 오픈 소스 스크래퍼 종합 정리에서 범주를 나란히 볼 수 있습니다.
공시: Thunderbit는 발행사의 제품이며, 이 Katana 테스트 환경에서는 검증하지 않았습니다. Thunderbit은 관리형 추출 범주에 있으며, 승인된 대상의 엔드포인트 표면을 열거하기보다 페이지를 텍스트나 구조화된 레코드로 바꾸는 데 초점이 있습니다. 하나의 워크플로에서 두 범주를 함께 쓸 수는 있지만, 이 리뷰가 제공하는 증거는 Katana의 발견 동작에 한정됩니다.
결론
접근 권한이 있는 대상을 크롤링하면서 결과물이 엔드포인트 목록이고, 모드별 커버리지를 그 대상에 맞게 검증할 수 있다면 Katana를 쓰십시오. 이 테스트에서는 standard -jc가 심어 둔 JavaScript 파일 리터럴을 복원했고, headless는 런타임 DOM 엔드포인트를 복원했습니다. 테스트한 browserless 모드들은 그 런타임 경로를 복원하지 못했습니다. 기본 scope는 두 번째 호스트를 요청하지 않았고, browserless 실행은 500과 끊어진 링크를 지나 계속 진행했습니다.
주의할 점은 운영상입니다. 혼합 엔드포인트 클래스라면 두 번 돌려 합치는 작업이 필요할 수 있고, headless는 로컬 wall time이 대략 5배 더 들었으며, 단일 시드 resume는 이미 완료된 경로를 다시 요청했고, known files recall은 IP 대상에서 0/2였습니다. 이 결과는 v1.6.1 로컬 테스트 환경의 결과이지, 모든 사이트에 대한 보장이 아닙니다. 그러나 프로덕션 평가에서 반복해야 할 체크리스트를 정의하기에는 충분합니다.
웹 데이터 추출용 Thunderbit 체험하기 Get Started Free
자주 묻는 질문
Katana의 resume 파일은 어디에 저장되며, 재개하면 이미 크롤링한 페이지를 건너뛰나요?
체크포인트는 작업 디렉터리의 resume.cfg가 아니라 ~/.config/katana/resume-<xid>.cfg에 저장됐습니다. 플래그 도움말이 암시하는 것과는 다릅니다. 그리고 완료된 페이지를 건너뛰지도 않습니다. 파일에는 진행 중인 seed URL만 저장되므로, 단일 시드 크롤을 재개하면 이미 끝난 10개를 포함해 baseline의 11개 경로를 모두 다시 요청했습니다. 최종 엔드포인트 집합은 같아지지만, 절약된 시간은 아닙니다.
왜 -kf all은 sitemap.xml을 요청했는데 그 안의 URL은 크롤하지 않았나요?
IP 대상에서는 플래그 실수라기보다 scope 검증 경계에 가깝습니다. Katana의 sitemap 파서는 각 <loc> 요청을 만들 때 root hostname을 이어서 전달하지 않으며, IP 리터럴 호스트의 DNS-scope 검사는 URL의 host를 비어 있는 root와 비교해 실패시키고 URL을 버립니다. 제가 시도한 모든 플래그, 깊이, 시드 변형에서 recall은 0이었습니다. 커스텀 -fs '(127.0.0.1|localhost)' 명령은 다른 scope 분기로 들어가고, 소스가 예측하는 수정 방향이긴 하지만, 제 머신에서는 -kf와 함께 확인하지 못했습니다. 지금 당장 신뢰할 방법은 <loc> URL을 직접 추출해서 Katana의 seed로 넣는 것입니다.
Katana 커버리지 테스트를 보고할 때 무엇을 남겨야 하나요?
정확한 Katana 버전과 명령을 기록하세요. -fs 정규식은 바이트 단위로 그대로 남겨야 합니다. 실행 전에 엔드포인트 클래스를 정의하고, stdout뿐 아니라 서버 측 hit 로그도 보관하세요. 소스 기반 가설과 실제 측정 결과도 분리해야 합니다. headless 실행이라면 브라우저 빌드도 기록하세요. 이번 테스트에서는 그것을 기록하지 않았기 때문에 재현성이 제한됩니다.
-jc, -headless, 아니면 둘 다 써야 하나요?
필요한 엔드포인트 클래스에 따라 선택하세요. 이 테스트에서는 standard -jc가 JavaScript 파일 안의 리터럴을 찾았고, headless는 런타임 DOM에 삽입된 엔드포인트를 찾았습니다. 어느 모드도 혼자서는 두 클래스를 모두 커버하지 못했습니다. 따라서 혼합 대상에는 두 번 실행한 뒤 중복 제거하는 방식이 가장 합리적이었습니다.
URL 하나가 실패하면 크롤이 멈추나요? 이 통제된 실행에서는 멈추지 않았습니다. Katana는 500 응답과 dead link를 지나 계속 진행했고, 나머지 접근 가능한 경로도 반환했습니다. 다만 이것이 운영 환경의 오류 집계까지 대신해 주는 것은 아닙니다. 실패 요청 로그를 따로 보관하고, 허용 가능한 실패율을 정의해야 부분 성공을 완전한 커버리지로 착각하지 않습니다.


