How-to2026-09-28약 8분

PDF API용 TypeScript SDK: 첫 호출까지 5분, 그리고 사용자에게 남겨 두는 일

TypeScript에서 @pdf123/sdk로 파일 병합, 워터마크 추가, 파일 정보 읽기, 여러 도구를 한 요청으로 잇는 작업을 합니다. 도구 이름이나 매개변수 철자 오류는 업로드 전에 잡아 주고, 서버의 거부에는 reason과 hint가 붙지만, 재시도와 취소, 배치 안의 부분 실패는 사용자가 처리해야 합니다.

PDF123 · Updated 2026-09-28

Node에서 PDF API를 호출할 때 소프트웨어 개발 키트(SDK)인 @pdf123/sdk는 도구 이름이나 매개변수의 철자 오류를 파일이 업로드되기 전에 잡아 줍니다. 재시도도, 대신 취소하는 것도, 결과를 스트리밍하는 것도 하지 않습니다. new Pdf123Client()는 기본적으로 익명으로 https://pdf123.xyz를 가리킵니다. 파일은 그곳에 업로드되어 그곳에서 처리됩니다. 로컬 엔진은 없기 때문입니다.

다이어그램: 호출 하나가 세 관문을 차례로 지납니다. 컴파일 시점의 tsc 검사, 요청을 보내기 전의 런타임 검증, 업로드 후 서버가 돌려주는 400입니다. 재시도와 취소는 호출하는 쪽에 맡겨지고, 입력 합계가 95 MiB를 넘으면 자동으로 청크 업로드가 됩니다

폴더에 PDF 두 개만 있으면 병합할 수 있습니다

Node 20.3 이상 또는 Bun이 필요합니다. 패키지는 ES 모듈이므로 코드를 .mjs 파일에 넣거나 먼저 npm pkg set type=module을 실행하세요. a.pdf와 b.pdf를 현재 디렉터리에 두세요.

npm install @pdf123/sdk
// merge.mjs
import { Pdf123Client } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const merged = await client.run("merge", {
  input: [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
});
console.log(await saveResult(merged, { output: "merged.pdf" }));

node merge.mjs를 실행하면 터미널에 넘겨 준 출력 경로 merged.pdf가 찍히고, 파일은 현재 디렉터리에 생깁니다. readFileInput과 saveResult는 @pdf123/sdk/node에 있습니다. 루트 진입점은 fetch에만 의존하고 입력을 평범한 { name, data }로 받으므로, 파일 시스템이 없는 환경에서도 같은 클라이언트를 쓸 수 있습니다. 결과는 { data, contentType, filename, json }이고, data는 통째로 된 Uint8Array입니다. Buffer.from(result.data)로 Buffer를 얻습니다.

PDF 병합은 여러 도구 중 하나일 뿐입니다.

파일은 data에, 보고서는 json에 담겨 돌아옵니다

모든 도구는 client.run(toolName, { input, params })로 호출합니다. 아래 코드는 앞 절의 merge.mjs에 이어지며, client와 saveResult는 이미 범위 안에 있습니다. 결과를 어디서 읽을지는 getTool의 returns 필드에 달려 있습니다. 값은 "file" 또는 "json"이고, 앞의 경우 data를, 뒤의 경우 json을 씁니다.

import { getTool } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

// client는 앞 절의 merge.mjs에서 온 것
const marked = await client.run("watermark", {
  input: await readFileInput("a.pdf"),
  params: { watermarkText: "DRAFT", fontSize: 40 },
});
console.log(await saveResult(marked, { output: "marked.pdf" }));

const info = await client.run("get-info", { input: await readFileInput("a.pdf") });
console.log(info.json.FileSize, info.json.Encrypted);
console.log(getTool("get-info")?.returns); // "json"

첫 블록은 워터마크가 들어간 파일을 marked.pdf에 쓰고, 두 번째 블록에서는 info.json이 보고서입니다. 필드 이름과 기본값, 허용되는 값을 외울 필요는 없습니다. getTool("watermark")?.fields가 바로 그 카탈로그이고, 명령줄의 pdfx describe watermark도 같은 것을 읽습니다. TOOLS에는 95개 도구가 전부 들어 있습니다. 전체 레퍼런스는 개발자 페이지의 SDK 가이드에 있습니다.

병합한 뒤 워터마크를 넣고 압축하려면 pipeline을 쓰거나 run을 세 번 연달아 호출하면 됩니다. 중간 파일이 필요한지에 따라 고르세요. 이것도 위의 client에서 이어집니다. pipeline은 세 단계를 한 요청으로 묶고, 중간 결과는 서버에 머물며, 호출하는 쪽은 마지막 단계만 받습니다. 아래 코드는 그것을 out.pdf에 씁니다. 모든 단계의 파일이 필요하면 따로따로 호출하세요.

const result = await client.pipeline(
  [{ tool: "merge" }, { tool: "watermark", params: { watermarkText: "DRAFT" } }, { tool: "compress" }],
  [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
);
console.log(await saveResult(result, { output: "out.pdf" }));

파이프라인은 최대 8단계입니다. 9번째 단계는 HTTP 400: at most 8 pipeline steps allowed를 받습니다.

익명으로, 키와 함께, 또는 직접 운영하는 서버로

PDFX_API_BASE를 설정했는데도 여전히 https://pdf123.xyz로 가는 것은, new Pdf123Client()가 환경 변수를 읽지 않기 때문입니다. 생성자 옵션만 봅니다.

익명 호출에는 그 생성자를 그대로 쓰세요. 키를 쓰려면 new Pdf123Client({ apiKey: process.env.PDFX_API_KEY })라고 씁니다. 요청 헤더는 X-API-KEY입니다.

직접 운영하는 서버를 가리키려면 @pdf123/sdk/node의 clientFromEnv()를 쓰세요. PDFX_API_BASE와 PDFX_API_KEY를 읽습니다. new Pdf123Client({ baseUrl: "http://localhost:8080" })처럼 직접 써도 됩니다. 주소에 닿지 않으면 호출은 실패하고, 오프라인 모드는 없습니다. 파일을 자기 네트워크 안에 두면 무엇을 얻고 무엇이 비용이 되는지는 자체 호스팅이 실제로 사 주는 것과 그 대가에서 다룹니다.

잘못된 매개변수는 업로드 전에 잡힙니다

이게 없으면 파일이 먼저 업로드되고, 필드 이름의 철자 오류는 서버가 돌려주는 400으로 비로소 드러납니다. 각 도구의 매개변수 타입은 카탈로그에서 생성되므로, 흔한 실수 세 가지는 tsc 단계에서 실패합니다.

작성한 코드 컴파일러 오류(발췌)
client.run("rotat", { input }) Argument of type '"rotat"' is not assignable to parameter of type 'ToolId'
params: { watermarkTxt: "DRAFT" } Object literal may only specify known properties, but 'watermarkTxt' does not exist, 끝은 Did you mean to write 'watermarkText'?
params: { angle: 45 } (rotate는 90, 180, 270만 받습니다) Type '45' is not assignable to type …, 이어서 허용되는 값

{ angle: 90 }이나 { watermarkText: "DRAFT", fontSize: 30 } 같은 올바른 호출은 컴파일됩니다. 숫자 필드는 숫자와 문자열을 모두 받으므로 fontSize: 30과 fontSize: "30"은 같습니다. TypeScript를 쓰지 않는 프로젝트도 같은 오류를 런타임에 받고, 요청은 전송되지 않습니다. 세 번째 경우는 Field "angle" of "rotate" must be one of: 90, 180, 270이 되고, 도구 이름을 잘못 쓰면 unknown_tool이 나오면서 비슷한 이름이 메시지에 나열됩니다.

서버가 거부하면 reason으로 분기하세요

서버가 요청을 거부하면 SDK는 status, code, reason, problem.hint를 가진 Pdf123Error를 던집니다. 하나의 code에 여러 reason이 있을 수 있으므로 reason을 먼저 확인하고, 없으면 code로 물러서세요. 흔한 실패 세 가지를 같은 입력으로 로컬에서 실행하면 다음과 같습니다.

입력 status code reason hint(원문은 영어)
암호화된 PDF, 비밀번호 없음 400 bad_request password_required Provide the document password, or unlock the PDF first
잘못된 비밀번호 400 bad_request wrong_password Check the password and try again
PDF가 아니거나 손상된 파일 400 invalid_document invalid_pdf Upload a valid, undamaged PDF; you can try repairing it first
import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("locked.pdf");
try {
  await client.run("compress", { input });
} catch (error) {
  if (!(error instanceof Pdf123Error)) throw error;
  console.error(error.status, error.code, error.reason, error.problem?.hint);
  if (error.reason === "password_required") {
    await client.run("compress", { input, password: "secret" });
  }
}

비밀번호를 알고 있다면 파일 하나에 password를 넘기세요. 잠금 해제와 대상 도구가 같은 요청 안에서 이루어집니다. 손상된 파일은 먼저 PDF 복구를 써 보세요.

"결과 없음"은 구분해야 합니다. PDF를 CSV로가 PDF에서 표를 찾지 못하면 서버는 204를 돌려주고 SDK는 code: "no_content"를 던집니다. 이유는 빈 표 내보내기(204): 이 PDF에는 열이 없을 가능성이 큽니다에 있습니다. 조건부 필터 도구는 조건이 거짓인 것을 정상적인 결과로 다룹니다. 던지지 않고 matched: false와 빈 data를 돌려줍니다.

재시도, 메모리, 취소는 호출하는 쪽의 몫입니다

네트워크 오류와 5xx 응답은 그대로 던져지며, SDK는 스스로 재시도하지 않습니다. 상태를 바꾸는 요청에는 직접 정한 idempotencyKey를 넘기세요. 같은 키로 다시 보내면 다시 처리하지 않고 첫 결과를 돌려주며, 동작 방식과 한도는 Idempotency-Key: PDF 작업을 안전하게 재시도하기에 있습니다. 결과는 통째로 메모리에 읽히며 스트리밍은 없습니다. 입력과 출력이 모두 크다면, 둘이 동시에 차지하는 메모리를 계산에 넣으세요.

취소와 타임아웃은 서로 다른 code 값입니다. 한 번의 호출은 먼저 일어나는 쪽 하나만 보고합니다. 아래 예제는 두 분기가 실제로 실행될 수 있도록 둘을 별개의 호출 두 번으로 시험합니다.

import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("a.pdf");

const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
try {
  await client.run("compress", { input, signal: controller.signal });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "cancelled") { /* 직접 취소했음 */ }
}

try {
  await client.run("compress", { input, timeoutMs: 60_000 });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "timeout") { /* timeoutMs 초과. 이번에는 signal 없음 */ }
}

중단 신호(AbortSignal)가 발동하는 즉시 요청은 code: "cancelled"로 거부됩니다. 타임아웃을 넘기면 code: "timeout"이 됩니다. 요청마다 기본 타임아웃은 5분이며, 클라이언트를 만들 때 바꾸거나 호출마다 timeoutMs로 덮어쓸 수 있습니다. 청크 업로드에서는 요청 하나하나가 각자 그 타임아웃을 가집니다. 취소는 이 연결만 닫을 뿐, 서버가 처리를 멈춘다는 보장은 없습니다.

파일 하나가 실패해도 나머지는 계속되고, 콜백은 완료된 순서로 옵니다

단일 파일 도구에 입력을 여러 개 넘길 때는 runBatch를 쓰세요. 파일 하나가 실패해도 나머지는 계속 진행됩니다. onResult는 각 파일이 끝나는 즉시 호출되므로 완료된 순서로 실행되고, index는 입력 배열에서 그 파일의 위치입니다. 반환되는 배열은 항상 입력 순서입니다. 바이트는 이 콜백 안에서 디스크에 쓰세요. retainData: false는 콜백이 반환되면 data를 비우므로, 여기서 saveResult하지 않은 결과는 사라집니다. 실행이 중간에 끊겨도 이미 쓴 파일은 남습니다.

폴더에 정상 PDF 세 개, 곧 a.pdf, b.pdf, 암호화된 locked.pdf(비밀번호 secret)가 있고, 몇 글자만 타이핑해 넣은 깨진 파일 broken.pdf도 있다고 가정합니다.

import { Pdf123Client } from "@pdf123/sdk";
import { fileSource, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const results = await client.runBatch("compress", [
  fileSource("a.pdf"), fileSource("broken.pdf"), fileSource("b.pdf"), fileSource("locked.pdf"),
], {
  concurrency: 2,
  passwordFor: (file) => (file.name === "locked.pdf" ? "secret" : undefined),
  onResult: async (entry, index) => {
    console.log(index, entry.input.name, entry.ok ? "ok" : entry.error.reason);
    if (entry.ok) await saveResult(entry.result, { output: "out/" });
  },
  retainData: false,
});

기본적으로 두 파일씩 처리합니다. 바꾸려면 concurrency를 쓰세요. fileSource(path)는 차례가 왔을 때에야 디스크에서 파일을 읽으므로, 큰 파일로 이루어진 큰 배치도 전부가 한꺼번에 메모리에 올라오지 않습니다. passwordFor 덕분에 잠긴 파일과 잠기지 않은 파일이 섞인 배치도 한 번에 실행됩니다. 위의 파일 네 개를 로컬에서 실행했을 때 콜백이 받은 것은 다음과 같습니다.

1 broken.pdf invalid_pdf
0 a.pdf ok
2 b.pdf ok
3 locked.pdf ok

완료 순서는 실행할 때마다 다를 수 있으며, 이것은 한 번 실행했을 때의 모습일 뿐입니다. retainData: false는 반환 배열의 data를 비웁니다. 성공한 파일은 위의 saveResult가 이미 out/에 써 두었습니다. runBatch에 idempotencyKey를 넘기면 파일마다 실제로 전송되는 키는 <key>:<index>입니다.

청크 업로드는 합계가 95 MiB를 넘을 때만 시작됩니다

직접 요청 본문은 100 MiB로 제한되며 그보다 큰 것은 거부됩니다. 자세한 내용은 큰 PDF 업로드가 거부될 때: 100 MiB 요청 본문 한도와 잘못된 방향을 가리키는 오류를 보세요. 한 요청에 들어 있는 모든 파일의 합이 95 MiB를 넘으면 SDK는 청크 업로드로 바꾸며, client.run 호출은 달라지지 않습니다. 서버의 단일 업로드 기본 최대치는 500 MiB이고, 직접 호스팅한다면 PDFX_UPLOAD_MAX_BYTES로 조정합니다.

같은 연산을 curl, MCP, 명령줄로 쓴 것은 같은 연산, 네 가지 클라이언트: 브라우저, curl, MCP, pdfx를 보세요. 그 글은 네 가지 호출을 나란히 놓았고, 이 글은 SDK만 더 풀어 설명합니다. npm의 패키지 페이지는 @pdf123/sdk입니다.

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool