본문으로 건너뛰기
5분 AI.
GITHUB 레포 딥다이브 · 2026-07-29 · BUILDERIO/GPT-CRAWLER · 사이트를 커스텀 GPT용 지식 파일로

BuilderIO/gpt-crawler 딥다이브
— 웹사이트를 크롤링해서 커스텀 GPT용 지식 파일 하나로

크롤러

빠른 판단

무엇
지정한 사이트의 본문을 크롤링해 LLM 지식 업로드용 JSON 파일로 저장하는 소형 크롤러
이럴 때
문서 사이트의 범위와 본문 선택자를 지정해 커스텀 GPT 등에 넣을 지식 파일을 준비할 때
입력
시작 URL·수집 범위·본문 CSS 선택자
결과
추출한 페이지 본문을 담은 output.json 또는 분할 JSON 파일
gpt-crawler는 하나(혹은 사이트맵 URL)를 출발점 삼아 사이트를 헤드리스 브라우저로 돌아다니며 본문 텍스트를 긁어모은 뒤, 그 결과를 JSON 지식 파일로 묶어주는 Node.js·TypeScript 도구다. 목적은 아주 좁고 명확하다 — 이렇게 만든 파일을 ChatGPT의 커스텀 GPT나 OpenAI Assistants API의 "지식(Knowledge)" 업로드 칸에 넣어, 특정 문서 사이트만 훤히 꿰고 있는 나만의 챗봇을 만드는 것.

내부는 크롤링 자동화 라이브러리 Crawlee의 PlaywrightCrawler(실제 브라우저를 원격 조종해 페이지를 여는 크롤러)를 그대로 가져다 쓰고, 그 위에 "CSS/XPath 셀렉터로 본문만 뽑기"와 "토큰·용량 기준으로 결과 파일을 자동으로 쪼개기"라는 얇지만 실용적인 두 겹의 로직만 얹은 구조다. CLI 대화형 실행, 로컬 스크립트 실행, Express API 서버, Docker 컨테이너까지 네 가지 실행 방식을 모두 지원한다.

(저장소 BuilderIO/gpt-crawler · 언어 TypeScript · 라이선스 ISC · package.json 버전 1.5.1 · 핵심 의존성 Crawlee 3.6.x + Playwright 1.40.x(lockfile 기준) · 최신 커밋 2025-07-07, 해시 d2245d6(PR #399 머지) — 이 문서 작성 시점 기준 약 1년간 신규 커밋 없음)
목차
  1. 한 줄 정체
  2. 왜 뜨는가 — 그리고 지금은 어떤 상태인가
  3. 무엇을 해결하나
  4. 전체 구조
  5. 핵심 설계·동작 원리
  6. 기술 스택·의존성
  7. 설치·요구사항
  8. 실제 사용 흐름
  9. 함정·오해 방지
  10. 대안 비교
  11. 정리 — 누가 쓰면 좋은가
  12. 직접 해볼 수 있는 실습 과제
  13. 관련 기술 심화 학습 로드맵
  14. 핵심 키워드 사전

1한 줄 정체

"사이트를 대신 읽어와 GPT가 먹기 좋은 JSON으로 정리해주는 소형 크롤러"

gpt-crawler는 "웹사이트 → LLM이 먹을 수 있는 지식 파일" 변환기다. 사용자가 config.ts에 시작 URL·크롤링 범위·본문 셀렉터만 적어주면, 실제 브라우저(Chromium 등)를 띄워 그 사이트의 페이지들을 순서대로 열고, 지정한 부분의 텍스트만 뽑아 output.json 하나(또는 용량/토큰 한도에 따라 여러 개)로 저장한다.

에이전트도 아니고, 검색엔진도 아니다. AI 추론은 전혀 하지 않는다 — 순수하게 "브라우저 자동화 + 텍스트 추출 + 파일 분할"만 담당하는 파이프라인이고, 결과물을 실제로 "똑똑하게" 만드는 일은 그 JSON을 업로드받는 ChatGPT·Assistants API 쪽 몫이다. 이 역할 분리가 이 프로젝트를 이해하는 첫 번째 열쇠다.

한 줄 비유

도서관 전체를 발췘해 "한 권의 요약 노트"로 만들어주는 대필 작가

어떤 회사의 문서 사이트에 100페이지짜리 매뉴얼이 흩어져 있다고 하자. 사람이 매번 사이트를 뒤져 답을 찾는 대신, 이 100페이지를 미리 다 읽고 핵심만 한 권의 노트로 정리해 둔 대필 작가가 있다면 훨씬 빠르다.

gpt-crawler가 바로 그 대필 작가 역할이다. 브라우저로 페이지를 하나씩 "읽고"(크롤링), 광고·메뉴·푸터는 걷어내고 본문만 옮겨 적고(셀렉터 추출), 마지막엔 노트가 너무 두꺼워지지 않게 권 단위로 나눠 제본한다(토큰/용량 분할). 이 노트를 ChatGPT에게 건네주면 그때부터 ChatGPT가 그 노트를 읽고 질문에 답한다.

용어
커스텀 GPT (Custom GPT)
ChatGPT 위에서 사용자가 지시문·지식 파일을 얹어 만드는 "특화 챗봇". 예를 들어 "우리 회사 문서만 아는 GPT"를 만들 때, 그 문서를 담은 파일을 업로드하는 칸에 gpt-crawler의 출력물을 넣는다.
용어
헤드리스 브라우저 (headless browser)
화면(창)을 띄우지 않고 코드로 원격 조종되는 브라우저. 사람이 클릭하듯 페이지 이동·스크롤·요소 대기를 자동화할 수 있어, 단순 HTTP 요청으로는 못 읽는 자바스크립트 렌더링 사이트도 크롤링할 수 있다.

2왜 뜨는가 — 그리고 지금은 어떤 상태인가

2023년 커스텀 GPT 열풍과 함께 떴고, 지금은 "꾸준히 인용되는 참고 구현"으로 남아있다

이 저장소는 2023년 11월 OpenAI가 커스텀 GPT 기능을 발표한 직후 만들어졌다. "내 문서 사이트를 GPT에게 학습시키고 싶은데, 페이지를 일일이 복사-붙여넣기 하기는 너무 번거롭다"는 아주 구체적인 불편을 정확히 겨냥했고, README에는 Builder.io가 자사 개발 문서로 실제 커스텀 GPT를 만든 예시가 그대로 실려 있다. 짧고 명확한 문제 정의 덕에 "LLM 지식 파일 만들기"의 대표적인 참고 구현으로 자리 잡았다.

다만 정직하게 짚어야 할 부분이 있다. 이 문서 작성 시점(2026-07-29) 기준으로 마지막 커밋은 2025-07-07이고, package.json의 버전은 CHANGELOG상 2025-01-23의 1.5.1에서 멈춰 있다. 즉 "요즘 활발하게 기능이 추가되는 중"이라기보다는, 한 번 완성된 뒤 큰 변화 없이 안정적으로 참조되는 스테디셀러에 가깝다. "왜 지금 뜨는가"보다는 "왜 지금도 계속 언급되는가"로 질문을 바꿔 보는 게 정확하다.

① 문제 정의가 좁고 코드가 짧아 "학습용 표준 예제"로 오래 살아남았다

소스 코드가 src/ 아래 5개 파일, 수백 줄뿐이다. Crawlee·Playwright라는 잘 관리되는 라이브러리 위에 얇게 얹혀 있어서, 라이브러리 쪽이 계속 업데이트되는 한 이 얇은 래퍼 자체는 손댈 일이 적다. "짧아서 읽기 쉽고, 얇아서 안 망가지는" 코드가 시간이 지나도 계속 인용되는 이유다.

② RAG·에이전트 지식 수집 파이프라인의 "가장 쉬운 첫 예제"로 계속 소환된다

"웹사이트를 LLM 지식으로 바꾼다"는 요구는 커스텀 GPT를 넘어 RAG(검색 증강 생성) 파이프라인 전반에서 여전히 흔하다. gpt-crawler는 셀렉터·매치 패턴·토큰 분할이라는 이 문제의 핵심 개념을 가장 적은 코드로 보여주는 예제라서, 각종 블로그·강의·awesome 목록에서 두고두고 다시 소개된다.

③ 실행 방식이 다양해 상황별로 바로 써먹을 수 있다

CLI 대화형 실행, npm start 스크립트 실행, Express API 서버, Docker 컨테이너까지 네 가지 실행 경로를 다 갖췄다. "일단 한 번 돌려보고 싶다"부터 "다른 서비스에서 API로 호출하고 싶다"까지 진입 장벽이 낮다.

균형 잡힌 평가
"활발한 개발"이 아니라 "안정적인 참조"로 보는 게 맞다

이 레포는 화려한 신규 기능 추가보다는 고정된 문제를 잘 푸는 작은 도구로 존재 가치가 있다. 유지보수 활동이 적다고 해서 코드가 못 쓰게 된 건 아니다 — 다만 이슈·PR 응답이 느릴 수 있고, Crawlee·Playwright의 최신 기능을 이 레포가 곧바로 흡수하진 않는다는 점은 감안해야 한다. 아래 함정·오해 방지에서 구체적으로 다룬다.

3무엇을 해결하나

"여러 페이지 → 지식 파일" 변환의 세 가지 실무적 골칫거리

겉보기엔 "웹사이트 긁어와서 파일로 저장"이라는 단순한 일 같지만, 막상 손으로 하려면 세 가지 지점에서 막힌다. gpt-crawler는 정확히 이 세 가지에만 집중해서 해결한다.

① 페이지가 자바스크립트로 그려지면 단순 요청으론 못 읽는다

많은 문서 사이트는 React·Vue 같은 프레임워크로 페이지를 브라우저 안에서 그린다. 단순히 URL에 HTTP 요청을 보내 받은 HTML엔 실제 본문이 비어 있는 경우가 흔하다. gpt-crawler는 Crawlee의 PlaywrightCrawler로 진짜 브라우저를 띄워 렌더링이 끝난 뒤의 화면을 읽기 때문에 이 문제를 원천적으로 피한다.

② 본문만 골라내지 않으면 메뉴·광고·푸터가 지식을 오염시킨다

페이지 전체 텍스트를 그대로 넣으면 내비게이션 메뉴, 배너, 저작권 문구 같은 반복되는 잡음이 매 페이지마다 끼어들어 지식 파일의 신호 대 잡음비를 떨어뜨린다. gpt-crawler는 CSS 셀렉터(또는 XPath)로 "본문 컨테이너 하나"만 지정하게 해, 그 안의 텍스트만 뽑아낸다.

③ 커스텀 GPT·Assistants 업로드에는 파일 크기·토큰 한도가 있다

ChatGPT나 Assistants API의 지식 파일 업로드에는 파일 하나당 크기 제한이 있다. 사이트가 커서 결과 JSON이 너무 커지면 업로드 자체가 실패한다. gpt-crawler는 maxFileSize(메가바이트)·maxTokens(토큰 수) 설정으로 결과를 자동으로 여러 파일로 쪼개 이 문제를 피해간다.

비유

이사할 때 필요한 짐만 골라(셀렉터), 트럭 적재량에 맞춰 상자를 나눠 싣는(토큰/용량 분할) 과정과 비슷하다. 집 전체를 통째로 옮기려 하면(전체 텍스트) 트럭에 안 실리고(업로드 실패), 짐을 안 고르면(셀렉터 없음) 필요 없는 잡동사니까지 다 실려 정작 중요한 물건이 묻힌다.

4전체 구조

3개의 진입점(CLI·main·server)이 모두 core.ts의 crawl()+write()로 모인다

레포 구조는 아주 단순하다. "어떻게 실행되는가"를 결정하는 진입점 파일 3개(main.ts·cli.ts·server.ts)와, "실제로 무슨 일을 하는가"를 담당하는 엔진 파일 2개(config.ts·core.ts)로 나뉜다. 어느 진입점으로 들어오든 결국 core.ts의 crawl()과 write() 두 함수를 순서대로 호출하는 것으로 수렴한다.

┌──────────────────────────────────────────────────────────────┐ │ gpt-crawler 전체 구조 │ └──────────────────────────────────────────────────────────────┘ 진입점 3가지 (사용자가 고르는 실행 방식) ┌───────────┐ ┌───────────┐ ┌──────────────┐ │ main.ts │ │ cli.ts │ │ server.ts │ │ npm start │ │ 대화형 CLI │ │ Express API │ │ config.ts │ │ commander+ │ │ POST /crawl │ │ 값 그대로 │ │ inquirer │ │ 로 값 받음 │ └─────┬─────┘ └─────┬─────┘ └──────┬───────┘ │ │ │ └────────────────┼────────────────┘ ▼ ┌────────────────────┐ │ config.ts │ ← zod 스키마로 검증(configSchema) │ (url·match·selector│ │ ·maxPagesToCrawl 등)│ └──────────┬──────────┘ ▼ ┌────────────────────────────┐ │ core.ts │ │ crawl(config) → 브라우저로│ │ 페이지를 돌며 storage/에 │ │ JSON 조각 저장(pushData) │ │ │ │ write(config) → storage의 │ │ JSON 조각들을 읽어 토큰/용량│ │ 기준으로 합치고 쪼개 │ │ output-N.json 으로 저장 │ └──────────┬──────────────────┘ ▼ Crawlee PlaywrightCrawler (실제 브라우저 자동화 엔진)

이 구조가 주는 실용적인 교훈은 "진입점(실행 방식)과 엔진(크롤링 로직)을 분리"했다는 점이다. CLI로 쓰든 API 서버로 쓰든 실제 크롤링·분할 로직은 core.ts 한 곳에만 있으니, 이 파일 하나만 이해하면 이 프로젝트의 8할을 이해한 셈이다. 다음 절에서 그 안을 그대로 뜯어본다.

5핵심 설계·동작 원리

crawl() — 브라우저로 페이지를 돌며 긁기 / write() — 조각을 모아 토큰·용량 기준으로 쪼개기

src/core.ts 하나에 이 프로젝트의 실질적인 로직이 거의 다 들어있다. 크게 두 함수 — 크롤링을 실행하는 crawl()과 결과를 파일로 합치는 write() — 로 나뉜다. 대표 흐름 한 줄기를 처음부터 끝까지 손으로 따라가 보자.

① 크롤 시작 — 사이트맵인가, 단일 URL인가

crawl(config)는 가장 먼저 configSchema.parse(config)로 설정값을 검증한다(잘못된 타입이면 여기서 바로 에러). 그다음 config.url이 정규식 /sitemap.*\.xml$/에 걸리는지로 분기한다.

// src/core.ts 의 분기 로직 (설명을 위해 발췌·정리) const isUrlASitemap = /sitemap.*\.xml$/.test(config.url); if (isUrlASitemap) { // 사이트맵이면: 사이트맵에 나열된 URL 전체를 큐에 한꺼번에 등록 const listOfUrls = await downloadListOfUrls({ url: config.url }); await crawler.addRequests(listOfUrls); await crawler.run(); } else { // 일반 URL이면: 그 한 페이지만 큐에 넣고 시작 → 이후 링크를 따라가며 확장 await crawler.run([config.url]); }

즉 url 하나로 "단일 시작점 크롤"과 "사이트맵 일괄 등록"을 모두 지원한다. sitemap.xml을 주면 enqueueLinks 없이도 사이트 전체 URL 목록을 한 번에 큐에 넣을 수 있어, 링크 구조가 복잡한 사이트에 특히 유용하다.

② 페이지 하나를 방문할 때 — requestHandler

PlaywrightCrawler에 등록된 requestHandler가 방문한 페이지마다 실행된다. 순서대로: (1) 제목을 읽고 진행 로그를 찍는다 → (2) config.selector가 있으면 그 요소가 나타날 때까지 기다린다(waitForSelector 또는 XPath면 waitForXPath) → (3) getPageHtml()로 텍스트를 뽑는다 → (4) pushData()로 storage/datasets/default/에 JSON 조각 하나를 저장한다 → (5) config.onVisitPage 콜백이 있으면 실행한다 → (6) enqueueLinks()로 match 패턴에 맞는 링크를 다음 크롤 대상 큐에 추가한다.

용어
셀렉터 (selector) — CSS vs XPath
페이지 안에서 "이 부분만" 가리키는 주소. CSS 셀렉터(예: .docs-container)는 클래스·태그로, XPath(예: //div[@id='content'], "/"로 시작)는 문서 트리 경로로 요소를 지정한다. gpt-crawler는 문자열이 "/"로 시작하면 자동으로 XPath로, 아니면 CSS로 처리한다.

여기서 짚고 넘어가야 할 세부 하나. getPageHtml()은 이름과 달리 실제로는 HTML 마크업이 아니라 순수 텍스트를 반환한다.

// src/core.ts
export function getPageHtml(page: Page, selector = "body") {
  return page.evaluate((selector) => {
    if (selector.startsWith("/")) {
      // XPath: 일치하는 첫 요소의 textContent(태그 없는 순수 텍스트)
      const elements = document.evaluate(selector, document, null, XPathResult.ANY_TYPE, null);
      let result = elements.iterateNext();
      return result ? result.textContent || "" : "";
    } else {
      // CSS: 일치하는 첫 요소의 innerText(화면에 보이는 대로의 텍스트)
      const el = document.querySelector(selector) as HTMLElement | null;
      return el?.innerText || "";
    }
  }, selector);
}

결과 JSON에는 이 값이 html이라는 필드 이름으로 저장된다(pushData({ title, url, html })). 필드 이름은 "html"인데 내용물은 텍스트라는 이 불일치는 코드를 처음 읽는 사람이 가장 자주 헷갈리는 지점이라 함정·오해 방지에서 다시 짚는다.

③ 크롤링 범위 제어 — match / exclude / resourceExclusions / cookie

enqueueLinks({ globs: config.match, exclude: config.exclude ?? [] })가 "어디까지 따라갈지"를 결정한다. match는 glob 패턴(예: "https://example.com/docs/**") 문자열 하나 또는 배열을 받는다. exclude는 그 반대로, 매칭되더라도 크롤 대상에서 뺄 패턴이다.

preNavigationHooks(페이지 이동 전에 실행되는 훅)에서 두 가지가 더 일어난다. 하나는 config.cookie가 있으면 그 값을 페이지 컨텍스트에 쿠키로 심는 것(쿠키 동의 배너를 미리 넘기는 용도로 흔히 쓴다). 다른 하나는 config.resourceExclusions(이미지·폰트·비디오 등 확장자 목록)가 있으면 그 확장자로 끝나는 요청을 page.route(...).abort("aborted")로 아예 막아버리는 것 — 크롤링 속도를 올리고 대역폭을 아끼는 최적화다.

비유

match/exclude는 "이 구역 안에서만, 이 골목은 빼고" 순찰 경로를 정해주는 것이고, resourceExclusions는 "사진첩·짐은 놔두고 문서만 챙겨오라"

④ 결과 합치기 — write()의 토큰·용량 기반 자동 분할

크롤이 끝나면 storage/datasets/default/*.json에 페이지 수만큼 조각 파일이 쌓여 있다. write(config)는 이 조각들을 순서대로 읽어 하나의 결과에 밀어 넣다가, 한도를 넘는 순간 파일을 잘라 다음 파일로 넘어가는 누적 알고리즘을 돌린다. 실제 로직은 이렇다.

// src/core.ts write() 안의 addContentOrSplit (설명을 위해 정리)
const contentString = JSON.stringify(data);
const tokenCount = isWithinTokenLimit(contentString, config.maxTokens || Infinity);
// tokenCount: 이 항목 하나의 토큰 수(숫자) — 단, 이 항목 자체가 이미
// maxTokens를 넘으면 숫자 대신 false 가 돌아온다

if (typeof tokenCount === "number") {
  if (estimatedTokens + tokenCount > config.maxTokens) {
    if (currentResults.length > 0) await writeBatchToFile(); // 지금까지 모은 걸 파일로 확정
    estimatedTokens = Math.floor(tokenCount / 2); // 다음 파일의 시작 추정치(근사치)
    currentResults.push(data);
  } else {
    currentResults.push(data);
    estimatedTokens += tokenCount;
  }
}
// tokenCount 가 false 였다면 위 if 블록에 아예 들어가지 않는다
// → 이 항목은 currentResults 에 추가되지 않고 그대로 버려진다

currentSize += getStringByteSize(contentString);
if (currentSize > maxBytes) await writeBatchToFile(); // 용량 기준 분할(별도 트리거)

풀어 말하면: 페이지 하나씩을 토큰 누적치와 바이트 누적치 두 자로 동시에 재면서, 둘 중 하나라도 한도(maxTokens / maxFileSize)를 넘는 순간 지금까지 모은 걸 output-1.json, output-2.json… 식으로 잘라 저장한다. maxTokens·maxFileSize를 아예 지정하지 않으면 두 한도 모두 사실상 무한대라 파일 하나(output-1.json)로만 끝난다.

코드에서 실제로 확인한 함정
maxTokens를 넘는 "한 페이지"는 결과에서 조용히 사라진다

만약 어떤 한 페이지의 본문 자체가 maxTokens 한도보다 크면, isWithinTokenLimit은 숫자가 아니라 false를 돌려준다. 코드는 이 경우를 if (typeof tokenCount === "number") 블록 밖에 두기 때문에, 그 페이지는 어떤 경고도 없이 그냥 결과 파일에서 빠진다.

큰 API 레퍼런스 페이지나 코드 예제가 잔뜩 있는 문서를 크롤링할 때 특히 주의해야 한다. 크롤 자체는 "성공"으로 끝나지만, 정작 가장 중요한 그 페이지가 빠진 채로 output.json이 만들어질 수 있다.

⑤ GPTCrawlerCore — API 서버가 쓰는 얇은 클래스 래퍼

core.ts 맨 아래엔 crawl()·write() 두 함수를 this.config로 감싼 GPTCrawlerCore 클래스가 있다. server.ts는 이 클래스 인스턴스를 만들어 crawler.crawl() → crawler.write() 순서로 호출한다. 함수형으로 짠 로직을 API 핸들러에서 쓰기 좋게 클래스 인스턴스 하나로 감싼, 아주 얇은 어댑터일 뿐이다.

용어
Crawlee / PlaywrightCrawler
Crawlee는 Apify가 만든 Node.js 크롤링·스크래핑 프레임워크로, 큐 관리·재시도·동시성 제어·결과 저장을 대신 해준다. PlaywrightCrawler는 그중에서도 Playwright(브라우저 자동화 라이브러리)로 실제 브라우저를 띄워 페이지를 여는 크롤러 구현체다. gpt-crawler는 이 두 라이브러리를 그대로 가져다 쓰고, 그 위에 셀렉터 추출·파일 분할만 얹었다.

6기술 스택·의존성

Crawlee + Playwright가 엔진, 나머지는 진입점(CLI/Express)을 위한 얇은 도구들

버전의 진실원(source of truth)은 package.json의 선언값이 아니라 package-lock.json에 실제로 고정된 설치 버전이다. 아래 표의 버전은 이 저장소를 그대로 클론했을 때 package-lock.json에 기록된 실측값이다.

구성실제 설치 버전 (lockfile)역할
crawlee3.6.1 (선언 ^3.0.0)크롤링 프레임워크 — 큐·재시도·저장소 관리
playwright1.40.0 (선언 * — 와일드카드)실제 브라우저 자동화 엔진(Chromium 등)
gpt-tokenizer2.1.2OpenAI 토크나이저로 텍스트의 토큰 수 계산(isWithinTokenLimit)
zod3.22.4config.ts의 설정 스키마 정의·런타임 검증
express4.18.2server.ts의 API 서버 프레임워크
commander11.1.0cli.ts의 커맨드라인 인자 파서
inquirer9.2.xCLI에서 값이 비어 있을 때 대화형으로 되묻는 프롬프트
glob10.3.xwrite()에서 storage/datasets/default/*.json 조각 파일 목록 찾기
swagger-ui-express / swagger-autogen5.0.x / 2.23.xAPI 서버의 /api-docs 문서 자동 생성
TypeScript5.x (devDependency)전체 소스가 TypeScript. 빌드는 tsc로 dist/에 컴파일
용어
zod
TypeScript용 스키마 검증 라이브러리. "이 값은 문자열이어야 한다", "이 필드는 선택값이다" 같은 규칙을 코드로 적어두면, 실행 중에 실제 값이 그 규칙을 어기는지 검사해준다. gpt-crawler는 config.ts의 configSchema로 크롤 설정을 검증한다.
주의
playwright 의존성이 "*"(와일드카드)로 선언돼 있다

package.json에는 "playwright": "*"라고 적혀 있어, 이론적으로는 어떤 버전이든 허용한다. 실제로는 package-lock.json이 1.40.0으로 고정하고 있어 npm ci를 쓰면 안전하지만, lockfile 없이 npm install만 새로 돌리는 환경에서는 예상보다 최신(혹은 호환되지 않는) Playwright가 깔릴 수 있다.

7설치·요구사항

Node.js + Playwright 브라우저 다운로드가 필수 — Docker로 통째로 감쌀 수도 있다
항목요구사항
Node.jsREADME 기준 16 이상. CI·Docker 이미지는 실제로 18을 사용(.github/workflows, Dockerfile)
브라우저 바이너리npm install 시 preinstall 훅이 npx playwright install을 자동 실행 — Chromium 등 브라우저 실행 파일을 별도로 내려받는다(수백 MB, 인터넷 필요)
로컬 실행git clone → npm i → config.ts 수정 → npm start(내부적으로 tsc 빌드 후 dist/src/main.js 실행)
API 서버 모드npm run start:server — 기본 포트 3000(코드 기본값). .env로 API_PORT·API_HOST 재정의 가능(.env.example은 5000 예시)
Docker(기본)베이스 이미지 apify/actor-node-playwright-chrome:18 — Playwright·Chrome이 이미 설치된 이미지 위에 빌드하는 멀티스테이지 Dockerfile
Docker(containerapp/)Ubuntu 기반 이미지 안에 Docker까지 함께 설치하는 "완전 격리 실행" 스크립트 세트. run.sh가 이미지를 빌드하고 data/ 폴더를 마운트해 config.ts 주입·output.json 회수를 자동화
가벼운 편
특별한 하드웨어 요구사항은 없다

GPU도, 대용량 메모리도 필요 없다. 유일한 무게감은 Playwright가 내려받는 브라우저 바이너리와, 크롤링할 사이트 규모(페이지 수 × 브라우저 렌더링 시간)에서 온다. 개인 노트북에서도 문제없이 돌아가는 수준이다.

8실제 사용 흐름

config.ts 편집 → npm start → output.json → ChatGPT/Assistants 업로드
흐름 A — 가장 흔한 로컬 실행

config.ts를 고쳐서 한 번 돌리고 끝내는 방식

1) git clone https://github.com/builderio/gpt-crawler → 2) npm i(이때 Playwright 브라우저도 함께 설치됨) → 3) 루트의 config.ts를 열어 url·match·selector·maxPagesToCrawl·outputFileName을 자신의 사이트에 맞게 수정 → 4) npm start 실행. 내부적으로는 main.ts가 defaultConfig를 그대로 읽어 crawl() → write() 순서로 실행하고, 루트에 output-1.json(또는 분할 시 -2, -3…)이 생성된다.

흐름 B — 값을 코드에 안 적고 그때그때 물어보게

대화형 CLI (npm run start:cli)

npm run start:cli로 실행하면 cli.ts가 실행된다. --url·--match·--selector·--maxPagesToCrawl·--outputFileName 플래그로 값을 바로 넘길 수 있고, 필수값(url·match·selector)이 비어 있으면 inquirer가 터미널에서 하나씩 되물어 채워준다. config.ts를 편집하지 않고 반복 실행할 때 편하다.

흐름 C — 다른 서비스에서 API로 호출

Express API 서버 (npm run start:server)

npm run start:server로 서버를 띄운 뒤, POST /crawl에 config JSON을 body로 보내면 그 자리에서 크롤·분할까지 마친 결과 파일 내용을 응답으로 돌려받는다. /api-docs에서 Swagger로 API 명세를 바로 확인할 수 있다. 자체 서비스의 백엔드에서 "필요할 때마다 크롤을 트리거"하고 싶을 때 이 방식을 쓴다.

흐름 D — 완전히 격리된 환경에서 실행

Docker / containerapp

기본 Dockerfile로 이미지를 빌드해 컨테이너 하나로 실행하거나, containerapp/ 폴더의 run.sh를 실행하면 아예 처음부터 Ubuntu + Docker + Node를 갖춘 이미지를 새로 빌드해, data/config.ts를 컨테이너 안에 복사해 넣고 크롤을 돌린 뒤 data/output.json으로 결과를 돌려준다. 로컬 Node.js 환경을 건드리고 싶지 않을 때 쓰는 방식이다.

마지막 — 결과를 실제로 사용하기

output.json을 ChatGPT/Assistants에 업로드

ChatGPT의 "My GPTs → Create a GPT → Configure → Knowledge"에서 파일을 업로드하는 게 기본 경로다. OpenAI Assistants API는 2026-08-26에 종료가 확정돼 있어(2026-08-22 기준) 새로 올릴 곳으로는 권하지 않는다 — 후속은 Responses API다. 파일이 너무 크다는 오류가 나면 그제서야 되돌아가 config.ts에 maxFileSize나 maxTokens를 추가해 다시 크롤하는 게 README가 권하는 순서다.

9함정·오해 방지

코드를 직접 읽고 실행해 확인한, 문서만 봐서는 알기 어려운 지점들

① CLI의 -m 단축 플래그가 두 옵션에 겹쳐 있다 (실제로 재현 확인됨)

cli.ts는 -m, --match와 -m, --maxPagesToCrawl을 둘 다 -m으로 등록한다. commander는 이 경우 에러를 내지 않고, 먼저 등록된 --match가 -m 단축 플래그를 그대로 가져간다. 즉 gpt-crawler -m 30처럼 짧게 쓰면 최대 페이지 수가 아니라 match 값이 "30"으로 설정된다. 실제로 이 저장소의 commander@11.1.0으로 재현해 확인한 동작이다. 페이지 수를 지정하려면 반드시 긴 플래그 --maxPagesToCrawl을 써야 한다.

② 필드 이름은 "html"인데 실제로는 텍스트다

앞서 핵심 설계·동작 원리에서 봤듯, 결과 JSON의 html 필드에는 innerText/textContent(순수 텍스트)가 들어간다. 원본 마크업·링크·이미지 태그는 전혀 남지 않는다. "html이라 적혀 있으니 태그도 있겠지" 하고 후처리 코드를 짜면 곧바로 어긋난다.

③ README의 설정 타입 예시가 실제 스키마보다 적다

README에 소개된 Config 타입 예시에는 url·match·selector·maxPagesToCrawl·outputFileName·resourceExclusions·maxFileSize·maxTokens만 나온다. 그러나 실제 src/config.ts에는 cookie(쿠키 주입), onVisitPage(페이지마다 실행할 커스텀 콜백), waitForSelectorTimeout까지 더 있다. README만 읽으면 이 세 옵션의 존재 자체를 모르고 지나치기 쉽다.

법적·윤리적 주의
robots.txt를 자동으로 지키지 않고, 요청 속도 제한도 기본으로는 없다

코드 어디에도 robots.txt를 확인하거나 요청 사이 지연을 강제하는 로직이 없다. 즉 "이 크롤러를 쓰면 자동으로 예의 바르게 긁는다"는 보장이 없다. 대상 사이트의 이용 약관·robots.txt를 사람이 직접 확인하고, 필요하면 maxPagesToCrawl을 보수적으로 잡거나 크롤 대상 사이트 소유자의 허락을 받아야 한다. 저작권이 있는 콘텐츠를 그대로 지식 파일로 만들어 배포하는 것도 별개의 법적 쟁점이다.

④ "테스트 통과"가 실제 검증을 의미하지 않는다

package.json의 test 스크립트는 echo "No test specified, please create tests." && exit 0다. PR CI(pr.yml)가 npm run test를 돌리지만, 이건 항상 성공하는 빈 스크립트라 실제 자동화된 테스트는 없다. CI가 초록불이어도 크롤링 로직 자체가 검증됐다는 뜻은 아니다.

⑤ 기타 자잘한 불일치

package.json에는 express-fileupload가 의존성으로 선언돼 있지만 src/ 어디에서도 실제로 쓰이지 않는다(죽은 의존성). 라이선스는 "ISC"로, MIT과 성격은 비슷하지만(둘 다 매우 관대한 허용 라이선스) 정확히는 별개 라이선스명이니 사내 라이선스 검수 시 "ISC"라고 정확히 표기해야 한다.

10대안 비교

Firecrawl · Crawl4AI · Crawlee를 직접 쓰기 — "완성품 vs 재료"의 스펙트럼

gpt-crawler는 "셀렉터 + 토큰 분할"이라는 아주 좁은 문제만 푼다. 비슷한 목적을 가진 다른 선택지와 견주면 이 프로젝트의 위치가 더 또렷해진다.

도구성격gpt-crawler와 다른 점
gpt-crawler셀프호스트, 셀렉터 기반 소형 크롤러서버 없이 로컬에서 바로 실행. 설정 3~4줄로 시작하지만, 페이지 구조가 바뀌면 셀렉터를 직접 다시 찾아야 한다
Firecrawl크롤링 결과를 마크다운/구조화 데이터로 자동 정제해주는 API·오픈소스 서비스셀렉터를 직접 지정하지 않아도 "본문 추출"을 어느 정도 자동화해준다. 대신 호스팅형 사용 시 API 키·과금이 따르고, 셀프호스트도 별도 인프라가 필요하다
Crawl4AILLM/RAG 파이프라인을 겨냥한 파이썬 크롤링 라이브러리파이썬 생태계에 있고, 마크다운 변환·청크 분할·비동기 크롤 등 RAG에 특화된 기능이 더 풍부하다. 대신 러닝 커브가 gpt-crawler보다 높다
Crawlee를 직접 쓰기gpt-crawler가 내부에서 쓰는 프레임워크 그 자체셀렉터 추출·토큰 분할 로직을 스스로 짜야 하지만, 그만큼 크롤 큐·재시도·동시성·다양한 크롤러 타입(HTTP/Cheerio/Playwright)을 훨씬 세밀하게 제어할 수 있다
선택 기준
"바로 실행되는 완성품"이 필요하면 gpt-crawler, "맞춤 제어"가 필요하면 Crawlee

문서 사이트 하나를 빠르게 커스텀 GPT 지식으로 바꾸고 끝낼 거라면 gpt-crawler의 단순함이 유리하다. 반대로 여러 사이트를 정기적으로 재크롤링하거나, 마크다운 변환·중복 제거·임베딩까지 파이프라인을 직접 짜야 한다면 Firecrawl·Crawl4AI처럼 RAG를 더 염두에 둔 도구나, Crawlee를 직접 다루는 편이 장기적으로 낫다.

11정리 — 누가 쓰면 좋은가

"한 문서 사이트를 GPT 지식으로" 빠르게, 그리고 코드로 크롤링 파이프라인을 배우고 싶을 때

gpt-crawler를 한 문장으로 다시 요약하면: "Crawlee의 PlaywrightCrawler를 셀렉터 추출 + 토큰/용량 자동 분할이라는 두 겹의 얇은 로직으로 감싼, 커스텀 GPT 지식 파일 생성 전용 도구"다. AI 추론은 하지 않고, 그 대신 "사이트를 읽어서 파일로 만드는" 앞단 파이프라인 한 조각을 아주 잘 해낸다.

이런 사람에게 잘 맞는다

· 회사·개인 문서 사이트 하나를 커스텀 GPT 지식으로 빠르게 옮기고 싶은 사람(Assistants API는 2026-08-26 종료)
· "웹 크롤링 → 셀렉터 추출 → 토큰 기준 파일 분할"이라는 패턴을 짧은 실제 코드로 배우고 싶은 초보 개발자
· Crawlee·Playwright를 처음 접하는데, 아주 얇은 실전 예제부터 읽고 싶은 사람

이런 상황엔 다른 도구를 먼저 보는 게 낫다

· 여러 사이트를 정기적으로 재크롤링하며 자동으로 최신 상태를 유지해야 하는 프로덕션 RAG 파이프라인 → Firecrawl·Crawl4AI 같은 RAG 특화 도구
· 크롤 큐·동시성·재시도 정책을 세밀하게 제어해야 하는 대규모 크롤 → Crawlee를 직접
· 대상 사이트가 로그인·CAPTCHA·강한 봇 차단을 쓰는 경우 → 이 저장소는 그런 회피 로직을 전혀 다루지 않는다

마지막으로, 함정·오해 방지에서 짚었듯 이 도구는 크롤링의 예의(robots.txt·속도 제한)를 대신 챙겨주지 않는다. 짧고 유용한 코드인 만큼, 그 짧음이 어디까지 책임지고 어디부터는 사용자가 직접 판단해야 하는지를 분명히 알고 쓰는 것이 이 도구를 안전하게 쓰는 법이다.

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

설정 검증부터 텍스트 추출·파일 재처리·누락 재현까지, 네 단계로 엔진을 이해한다

아래 과제는 Node.js 의존성과 Playwright 브라우저가 이미 준비된 별도 학습용 복사본을 기준으로 한다. 명령은 레포 루트에서 실행하며, 외부 사이트 대신 메모리에 만든 문서와 임시 폴더의 JSON을 사용한다. 예제 폴더에 의존하지 않고, 실제로 공개된 함수와 실행 스크립트를 직접 다룬다.

과제 1 · 입문

설정 파일과 검증 스키마의 역할 구분하기

목표: 루트의 config.ts는 실행할 값, src/config.ts는 허용할 값의 규칙이라는 차이를 이해한다.

  1. package.json의 build 스크립트와 tsconfig.json의 outDir을 읽고, 빌드 후 생성된 CLI의 도움말을 확인한다.
npm run build
node dist/src/cli.js --help
  1. src/main.ts에서 defaultConfig가 crawl()과 write()로 전달되는 순서를 따라간다.
  2. 아래 명령으로 정상 설정과 잘못된 페이지 수를 비교한다. 크롤링 함수는 호출하지 않는다.
node --input-type=module -e '
import { strict as assert } from "node:assert";
import { configSchema } from "./dist/src/config.js";

const config = {
  url: "about:blank",
  match: "**",
  maxPagesToCrawl: 1,
  outputFileName: "lab.json"
};

assert.doesNotThrow(() => configSchema.parse(config));
assert.throws(() => configSchema.parse({
  ...config,
  maxPagesToCrawl: 0
}));
console.log("정상 설정 통과, 페이지 수 0 거부");
'

완료 기준: 두 검증이 모두 통과하고, maxPagesToCrawl이 양의 정수여야 하는 근거를 스키마에서 찾는다. CLI 도움말에서는 --match와 --maxPagesToCrawl을 구분하고, 겹치는 단축 플래그 대신 긴 이름을 사용한다.

과제 2 · 중급

같은 문서를 CSS와 XPath로 읽어 비교하기

목표: src/core.ts의 getPageHtml()이 선택 범위와 추출 방식에 따라 다른 텍스트를 돌려준다는 점을 확인한다.

  1. 과제 1의 빌드를 마친 뒤 아래 명령을 실행한다. 브라우저에는 문자열로 만든 문서를 넣고, 페이지의 네트워크 요청은 모두 차단한다.
  2. 기본 선택 범위, article, //article의 결과를 비교한다. 예제의 hidden 속성은 숨겨진 텍스트의 차이를 드러내기 위한 장치다.
node --input-type=module -e '
import { strict as assert } from "node:assert";
import { chromium } from "playwright";
import { getPageHtml } from "./dist/src/core.js";

const browser = await chromium.launch();
try {
  const page = await browser.newPage();
  await page.route("**/*", route => route.abort());
  await page.setContent(
    "<nav>메뉴</nav><article>본문<span hidden>숨김</span></article>"
  );

  const whole = await getPageHtml(page);
  const css = await getPageHtml(page, "article");
  const xpath = await getPageHtml(page, "//article");

  assert.ok(whole.includes("메뉴"));
  assert.equal(css.trim(), "본문");
  assert.equal(xpath, "본문숨김");
  console.log({ whole, css, xpath });
} finally {
  await browser.close();
}
'

완료 기준: 기본 범위에는 메뉴가 포함되고, CSS 결과에서는 숨겨진 글자가 빠지며, XPath 결과에는 포함된다. 이를 innerText와 textContent의 차이로 설명하고, 결과에 원본 태그가 남지 않는 것도 확인한다.

과제 3 · 중급

크롤링 없이 JSON 조각을 결과 파일로 합치기

목표: 수집 단계와 저장 단계를 분리해 write()의 입력 경로와 출력 이름을 확인한다.

  1. src/core.ts에서 NO_CRAWL 조건과 storage/datasets/default/*.json 검색 부분을 읽는다.
  2. 아래 명령으로 임시 작업 폴더에 페이지 조각 두 개를 만든다. process.chdir() 이후의 상대 경로는 그 임시 폴더를 기준으로 하므로 기존 수집 데이터와 섞이지 않는다.
  3. NO_CRAWL=true로 실제 방문을 건너뛰고, 반환된 파일을 다시 읽어 입력과 출력의 URL 목록을 비교한다.
NO_CRAWL=true node --input-type=module -e '
import { strict as assert } from "node:assert";
import { mkdtemp, mkdir, writeFile, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { crawl, write } from "./dist/src/core.js";

const dir = await mkdtemp(join(tmpdir(), "gpt-crawler-lab-"));
process.chdir(dir);
await mkdir("storage/datasets/default", { recursive: true });

const pages = [
  { title: "첫 문서", url: "about:blank#one", html: "짧은 설명." },
  { title: "둘째 문서", url: "about:blank#two", html: "추가 설명." }
];
for (const [i, page] of pages.entries()) {
  await writeFile(
    "storage/datasets/default/" + (i + 1) + ".json",
    JSON.stringify(page)
  );
}

const config = {
  url: "about:blank",
  match: "**",
  maxPagesToCrawl: 2,
  outputFileName: "lab.json"
};
await crawl(config);
const path = await write(config);
const rows = JSON.parse(await readFile(path, "utf8"));

assert.equal(path, "lab-1.json");
assert.deepEqual(
  rows.map(row => row.url).sort(),
  pages.map(page => page.url).sort()
);
console.log(dir, path, rows.length);
'

완료 기준: 출력된 임시 폴더에 lab-1.json이 생성되고 두 문서의 URL이 모두 보존된다. outputFileName을 lab.json으로 지정해도 실제 파일에는 번호가 붙는 이유를 nextFileName()에서 찾는다.

과제 4 · 심화

토큰 한도 초과로 문서가 빠지는 조건 재현하기

목표: 실행 성공과 데이터 보존을 별도로 검증하는 방법을 익힌다. 과제 3의 명령을 바탕으로 같은 임시 폴더 생성 절차를 매번 새로 수행한다.

  1. 과제 3 코드에서 둘째 문서의 html을 "설명 ".repeat(1000)으로 바꾸고, config에 maxTokens: 64를 추가한다. 이 값은 누락 조건을 관찰하기 위한 실습용 한도다.
  2. 다음 import를 코드 맨 위에 추가한다.
import { isWithinTokenLimit } from "gpt-tokenizer";
  1. await crawl(config) 앞에 아래 검증을 넣는다. 첫 문서는 한도 안에 있고 둘째 문서는 한도를 넘는 입력인지 먼저 확인한다.
assert.equal(
  typeof isWithinTokenLimit(JSON.stringify(pages[0]), config.maxTokens),
  "number"
);
assert.equal(
  isWithinTokenLimit(JSON.stringify(pages[1]), config.maxTokens),
  false
);
  1. 과제 3의 URL 목록 비교 검증을 아래 코드로 바꾼 뒤, 같은 NO_CRAWL=true node --input-type=module -e 방식으로 전체 코드를 다시 실행한다.
assert.equal(rows.length, 1);
assert.equal(rows[0].url, pages[0].url);
console.log("누락 재현:", pages[1].url);

완료 기준: 입력은 두 문서인데 출력에는 짧은 문서 하나만 남는 조건을 재현하고, addContentOrSplit()에서 false를 받은 항목이 추가되지 않는 분기를 찾는다. 이어서 “한도 초과 항목을 나누거나 오류로 알린다”는 개선 규칙과, 입력 URL이 설명 없이 사라지면 실패하는 회귀 테스트의 조건을 적는다.

검증 습관
파일 수보다 입력과 출력의 대응 관계를 확인한다

npm run test는 이 저장소에서 실제 검증을 하지 않는 빈 스크립트다. 실습에서는 직접 작성한 단언문으로 문서 수·URL·본문을 확인하고, 파일을 여러 개 만들었다면 전체 결과를 합쳐 입력과 비교한다.

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

5주 동안 설정 → 브라우저 → 크롤 큐 → 파일 처리 → 실행 환경으로 범위를 넓힌다

처음부터 모든 실행 방식을 켜기보다, 한 주에 한 경계를 읽고 설명 가능한 결과물을 남긴다. 앞의 실습은 외부 사이트에 접속하지 않고 동작을 관찰하는 출발점으로 삼는다.

주차배울 개념이 레포에서 읽을 곳남길 결과물
1주차 TypeScript 타입과 실행 중 검증의 차이, 모듈 가져오기, 빌드 결과의 위치 package.json, tsconfig.json, 루트 config.ts, src/config.ts, src/main.ts 설정값이 검증·수집·저장으로 흐르는 그림과 과제 1의 정상·오류 입력 비교표
2주차 문서 트리, CSS·XPath, 브라우저 안에서 실행되는 코드와 Node.js 코드의 경계 src/core.ts의 getPageHtml()·waitForXPath()·셀렉터 대기 부분 과제 2의 추출 결과표. 선택 요소가 없을 때 직접 추출 함수와 크롤러의 대기 단계가 어떻게 다른지도 설명
3주차 요청 큐, 링크 발견, 이동 전 훅, 페이지 처리 콜백의 실행 순서 src/core.ts의 crawl()·requestHandler·preNavigationHooks, src/config.ts match·exclude와 resourceExclusions의 역할 비교표. 기본 데이터 저장과 onVisitPage 호출 순서를 표시한 흐름도
4주차 비동기 파일 처리, JSON 직렬화, 토큰 수와 바이트 수, 경계 조건과 데이터 보존 src/core.ts의 write()·addContentOrSplit()·writeBatchToFile() 과제 3·4의 입력·출력 URL 비교 기록과 한도 초과 항목 처리 개선안. 완성된 JSON 파일의 크기도 별도로 확인
5주차 CLI와 HTTP 진입점, 오류 전달, 컨테이너 실행, CI가 확인하는 범위 src/cli.ts, src/server.ts, swagger.js, Dockerfile, containerapp/, .github/workflows/pr.yml 실행 방식별 설정 입력·출력 경로·오류 처리 비교표와 실제 회귀 테스트 제안. API가 분할 결과 중 어떤 파일을 읽는지도 추적
마지막 점검

src/server.ts는 write()가 돌려준 경로 하나를 읽어 응답하고, write()는 마지막으로 저장한 파일 경로를 반환한다. 여러 파일이 생겼을 때 모든 문서를 API로 전달하려면 반환 형식을 어떻게 바꿔야 할지 설명해 보면, 함수 내부와 서비스 경계를 함께 이해했는지 확인할 수 있다.

14핵심 키워드 사전

설정·추출·수집·저장을 잇는 용어 15개를 실제 코드 위치와 연결한다
설정 스키마 · Zod
프로그램이 받아도 되는 값의 모양과 조건을 적은 규칙이다. 이 레포에서는 src/config.ts의 configSchema가 페이지 수의 양의 정수 조건 등을 정의하고, crawl()이 실행 전에 parse()로 확인한다.
문서 트리 · DOM
브라우저가 페이지의 요소와 글자를 부모·자식 관계로 구성한 구조다. getPageHtml()은 page.evaluate() 안에서 이 구조를 조회해 필요한 텍스트를 꺼낸다.
CSS 셀렉터
태그나 클래스 같은 특징으로 페이지의 특정 요소를 가리키는 표현이다. 이 레포의 CSS 추출 경로는 document.querySelector()를 쓰므로 일치하는 첫 요소의 텍스트를 읽는다.
XPath
문서 트리의 경로와 조건으로 요소를 찾는 표현이다. src/core.ts는 셀렉터가 /로 시작하면 XPath로 판단하고, document.evaluate()의 첫 결과를 읽는다.
innerText · textContent
innerText는 화면에 표시되는 상태를 반영한 글자를, textContent는 숨김 여부와 관계없이 요소 안의 글자를 읽는다. 이 레포는 CSS 경로에서 전자를, XPath 경로에서 후자를 사용하며 결과를 html 필드에 담는다.
헤드리스 브라우저
눈에 보이는 창 없이 코드로 조작하는 브라우저다. 이 레포에서는 Playwright를 사용하는 크롤러가 페이지를 열고, 자바스크립트로 만들어진 문서에서도 요소를 기다리며 내용을 추출한다.
Crawlee · PlaywrightCrawler
Crawlee는 크롤링에 필요한 요청 관리와 결과 저장 등을 맡는 프레임워크이며, PlaywrightCrawler는 그 안의 브라우저 기반 크롤러다. src/core.ts는 여기에 페이지 처리 함수와 최대 요청 수를 설정한다.
요청 처리 함수 · requestHandler
방문한 페이지마다 수행할 일을 모아 둔 함수다. 이 레포에서는 제목 읽기, 요소 대기, 본문 추출, 데이터 저장, 사용자 콜백, 다음 링크 등록 순서로 동작한다.
요청 큐 · enqueueLinks
앞으로 방문할 주소를 모아 처리하는 대기 목록이다. src/core.ts의 enqueueLinks()는 현재 페이지에서 발견한 링크에 match와 exclude를 적용해 다음 방문 후보를 등록한다.
glob 패턴
별표 같은 문자를 써서 여러 경로나 주소를 한 번에 표현하는 규칙이다. 이 레포에서는 크롤 대상 링크를 고를 때와 storage/datasets/default/*.json 파일 목록을 찾을 때 사용한다.
데이터셋 · Dataset
수집한 결과를 항목별로 쌓아 두는 저장소다. 이 레포는 pushData()로 제목·URL·본문을 저장하고, write()가 로컬 데이터셋의 JSON 조각을 읽어 최종 파일로 합친다.
콜백 · onVisitPage
다른 코드가 정해진 시점에 호출하도록 넘겨 두는 함수다. 이 레포의 onVisitPage는 기본 데이터 저장 뒤에 호출되고 page와 pushData를 받아 추가 처리를 할 수 있다.
토큰 · gpt-tokenizer
모델이 글을 처리할 때 사용하는 단위로, 글자 수나 파일 바이트 수와 같지 않다. write()는 gpt-tokenizer의 isWithinTokenLimit()로 JSON 항목을 검사하며, 한도 초과 시 돌아오는 false를 숫자와 구별해야 한다.
UTF-8 바이트 수
글자를 UTF-8로 저장했을 때 차지하는 실제 데이터 크기이며 한 글자가 항상 한 바이트인 것은 아니다. 이 레포는 Buffer.byteLength()로 항목 크기를 재고, maxFileSize에 1024 × 1024를 곱한 값과 비교한다.
검색 증강 생성 · RAG
질문에 필요한 자료를 먼저 찾아 모델에 제공한 뒤 답변을 만드는 방식이다. 이 레포는 그 앞단의 웹 문서 수집과 파일 생성을 담당하며, 검색·임베딩·답변 생성 자체는 구현하지 않는다.