StreamPDFStreamPDF
블로그로 돌아가기

데모로 파헤치는 PDF.js 2: getDocument 메서드의 전체 모습

2026년 10월 27일 · StreamPDF Team

데모로 파헤치는 PDF.js 2: getDocument 메서드의 전체 모습

들어가며

이전 글의 데모는 getDocument를 호출할 때 { url: '...' }만 전달합니다. 이번 글에서는 이 호출에서 출발해 getDocument의 실행 체인을 끝까지 따라갑니다.

getDocument 호출

소스 코드의 형식 매개변수 이름은 src이고, 데모가 실제로 전달하는 키는 하나뿐입니다:

src의 키값
url'https://assets-blog.streampdf.com/PDF32000_2008.pdf'

display/api.js의 getDocument 정의에 따르면 코드 실행 흐름은 다음과 같습니다.

235~236행: loading 태스크 초기화

PDFDocumentLoadingTask 클래스를 인스턴스화하고: 소스 살펴보기, 반환값을 task 변수에 할당합니다.

task에서 docId 변수를 꺼냅니다. 이 시점에서 docId는 d0입니다.

238~335행: 파라미터 검증 및 처리

이 구간은 사용자가 전달한 src 객체를 35개 이상의 로컬 변수로 펼칩니다: 전달된 값은 검증과 타입 변환을 거치고, 전달되지 않은 값은 기본값을 갖습니다. 데모는 url만 전달했으므로 대부분의 파라미터는 기본 분기를 탑니다.

유일하게 전달된 url은 getUrlProp를 거쳐 URL 객체로 변환됩니다:

const url = URL.parse('https://assets-blog.streampdf.com/PDF32000_2008.pdf', window.location);
// const url = src.url ? getUrlProp(src.url) : null;

나머지 파라미터는 모두 전달되지 않았습니다. 소스 코드 순서대로 계산 결과는 다음과 같습니다:

로컬 변수계산 결과값 결정 로직
datanull전달되면 getDataProp으로 일괄 Uint8Array로 변환
httpHeadersnull그대로 전달, 미지정이면 null
withCredentialsfalse=== true만 허용
passwordnull?? null
rangeTransportnullPDFDataRangeTransport 인스턴스만 허용
rangeChunkSize2 ** 16 (64KB)양의 정수일 때만 전달값 허용
workernullPDFWorker 인스턴스만 허용
verbosityundefinedsrc.verbosity를 그대로 사용
docBaseUrlnulldata:가 아닌 프로토콜의 string만 허용
cMapUrlnullgetFactoryUrlProp: string이 아니면 null, /로 끝나야 함
cMapPackedtrue!== false
iccUrlnullcMapUrl과 동일
standardFontDataUrlnullcMapUrl과 동일
wasmUrlnullcMapUrl과 동일
ignoreErrorstruesrc.stopAtErrors !== true, 아래 참고
maxImageSize-1 (제한 없음)정수이면서 > -1일 때만 허용
isOffscreenCanvasSupportedtrue미지정이면 !isNodeJS
isImageDecoderSupportedtrue미지정이면 !isNodeJS
canvasMaxAreaInBytes-1 (제한 없음)정수만 허용
disableFontFacefalse미지정이면 isNodeJS
fontExtraPropertiesfalse=== true만 허용
enableXfafalse=== true만 허용
ownerDocumentglobalThis.document미지정이면 전역 document
disableRangefalse=== true만 허용
disableStreamfalse=== true만 허용
disableAutoFetchfalse=== true만 허용
pdfBugfalse=== true만 허용
CanvasFactoryDOMCanvasFactory환경 분기, 아래 참고
FilterFactoryDOMFilterFactory위와 동일
BinaryDataFactoryDOMBinaryDataFactory위와 동일
enableHWAfalse=== true만 허용
enableWebGPUfalse=== true만 허용
gpuPromisePromise.resolve(false)WebGPU가 꺼져 있어 GPU 초기화를 시작하지 않음
useWasmtrue!== false
pagesMappernew PagesMapper()미지정이면 새로 생성
useSystemFontstrue연쇄 기본값, 아래 참고
useWorkerFetchfalse복합 판단, 아래 Q&A 참고
styleElementnullTESTING 빌드에서만 전달값 유지, 프로덕션에서는 항상 null

이상이 이 구간에서 생성되는 모든 로컬 변수입니다. 대부분의 판단은 "미지정이면 기본값"이라는 평범한 것이지만, 몇 가지는 더 들여다볼 가치가 있습니다.

ignoreErrors: 의미가 뒤집힌 내부 변수

사용자 대상 파라미터는 stopAtErrors인데, 내부 변수는 ignoreErrors이고 값 결정 로직은 src.stopAtErrors !== true입니다. 즉 기본 동작은 오류를 무시하는 것이며, 파싱 오류가 나면 getOperatorList / getTextContent / RenderTask 같은 promise를 reject하는 대신 가능한 한 상태를 복구하려고 합니다. 소스를 읽을 때 이름과 값을 뒤집기 쉬운 지점입니다.

rangeChunkSize: 64KB라는 복선

rangeChunkSize의 기본값은 64KB입니다. disableRange = false이므로 뒤에서 transport를 만들 때 PDF에 range 요청을 보내게 되고, 매번 가져오는 청크 크기가 바로 이 값입니다. 기억해 두세요.

docBaseUrl: 왜 data: URL을 제외하는가

소스 코드 주석이 이유를 설명합니다: data: URL은 절대 경로를 복원하는 데 쓸 수 없고, 게다가 PDF 문서 전체를 담고 있어 길이에 상한이 없기 때문에 worker 스레드로 보내지 않습니다.

gpuPromise: Worker 기동과 병행하는 설계

const gpuPromise = Promise.resolve(false); // enableWebGPU ? initGPU() : Promise.resolve(false);

WebGPU를 켜지 않아도 여기서는 즉시 resolved된 promise를 반환합니다. 판단을 사용 시점까지 미루지 않고, GPU 초기화를 worker 기동과 병행해서 실행한 뒤 해석된 불리언을 worker로 전달한다는 의도가 주석에 적혀 있습니다.

세 개의 Factory: 환경 분기

const CanvasFactory = DOMCanvasFactory;
// src.CanvasFactory || (GENERIC 빌드 && isNodeJS ? NodeCanvasFactory : DOMCanvasFactory);

FilterFactory, BinaryDataFactory도 마찬가지입니다. 판단은 세 단계입니다: 사용자 정의 > Node 환경 구현 > DOM 구현. 데모는 브라우저에서 실행되고 커스텀도 하지 않으므로 세 팩토리 모두 DOM 버전입니다. 각자의 역할:

  • CanvasFactory: 렌더링용 canvas 생성
  • FilterFactory: 이미지 필터 (예: SVG 필터) 생성
  • BinaryDataFactory: 내장 정적 리소스 다운로드, 아래의 useWorkerFetch와 연동

useSystemFonts: 기본값이 다른 파라미터에 의존

const useSystemFonts = true;
// typeof src.useSystemFonts === "boolean" ? src.useSystemFonts : !isNodeJS && !disableFontFace;

기본값은 바로 앞에서 파싱한 disableFontFace에 의존합니다. 왜일까? PDF에 내장되지 않은 폰트는 FontFace로 로딩되는데, disableFontFace가 true이면 이 로딩 경로가 막히고, 그 상태에서 시스템 폰트로 대체하면 글리프가 완전히 깨집니다. 그래서 이 경우에는 기본값도 false로 두어 완전히 망가진 폰트가 렌더링되는 것을 피합니다.

useWorkerFetch: 복합 판단

const useWorkerFetch = false;
// typeof src.useWorkerFetch === "boolean"
//   ? src.useWorkerFetch
//   : MOZCENTRAL ||
//     !!(
//       BinaryDataFactory === DOMBinaryDataFactory &&
//       cMapUrl && cMapPacked && standardFontDataUrl && wasmUrl &&
//       isValidFetchUrl(cMapUrl, document.baseURI) &&
//       isValidFetchUrl(standardFontDataUrl, document.baseURI) &&
//       isValidFetchUrl(wasmUrl, document.baseURI)
//     );

Q: 여기서 하는 판단은 무엇인가? A: "내장 정적 리소스"(CMap 파일, 표준 폰트 파일, wasm 파일)를 누가 다운로드할지를 결정합니다: worker 스레드가 Fetch API로 직접 다운로드하는지 (true), 아니면 메인 스레드가 다운로드해 worker로 전달하는지 (false)입니다.

사용자가 명시적으로 boolean을 전달하면 그 값을 그대로 씁니다. 아니라면 기본값을 계산합니다: MOZCENTRAL (Firefox 내장 viewer)은 항상 true이고, 일반 빌드는 다음 세 조건을 동시에 만족할 때만 true입니다:

  1. 커스텀 BinaryDataFactory가 없을 것 —— useWorkerFetch가 true이면 이 파라미터는 무시되므로 (뒤의 transportFactory에서 binaryDataFactory가 null로 설정됨), 사용자 정의 팩토리는 존중되어야 하고 따라서 기본값을 true로 둘 수 없다;
  2. cMapUrl / standardFontDataUrl / wasmUrl이 모두 제공되고 cMapPacked가 true일 것 —— URL이 없으면 fetch할 대상이 없다;
  3. 이 세 URL이 document.baseURI 기준으로 해석했을 때 http/https일 것 —— Fetch API는 http/https만 지원하고 file/ftp는 지원하지 않는다.

이 글의 데모는 url만 전달하고 cMapUrl이 null이므로 조건 2를 만족하지 않아 useWorkerFetch = false입니다: 메인 스레드 경로를 따라 DOMBinaryDataFactory가 메인 스레드에서 다운로드하고, worker는 필요할 때 FetchBinaryData 메시지로 메인 스레드에 요청합니다.

338행: 메인 스레드 로그 레벨 설정

setVerbosityLevel(verbosity); // verbosity는 undefined

verbosity가 undefined이므로 메인 스레드의 로그 레벨은 기본값을 유지합니다. 이 설정은 메인 스레드에만 영향을 준다는 점에 주의하세요. worker 스레드의 로그 레벨은 worker 생성 시 별도로 전달됩니다.

342~350행: 세 팩토리 인스턴스화

const transportFactory = {
  canvasFactory: new DOMCanvasFactory({ ownerDocument: document, enableHWA: false }),
  filterFactory: new DOMFilterFactory({ docId: 'd0', ownerDocument: document }),
  binaryDataFactory: new DOMBinaryDataFactory({
    cMapUrl: null,
    standardFontDataUrl: null,
    wasmUrl: null,
  }),
};
// binaryDataFactory: MOZCENTRAL이거나 useWorkerFetch가 true일 때 null

앞에서 고른 세 Factory 클래스를 인스턴스화해 transportFactory로 묶고, 이후 transport에 넘깁니다. useWorkerFetch = false이므로 binaryDataFactory는 null이 아닙니다 —— 앞 절의 Q&A와 호응합니다: 내장 정적 리소스는 메인 스레드에서 다운로드합니다.

DOMCanvasFactory 인스턴스화: 소스 살펴보기

DOMFilterFactory 인스턴스화: 소스 살펴보기

DOMBinaryDataFactory 인스턴스화: 소스 살펴보기

352~360행: Worker 생성

worker = PDFWorker.create({ verbosity: undefined, port: undefined });
// port는 GlobalWorkerOptions.workerPort에서 오며, 데모에서는 설정하지 않음
task._worker = worker;

데모는 worker를 전달하지 않았으므로 PDF.js가 직접 하나를 생성하고 소유합니다. PDFWorker.create는 PDFWorker 인스턴스를 동기적으로 반환하고, 그 생성자는 GlobalWorkerOptions.workerSrc(첫 글에서 설정한 worker 파일 주소)를 읽어 Worker를 만들고 ready / test 메시지로 기동 핸드셰이크를 완료합니다.

PDFWorker.create는 동기 호출입니다: 호출 살펴보기

여기서는 한 가지만 기억하면 됩니다: worker.promise는 Promise<void>이며, test 응답이 성공하면 resolve되고(값은 undefined), 기동 실패나 파괴 시 reject됩니다.

362~402행: 두 개의 파라미터 묶음 조립

첫 번째는 docParams로, 이후 스레드를 넘어 worker로 전송됩니다:

const docParams = {
  docId: 'd0',
  apiVersion: '6.3.289', // 빌드 버전 번호
  data: null,
  password: null,
  disableAutoFetch: false,
  rangeChunkSize: 65536,
  docBaseUrl: null,
  enableXfa: false,
  evaluatorOptions: {
    maxImageSize: -1,
    disableFontFace: false,
    ignoreErrors: true,
    isOffscreenCanvasSupported: true,
    isImageDecoderSupported: true,
    canvasMaxAreaInBytes: -1,
    fontExtraProperties: false,
    useSystemFonts: true,
    useWasm: true,
    useWorkerFetch: false,
    cMapUrl: null,
    cMapPacked: true,
    iccUrl: null,
    standardFontDataUrl: null,
    wasmUrl: null,
    hasGPU: false, // 자리표시자, 나중에 채워 넣음
  },
};

두 번째는 transportParams로, 메인 스레드에만 남깁니다:

const transportParams = {
  ownerDocument: document,
  pdfBug: false,
  styleElement: null,
  enableHWA: false,
  loadingParams: { disableAutoFetch: false, enableXfa: false },
};

한 문장으로 구분하면, docParams는 worker로 보내는 "작업 지시"이고 transportParams는 메인 스레드가 스스로 보관하는 "로컬 설정"입니다.

404~473행: worker 준비 완료 후 핸드셰이크

Promise.all([worker.promise, gpuPromise]).then(...)

worker 준비 완료와 GPU 감지(resolve 값은 false)가 모두 끝나면 다음이 순서대로 일어납니다:

  1. hasGPU = false를 evaluatorOptions에 채워 넣는다;
  2. worker로 GetDocRequest 메시지를 보낸다. 페이로드는 docParams이다. data가 null이므로 소유권을 이전할 버퍼는 없다;
  3. 네트워크 스트림을 선택한다: data가 null이고 rangeTransport도 null이므로 url 분기를 타고 네트워크 스트림을 인스턴스화한다:
networkStream = new NetworkStream({
  url,
  httpHeaders: null,
  withCredentials: false,
  rangeChunkSize: 65536,
  disableRange: false,
  disableStream: false,
});

여기서 NetworkStream은 import된 고정 클래스가 아니라, 이 분기 안에서 실행 환경에 따라 동적으로 선택됩니다:

const NetworkStream = getNetworkStream(url); // api.js#L431

getNetworkStream은 3지 선택을 한 번만 수행합니다:

function getNetworkStream(url) {
  // eslint-disable-next-line no-nested-ternary
  return isValidFetchUrl(url)
    ? PDFFetchStream
    : typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC') && isNodeJS
      ? PDFNodeStream
      : PDFNetworkStream;
}
판정반환되는 스트림 구현
isValidFetchUrl(url): 프로토콜이 http / httpsPDFFetchStream
그 외이면서 GENERIC 빌드이고 Node 환경PDFNodeStream
나머지 경우PDFNetworkStream (XHR 구현)

데모의 url은 https://...이므로 실제로 인스턴스화되는 것은 PDFFetchStream입니다. 다른 두 입력에도 각자의 스트림이 있습니다: rangeTransport를 전달하면 PDFDataTransportStream, data를 전달하면 PDF 전체가 이미 메모리에 있으므로 스트림을 전혀 만들지 않습니다.

정리하면: const NetworkStream = PDFFetchStream.

이어서 실행됩니다:

  1. GetDocRequest에 대한 worker의 응답(workerId)을 받는다: 이벤트 살펴보기. 여기서 받는 반환값은 d0_worker이며 변수 workerId에 할당된다
  2. MessageHandler를 생성하고 { docId: 'd0', workerId: 'd0_worker', port: Worker }를 전달한다: 호출 살펴보기, 변수 messageHandler에 할당
  3. WorkerTransport를 생성하고 { messageHandler, task, networkStream, transportParams, transportFactory, pagesMapper }를 전달한다: 호출 살펴보기, task._transport에 연결
  4. worker로 Ready 메시지를 보내 메인 스레드가 준비되었음을 알린다: 이벤트 살펴보기

체인의 마지막에서 task._setupCapability.resolve가 호출되어 task의 비동기 작업이 완료됩니다.

474~475행: 반환값

위의 .then 체인은 모두 백그라운드에서 비동기로 진행되고, getDocument 자체는 동기적으로 task를 반환합니다. 체인 어디에서든 오류가 나면 task 내부의 capability가 reject되며, 그것이 데모의 await loadingTask.promise가 기다리는 promise입니다.

이로써 getDocument 실행이 끝났습니다.

정리

이번 절에서는 데모를 따라 getDocument 호출을 끝까지 추적했습니다:

  1. PDFDocumentLoadingTask를 생성하고 docId = 'd0'을 꺼낸다;
  2. src를 35개 이상의 로컬 변수로 펼친다. 데모는 url만 전달했으므로 나머지는 모두 기본값이 된다;
  3. 세 개의 DOM 팩토리를 인스턴스화해 transportFactory로 묶는다;
  4. PDFWorker.create가 worker를 생성하고 핸드셰이크를 시작한다;
  5. docParams(worker로 보냄)와 transportParams(메인 스레드에 남김)를 조립한다;
  6. worker 준비 완료 후 GetDocRequest를 보내고, 네트워크 스트림을 만들고, MessageHandler와 WorkerTransport를 생성하고, 마지막으로 Ready를 보낸다;
  7. 동기적으로 task를 반환하고, 데모는 await loadingTask.promise로 문서 준비 완료를 기다린다.

첫 글의 데모로 돌아가 보면, 4~7행에서 호출하는 것이 바로 이 글에서 분해한 getDocument입니다.

상위 호출 체인을 따라가면 다음 호출 지점은: getPage


저에 대해: StreamPDF 창작자로, AI와 PDF 분야의 연구 개발에 집중하고 있습니다.

데모로 파헤치는 PDF.js 더 보기