TRENDSHIFT · 2026.06.16

kage 딥다이브
— 진짜 브라우저로 렌더링한 뒤 JS를 전부 뜯어내는 오프라인 웹 복제기

kage(影, 일본어로 "그림자")는 웹사이트를 오프라인 폴더로 통째로 복제하되, 모든 자바스크립트를 제거해 "코드가 한 줄도 안 도는 정적 사본"을 만드는 Go 명령줄 도구다. 핵심 아이디어는 단순한 wget 미러링과 정반대다 — 헤드리스 Chrome으로 페이지를 실제로 렌더링해서 JS가 만들어 낸 최종 DOM을 스냅샷한 다음, 거기서 스크립트·이벤트 핸들러·추적 코드를 전부 도려내고 CSS·이미지·폰트만 로컬 경로로 끌어내린다. 결과물은 살아있는 사이트처럼 보이지만 아무 코드도 실행하지 않고, 네트워크에 전화를 걸지도 않는다. (저장소: tamnd/kage · 언어 Go 1.26 · 약 9,800 LOC · 라이선스 MIT)

이 문서는 README를 번역하지 않는다. 대신 clone/, browser/, sanitize/, urlx/, zim/ 같은 실제 패키지 소스를 해부해 "렌더 → JS 제거 → 자산 로컬화 → 정적 사본/단일 파일 패킹" 파이프라인이 어떻게 동작하는지에 초점을 맞춘다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 — 렌더→JS제거→복제 파이프라인
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

이 레포가 무엇을 하는 물건인가.

핵심 메시지

"웹페이지를 사진처럼 박제한다 —
실행되는 프로그램이 아니라, 그저 보이는 그대로의 정적 사본으로."

kage는 한 줄로 말하면 "JS를 제거하는 오프라인 웹 아카이버"다. 보통 페이지 저장(Ctrl+S)이나 wget으로 받은 사본은 6개월 뒤 열어 보면 빈 화면이거나, 멈추지 않는 스피너이거나, 이미 사라진 분석 서버로 계속 요청을 보낸다. 페이지가 사실은 "남의 자바스크립트를 위한 얇은 클라이언트"였기 때문이다. kage는 진짜 브라우저로 페이지를 끝까지 렌더링한 뒤, 그 결과물에서 스크립트를 전부 뜯어내고 자산만 로컬에 남긴다. 그래서 10년 뒤에 열어도 그냥 열린다.

제공하는 명령은 네 개뿐으로 단순하다. kage clone <url>로 사이트를 복제하고, kage serve [dir]로 로컬에서 미리 보고, kage pack <dir>로 폴더를 단일 ZIM 파일 또는 자기완결 실행파일로 압축하고, kage open <file.zim>으로 그 패킹된 파일을 다시 열어 본다.

핵심 용어
헤드리스 브라우저 (Headless Browser)
화면(GUI) 없이 백그라운드에서 돌아가는 진짜 웹 브라우저(보통 Chrome/Chromium). 사람이 보는 것과 똑같이 페이지를 렌더링하고 JS를 실행하지만, 창을 띄우지 않아 서버·자동화에서 쓴다. kage는 이걸로 "JS가 다 그린 뒤의 최종 화면"을 캡처한다.

2왜 주목받는가

수많은 웹 아카이빙 도구가 있는데, kage가 트렌딩에 오른 이유.

웹 아카이빙 도구는 많다. wget --mirror, HTTrack, SingleFile, ArchiveBox… kage가 차별화되는 지점은 두 가지 결정이다 — ① "렌더링 후 캡처"로 JS 사이트의 진짜 내용을 잡고, ② "캡처 후 JS 완전 제거"로 코드가 안 도는 영구 사본을 만든다는 것.

강점 1 — wget이 못 잡는 걸 잡는다

wget은 서버가 처음 내려준 HTML만 받는다. 요즘 사이트는 그 HTML이 거의 비어 있고 JS가 내용을 그려 넣으므로, wget 사본은 빈 껍데기가 되기 일쑤다. kage는 헤드리스 Chrome으로 페이지를 실제로 띄워 JS가 DOM을 다 그릴 때까지(network-idle + 정착 시간) 기다린 뒤, 사람이 봤을 최종 DOM을 직렬화한다. SPA(싱글 페이지 앱)도 정적으로 굳는다.

강점 2 — SingleFile과 정반대 철학: JS를 죽인다

SingleFile은 페이지를 한 파일로 묶지만 스크립트를 그대로 둘 수 있다. kage는 정반대로 스크립트·이벤트 핸들러·javascript: URL·죽은 preconnect/preload 힌트·IE 조건부 주석에 숨은 script까지 모조리 제거한다. 추적도, 네트워크 호출도, 깜짝 동작도 없다. 결과물은 "프로그램이 아니라 사진"이다.

강점 3 — 폴더를 넘어 "단 하나의 파일"로

미러는 폴더라 옮기기 번거롭다. kage는 pack으로 미러 전체를 ZIM(위키피디아를 오프라인으로 나르는 Kiwix 생태계의 표준 포맷) 한 파일로 압축하거나, 아예 자기 자신이 사이트인 단일 실행파일로 만든다. 받는 사람은 kage도, ZIM 리더도 필요 없이 그 파일만 더블클릭하면 된다. macOS .app, Linux AppImage, Windows GUI exe까지 지원한다.

균형 잡힌 시각
한계도 분명하다

① Chrome 의존. 진짜 브라우저를 돌리므로 Chrome/Chromium이 호스트에 있어야 하고(컨테이너 이미지는 Chromium 내장), wget보다 느리고 무겁다. ② 인터랙티브 기능은 죽는다. JS를 제거하므로 검색창·필터·동적 메뉴 등 클라이언트 동작은 사라진다 — "읽기 전용 박제"가 목적이다. ③ ZIM 전문검색 미지원. 구조적으로 유효한 ZIM을 쓰지만 Kiwix가 만드는 풀텍스트 인덱스는 없어 리더 내 검색이 제한적이다.

3기술 스택 전체 지도

go.mod와 실제 import를 뜯어 본 kage의 기술 구성.

kage는 순수 Go 단일 바이너리를 지향한다(기본 빌드는 CGO_ENABLED=0). 외부 의존성은 신중히 골랐고, 핵심은 "브라우저 제어 + HTML 파싱 + 압축 + CLI" 네 갈래다.

영역기술 / 라이브러리역할
언어/런타임Go 1.26정적 단일 바이너리, 강력한 동시성(goroutine)
브라우저 제어go-rod/rod + stealthDevTools 프로토콜로 헤드리스 Chrome 구동·DOM 스냅샷
HTML 파싱golang.org/x/net/htmlDOM 트리 파싱·순회·재직렬화 (sanitize의 토대)
URL 처리golang.org/x/net/publicsuffix등록 가능 도메인 판정(서브도메인 범위 결정) — x/net의 서브패키지라 go.mod 의존성은 golang.org/x/net 하나다
CLI 프레임워크spf13/cobra + charmbracelet/fang명령 트리·플래그·자동완성·세련된 도움말
터미널 UIcharm.land/lipgloss/v2진행 로그·요약의 스타일링
압축klauspost/compress (zstd)ZIM 본문 텍스트 zstd 압축
데이터셋 출력parquet-go/parquet-go크롤 결과를 Parquet 컬럼형으로 내보내기
네이티브 창(opt-in)webview/webview_go-tags webview 빌드 시 OS 내장 WebView 창
식별자google/uuidZIM 아카이브 UUID(콘텐츠 기반 결정적 생성)
배포GoReleaser + Docker(GHCR)다중 OS 아카이브·deb/rpm/apk·Chromium 내장 이미지·SBOM·cosign 서명
비유

kage의 스택을 사진관에 비유하면 — go-rod는 피사체(웹페이지)를 제대로 세팅하고 셔터를 누르는 카메라, x/net/html은 찍힌 필름을 현상해 합성·보정하는 암실, sanitize는 배경에 끼어든 잡티(JS)를 지우는 리터칭, zim/pack은 완성된 사진을 한 권의 앨범(단일 파일)으로 제본하는 제본소다.

왜 go-rod인가 (Puppeteer/Playwright 대신)

go-rod는 Go 네이티브 DevTools 프로토콜 클라이언트다. Node 런타임 없이 Go 바이너리 하나로 Chrome을 제어할 수 있어, "단일 정적 바이너리" 목표와 맞는다. stealth 플러그인은 자동화 탐지(navigator.webdriver 등)를 우회해, 봇 차단이 있는 사이트도 렌더링할 여지를 준다.

4아키텍처 심화 — 렌더→JS제거→복제 파이프라인

한 줄의 URL이 디스크 위의 정적 사본이 되기까지.

kage의 전체 흐름은 README가 압축해 보여 준다: seed URL ▶ headless Chrome(render) ▶ final DOM(snapshot) ▶ strip JS(sanitize) ▶ localise assets(rewrite links) ▶ disk. 이를 실제 패키지 단위로 펼치면 다음과 같다.

┌─────────────────────────── kage clone 파이프라인 ───────────────────────────┐ │ │ │ seed URL ──▶ [urlx.ParseSeed] 스킴 없으면 https:// 부여, 정규화 │ │ │ │ │ ▼ │ │ [frontier] 중복 제거 큐 (BFS, robots.txt + sitemap.xml 시드) │ │ │ │ │ ┌────────────┴─────────────┐ │ │ ▼ (페이지 워커 N개) ▼ (자산 워커 M개) │ │ ┌───────────────────┐ ┌────────────────────┐ │ │ │ browser.Pool │ │ asset.Downloader │ │ │ │ 헤드리스 Chrome │ │ plain HTTP 다운로드│ │ │ │ navigate │ │ CSS·이미지·폰트 │ │ │ │ settle(idle 대기)│ │ url() 재작성 │ │ │ │ (옵션) scroll │ └─────────┬──────────┘ │ │ │ DOM 직렬화 │ │ │ │ └─────────┬─────────┘ │ │ │ │ 최종 DOM(*.html 문자열) │ │ │ ▼ │ │ │ ┌───────────────────────────┐ │ │ │ │ sanitize.CleanTree │ │ │ │ │ <script> 삭제 │ │ │ │ │ onclick 등 핸들러 삭제 │ │ │ │ │ javascript: URL 무력화 │ │ │ │ │ noscript / meta-refresh │ │ │ │ │ 죽은 preconnect/preload │ │ │ │ └─────────┬─────────────────┘ │ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────────────────────────────┐ │ │ │ urlx.LocalPath / urlx.Rel │ │ │ │ 모든 URL ⇄ 디스크 경로를 결정적으로 매핑 │ │ │ │ → 링크는 자산이 받아지기 전에 미리 재작성 가능 │ │ │ └─────────┬──────────────────────────────────────┘ │ │ ▼ │ │ disk: <out>/<host>/index.html · greatwork.html · _kage/... │ │ │ └──────────────────────────────────────────────────────────────────────────────┘ ── 이후 (선택) ──▶ pack ──▶ ZIM 한 파일 / 자기완결 바이너리

핵심 설계 패턴 4가지

패턴 1 — 순수(pure)한 URL↔경로 계약

urlx 패키지는 네트워크도, 파일시스템도, 시계도 건드리지 않는 순수 함수 집합이다. Normalize가 모든 URL을 정규 키로 접고, LocalPath가 그 키를 디스크 경로로 결정적 매핑한다. 그래서 자산이 아직 다운로드되기도 전에 페이지 워커가 그 자산을 가리키는 링크를 미리 재작성할 수 있다 — 양쪽이 "바이트가 어디 놓일지"에 미리 합의하기 때문. 이게 파이프라인을 단순하게 만드는 핵심이다.

패턴 2 — DOM은 한 번만 파싱, 트리를 공유

sanitize.Strip은 바이트→트리→바이트를 다 하지만, 클로너는 더 영리하게 sanitize.CleanTree(이미 파싱된 *html.Node를 제자리에서 청소)를 쓴다. 즉 HTML을 한 번만 파싱해서 JS 제거기와 자산 재작성기가 같은 트리를 공유한다. 같은 문서를 두 번 파싱하는 낭비를 없앤 설계.

패턴 3 — 두 개의 풀, 두 개의 동시성

페이지 렌더링(무겁고 Chrome 탭이 한정적)과 자산 다운로드(가볍고 plain HTTP)는 성격이 달라 별도 워커 풀로 분리했다. browser.Pool은 세마포어(chan struct{})로 동시 탭 수를 제한하고(기본 4), asset.Downloader는 독립적으로 HTTP를 친다. --workers(기본 4, README에 문서화)가 렌더 풀 크기이고, 자산 다운로드 풀은 --asset-workers(기본 8)로 따로 조절한다.

패턴 4 — 멱등성과 재개(resume)

각 페이지는 "쓰여질 파일"로 키가 매겨져, http/https·트레일링 슬래시 유무와 무관하게 같은 글은 정확히 한 번만 가져온다. frontier의 visited 집합은 _kage/state.json에 저장돼, Ctrl-C로 끊어도 다시 실행하면 멈춘 곳부터 이어 간다(--refresh는 제자리 재렌더, --force는 호스트 삭제 후 새로).

JS를 "어떻게" 제거하나 — sanitize 내부

가장 핵심인 JS 제거를 코드 수준으로 보면, golang.org/x/net/html로 파싱한 트리를 재귀 순회하며 노드별로 처리한다.

// sanitize.CleanTree — DOM 트리를 제자리에서 청소 (요지)
switch c.DataAtom {
case atom.Script:           // <script> 통째로 삭제
    n.RemoveChild(c); rep.ScriptsRemoved++
case atom.Noscript:         // 기본은 삭제, KeepNoscript면 본문으로 펼침
    n.RemoveChild(c)
case atom.Meta:             // JS 타깃 meta-refresh 제거
    if isMetaRefresh(c) { n.RemoveChild(c) }
case atom.Link:             // 오프라인에서 무의미한 preconnect/preload 제거
    if isDeadLink(c) { n.RemoveChild(c) }
}
stripHandlers(c, rep)        // onclick, onload 등 on* 속성 제거
neutralizeJSURLs(c, rep)     // href="javascript:..." 무력화

특히 영리한 부분은 IE 조건부 주석 처리다. <!--[if lt IE 9]><script src=...><![endif]-->은 하나의 "주석 노드"로 파싱되는데, 그 안에 <script>가 텍스트로 숨어 있다. 요소만 순회하면 이 스크립트를 못 보고 지나치므로, kage는 조건부 주석을 통째로 삭제해 주석에 밀반입된 JS까지 차단한다. 또 문서에 charset이 없으면 ensureCharset으로 추가해 오프라인에서 한글·UTF-8이 깨지지 않게 한다.

설계 결정의 트레이드오프
"왜 굳이 진짜 Chrome을?"

JS를 결국 지울 거면서 왜 무겁게 Chrome으로 실행부터 시킬까? 답은 "실행 결과(DOM)는 남기되 실행 능력(JS)은 없앤다"는 것. 정적 HTML만 받으면(=wget) 빈 페이지가 되고, JS를 남기면(=SingleFile 일부) 박제가 아니다. kage는 "JS가 한 일의 결과만 화석으로 굳히는" 중간 길을 택했고, 그래서 렌더 후 제거라는 2단계가 필수다.

5디렉토리 구조 해부

"관심사별로 쪼갠다" — kage 저장소는 패키지 경계가 곧 설계도다.

kage는 README가 직접 밝히듯 "관심사로 분할(split by concern)"된 구조다. 각 디렉토리가 파이프라인의 한 단계에 정확히 대응한다.

kage/ ├── cmd/kage/main.go 얇은 진입점: 메인 스레드 고정 후 cli.Execute로 위임 ├── cli/ cobra 명령 트리와 플래그 배선 │ ├── root.go 루트 명령·전역 플래그 │ ├── clone.go clone 명령 (가장 큰 플래그 집합) │ ├── serve.go 로컬 미리보기 HTTP 서버 │ ├── pack.go 미러 → ZIM/바이너리 패킹 │ ├── open.go 패킹된 ZIM 열기 │ └── parquet.go 크롤 결과 Parquet 내보내기 ├── clone/ 크롤 엔진: frontier·렌더워커·자산워커·재개 상태 │ ├── cloner.go 전체 오케스트레이션 │ ├── frontier.go 중복제거 큐 + visited 영속 │ ├── config.go Config 구조체(모든 knob) │ ├── sitemap.go sitemap.xml 시드 │ └── stats.go 실행 요약 집계 ├── browser/ 헤드리스 Chrome 제어와 DOM 스냅샷 │ └── pool.go Chrome 풀·세마포어·settle·autoScroll ├── sanitize/ DOM에서 스크립트·핸들러·javascript: 제거 │ └── sanitize.go CleanTree / Strip / 조건부주석 처리 ├── asset/ CSS·이미지·폰트 다운로드와 로컬화 │ ├── download.go 자산 가져오기 │ ├── css.go url() 재작성 │ └── html.go HTML 내 자산 참조 재작성 ├── urlx/ URL ⇄ 경로의 결정적 매핑 (순수 함수) │ └── urlx.go Normalize / LocalPath / Rel ├── zim/ 순수 Go ZIM 리더 & 라이터 │ ├── reader.go writer.go codec.go format.go ├── pack/ 미러 → ZIM/자기완결 바이너리, 오프라인 HTTP 핸들러 │ ├── zim.go 미러를 ZIM으로 │ ├── binary.go 아카이브를 바이너리에 append │ ├── appbundle.go macOS .app 번들 │ ├── appdir.go Linux AppImage/.AppDir │ ├── icon.go icns.go favicon 추출·아이콘 생성 │ └── serve.go 패킹본을 오프라인으로 서빙 ├── viewer/ 서빙 표현: 시스템 브라우저 또는 네이티브 창 ├── robots/ robots.txt 매처 ├── dataset/ Parquet 데이터셋 출력 ├── docs/ tago 기반 문서 사이트 ├── Dockerfile Makefile go.mod CHANGELOG.md └── scripts/ asset/ pack/ ...

이 구조의 미덕은 의존성이 한 방향으로 흐른다는 것이다. cliclone을 부르고, clonebrowser·sanitize·asset·urlx를 조립한다. 가장 밑바닥의 urlx는 아무것도 의존하지 않는 순수 패키지라, 단위 테스트가 쉽고 전체를 추론하기 좋다(각 패키지마다 *_test.go가 동반된다).

6학습 포인트 (기술별)

이 레포를 읽으며 실제로 손에 남기면 좋은 것들.

① 헤드리스 브라우저 자동화 (go-rod / DevTools 프로토콜)

"JS 사이트를 어떻게 캡처하나"의 답이 여기 있다. browser/pool.go는 페이지로 이동(navigate) → 네트워크가 조용해질 때까지 대기(settle, 기본 1.5초) → (옵션) 자동 스크롤로 lazy 이미지 트리거 → 최종 DOM 직렬화의 순서를 보여 준다. 스크래핑·E2E 테스트·스크린샷 서비스의 토대가 되는 패턴이다.

// autoScroll — lazy-load 이미지를 끌어내기 위해 페이지를 단계적으로 스크롤
page.Eval(`() => new Promise((resolve) => {
    let total = 0; const step = 400;
    const timer = setInterval(() => {
      window.scrollBy(0, step); total += step;
      if (total >= document.body.scrollHeight) {
        clearInterval(timer); window.scrollTo(0, 0); resolve();
      }
    }, 100);
})`)

② DOM 파싱과 트리 변형 (x/net/html)

정규식으로 HTML을 자르는 건 위험하다. kage는 golang.org/x/net/html제대로 된 DOM 트리를 만들어 재귀 순회하며 노드를 삭제·치환한다. "트리를 한 번만 파싱해 여러 변형기가 공유"하는 CleanTree 패턴은 어떤 HTML 처리기에도 응용된다.

③ 결정적 URL↔경로 매핑 (멱등성의 비결)

크롤러의 영원한 골칫거리인 "같은 페이지를 여러 URL로 만나는 문제"를 urlx.Normalize가 정규화로 푼다. 그리고 경로를 해시 충돌 없이 결정적으로 만들어, 자산이 받아지기 전에 링크를 미리 재작성하는 비동기 파이프라인을 가능케 한다. "순수 함수로 부수효과를 격리"하는 설계의 교과서.

④ Go 동시성: 워커 풀과 세마포어

chan struct{} 세마포어로 동시 실행 수를 제한하고, sync.WaitGroup·sync.Mutex로 공유 상태(seenAssets, frontier)를 보호하는 전형적이고 견고한 Go 동시성 패턴을 실전 코드로 볼 수 있다.

⑤ 바이너리 포맷 직접 다루기 (ZIM)

zim/은 외부 라이브러리 없이 순수 Go로 ZIM 포맷을 읽고 쓴다. 헤더·디렉토리 엔트리·클러스터·zstd 압축을 직접 구현한 것이라, 파일 포맷 설계와 직렬화를 배우는 살아있는 예제다. "패킹이 결정적(같은 입력→바이트 동일 출력)"이라 체크섬·캐시가 안전하다는 점도 주목할 만하다.

실습 아이디어
"sanitize 패키지만 따로 떼서 써 보기"

kage의 sanitizeStrip([]byte) ([]byte, Report, error)라는 깔끔한 인터페이스다. 임의의 HTML 한 덩이를 넣어 JS가 얼마나 제거됐는지 Report로 확인하는 작은 프로그램을 만들어 보면, "안전한 HTML 살균"이 어떻게 동작하는지 손에 잡힌다. XSS 방어·메일 클라이언트 HTML 정제와도 직결된다.

7시스템 요구사항

kage를 돌리려면 무엇이 필요한가.

항목요구사항비고
실행 환경Go 1.26+ (소스 빌드 시) 또는 프리빌트 바이너리go install github.com/tamnd/kage/cmd/kage@latest
필수 외부 의존Chrome 또는 Chromiumkage가 시스템 설치를 자동 탐지. --chrome/KAGE_CHROME로 지정 가능
컨테이너 사용 시Docker만 있으면 됨GHCR 이미지에 Chromium 내장 — 호스트에 Chrome 불필요
메모리Chrome 탭 수 × 페이지 크기에 비례--workers로 동시 탭 수 조절(기본 4), 사양 낮으면 줄일 것
네이티브 창 빌드cgo + 플랫폼 WebView-tags webview 옵트인. 기본 빌드는 순수 Go(CGO 끔)
OSLinux / macOS / Windows릴리스가 전 플랫폼 아카이브·deb/rpm/apk 제공
초보자 함정
"Chrome이 없는데요"

kage는 자체적으로 브라우저를 받지 않고 호스트의 Chrome/Chromium을 빌려 쓴다. 설치가 없거나 못 찾으면 렌더가 실패한다. 가장 쉬운 회피책은 컨테이너 이미지(ghcr.io/tamnd/kage) 사용 — Chromium이 들어 있어 추가 설치가 필요 없다. docker run --rm -v "$PWD/out:/out" ghcr.io/tamnd/kage clone example.com.

8직접 해볼 수 있는 실습 과제

"읽기"에서 "하기"로 — 난이도별 5단계.

① 첫 사이트 복제 + 서빙 난이도 ★☆☆☆☆

작은 정적 블로그 하나를 kage clone example.com --max-pages 20으로 복제하고, kage serve ~/data/kage/example.com으로 띄워 본다. 브라우저 개발자도구 Network 탭을 열어 외부로 나가는 요청이 0인지 확인 — JS 제거가 실제로 됐다는 증거다.

② wget 사본과 비교하기 난이도 ★★☆☆☆

JS로 내용을 그리는 사이트(예: 일부 SPA)를 wget과 kage로 각각 받아 결과 HTML을 비교한다. wget 사본은 비어 있고 kage 사본엔 내용이 있는 차이를 직접 눈으로 확인하면 "렌더 후 캡처"의 가치가 와닿는다.

③ 범위·깊이 제어로 정밀 크롤 난이도 ★★★☆☆

큰 문서 사이트에서 한 섹션만 복제: kage clone go.dev --scope-prefix /doc --max-depth 2 --scroll. --exclude로 특정 경로를 빼고, Ctrl-C로 끊은 뒤 다시 실행해 재개(resume)가 동작하는지(_kage/state.json 확인) 검증한다.

④ ZIM/단일 바이너리로 패킹 난이도 ★★★★☆

kage pack example.com으로 ZIM을 만들고 kage open으로 다시 열어 본다. 이어 kage pack example.com --format binary -o site실행하면 자기 자신이 사이트가 되는 단일 파일을 만들어, kage 없이 ./site만으로 서빙되는지 확인한다.

⑤ sanitize를 라이브러리로 끌어다 쓰기 난이도 ★★★★★

작은 Go 프로그램에서 github.com/tamnd/kage/sanitize를 import해 임의 HTML을 Strip하고, 반환된 Report(ScriptsRemoved, HandlersRemoved 등)를 출력해 본다. 더 나아가 Options.KeepNoscript 동작 차이를 실험하며 HTML 살균 로직을 내 프로젝트에 이식한다.

9관련 기술 심화 학습 로드맵

kage를 발판으로 "웹 자동화 + 시스템 프로그래밍"을 4주에 쌓는다면.

주차주제도달 목표
1주차Go 기초 + 동시성goroutine·channel·WaitGroup·세마포어 패턴 이해, kage의 워커 풀 코드 읽기
2주차HTML/DOM 처리x/net/html로 트리 파싱·순회·변형, 작은 HTML 살균기 직접 작성
3주차헤드리스 브라우저 자동화go-rod(또는 Playwright)로 navigate·settle·DOM 스냅샷·스크린샷 구현
4주차크롤러 설계 + URL 정규화BFS frontier·중복제거·robots.txt·재개 상태 영속화 설계
이후바이너리 포맷 / 패키징ZIM·zstd 압축 이해, Go 바이너리에 데이터 append, GoReleaser 배포
조언

kage는 "작지만 완성도 높은 Go 프로젝트"의 좋은 표본이다. 한꺼번에 다 이해하려 말고 한 패키지씩(특히 의존 없는 urlxsanitizebrowser 순서로) 테스트 코드와 함께 읽는 것이 가장 빠르다. 각 패키지가 단일 책임을 지므로, 하나를 떼어 내 다른 프로젝트에 재사용하기도 쉽다.

10핵심 키워드 사전

kage를 읽다 막히는 용어 빠른 참조.

헤드리스 Chrome화면 없이 백그라운드로 도는 Chrome. JS를 실제로 실행해 페이지를 렌더링한다.
DevTools 프로토콜Chrome을 외부에서 원격 제어하는 디버깅 프로토콜(CDP). go-rod가 이걸로 통신한다.
go-rodGo용 DevTools 프로토콜 클라이언트. Node 없이 Chrome을 제어한다.
DOM 스냅샷JS가 다 그린 뒤의 최종 문서 구조(DOM)를 HTML 문자열로 직렬화한 것.
settle / network-idle페이지 로드 후 네트워크 요청이 잠잠해지는 구간. 이때 DOM이 안정됐다고 보고 캡처한다.
sanitize(여기선) HTML에서 스크립트·핸들러·javascript: URL을 제거해 무해하게 만드는 것.
javascript: URLhref="javascript:..."처럼 링크에 숨은 실행 코드. kage가 무력화한다.
조건부 주석구형 IE용 <!--[if IE]>...<![endif]-->. 안에 script가 숨을 수 있어 통째로 제거.
frontier크롤러가 아직 방문 안 한 URL들의 큐. 중복을 제거해 같은 페이지를 한 번만 받는다.
멱등성(idempotent)같은 입력에 몇 번을 실행해도 결과가 같은 성질. kage는 같은 글을 정확히 한 번만 쓴다.
정규화(Normalize)다르게 생긴 URL들을 같은 자원이면 하나의 정규 키로 접는 것.
ZIM웹사이트/위키 전체를 압축·인덱싱한 단일 읽기전용 파일 포맷. Kiwix 생태계 표준.
KiwixZIM 파일을 읽어 위키피디아 등을 오프라인으로 보게 해 주는 프로젝트/앱.
zstd구글이 만든 빠르고 압축률 좋은 압축 알고리즘. kage가 ZIM 본문 압축에 사용.
WebViewOS에 내장된 웹 렌더링 엔진(WKWebView·WebView2·WebKitGTK). 네이티브 창에 쓴다.
cgoGo에서 C 라이브러리를 호출하는 다리. WebView 빌드에 필요(기본 빌드는 끔).
SPASingle Page App. 한 HTML에서 JS가 화면을 갈아끼우는 앱. wget이 못 잡는 대표 사례.

11참고 링크