두 npm 패키지가 pdfjs‑dist를 놓고 싸울 때: 시스템 바이너리로 전환
Source: Dev.to
위에 제공된 링크에 있는 전체 텍스트를 번역하려면 해당 글의 내용을 복사해서 여기 붙여 주세요.
텍스트를 제공해 주시면 원본 형식과 코드 블록을 유지한 채로 한국어 번역을 도와드리겠습니다.
문제
스캔한 PDF에 대한 OCR 지원을 Next.js 앱에 추가하고 있었습니다. 계획은 간단했습니다:
- pdf-to-img를 사용해 PDF 페이지를 래스터화한다.
- 이미지를 Tesseract에 파이프한다.
- 추출된 텍스트를 연결한다.
pdf-to-img를 설치하고 프리‑프로덕션에 배포한 후, 스캔한 PDF를 업로드하면 다음 오류가 발생했습니다:
Error: API version does not match Worker version
왜 이런 일이 발생했는가
프로젝트에서는 이미 디지털 PDF에서 텍스트를 추출하기 위해 unpdf를 사용하고 있었습니다. pdf-to-img와 unpdf 모두 pdfjs-dist의 자체 복사본을 번들에 포함하지만, 사용하고 있는 버전이 서로 다릅니다:
| Package | Bundled pdfjs‑dist version |
|---|---|
| pdf-to-img | ~5.4.624 |
| unpdf | ~5.4.296 |
두 패키지를 동일한 Node.js 프로세스에서 로드하면 각각 자체 PDF.js 워커를 등록하려고 합니다. 워커가 충돌하면서 PDF.js는 “API version does not match Worker version” 오류를 발생시킵니다. 번들된 복사본이 peer dependency가 아니기 때문에 npm 중복 제거(deduplication)로 이 충돌을 해결할 수 없습니다.
막다른 대안
- pdfjs-dist 직접 사용 (여전히
unpdf가 요구하는 버전에 고정됨) - canvas + 수동 PDF.js 렌더링 (네이티브 바인딩 및 복잡한 Docker 설정 필요)
- sharp (PDF를 래스터화할 수 없음)
- pdf-poppler (유지 관리가 부실한 래퍼)
이들 모두는 동일한 pdfjs‑dist 충돌을 다시 일으키거나, 무거운 네이티브 빌드가 필요하거나, 폐기되었습니다.
Source: …
더 나은 해결책: 시스템 바이너리 사용
PDF를 이미지로 변환하고 OCR을 수행하는 작업은 OS 수준에서 이미 해결된 문제입니다. poppler-utils(pdftoppm)와 tesseract-ocr 같은 도구는 안정적이고 빠르며 검증된 성능을 제공합니다.
바이너리 설치
RUN apt-get update && apt-get install -y \
poppler-utils \
tesseract-ocr \
tesseract-ocr-eng \
&& rm -rf /var/lib/apt/lists/*
OCR 파이프라인 구현
import { execSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
async function ocrScannedPdf(pdfPath: string): Promise {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ocr-"));
const outputPrefix = path.join(tmpDir, "page");
try {
// PDF 페이지를 PNG 이미지(300 DPI, OCR 정확도에 적합)로 변환
execSync(`pdftoppm -png -r 300 "${pdfPath}" "${outputPrefix}"`, {
timeout: 60000,
});
// 생성된 이미지 수집
const images = fs
.readdirSync(tmpDir)
.filter((f) => f.endsWith(".png"))
.sort()
.map((f) => path.join(tmpDir, f));
if (images.length === 0) {
throw new Error("pdftoppm이 출력 파일을 생성하지 않았습니다");
}
// 각 페이지에 대해 Tesseract 실행
const texts = images.map((imgPath) => {
const result = execSync(`tesseract "${imgPath}" stdout -l eng`, {
timeout: 30000,
});
return result.toString().trim();
});
return texts.filter(Boolean).join("\n\n");
} finally {
// 임시 파일 정리
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
핵심 포인트
- 변환이나 OCR 단계에 npm 패키지가 전혀 필요하지 않습니다.
- 시스템 바이너리는 Node.js 모듈과 독립적이므로 버전 충돌이 발생하지 않습니다.
- 전체 파이프라인은 약 20줄의 코드로 구현됩니다.
시스템 바이너리를 npm 래퍼보다 선호하는 이유
npm 패키지가 시스템 바이너리(예: ImageMagick, FFmpeg, Ghostscript, Poppler, Tesseract, wkhtmltopdf)를 단순히 래핑할 때:
- 유지 관리 확인 – 래퍼가 잘 유지 관리되고 있는가, 아니면 얇은 쉼인가?
- 전이적 충돌 감시 – 래퍼가 자체 라이브러리 사본을 번들링하여 다른 의존성과 충돌할 가능성이 있는가?
- Docker 단순성 고려 – 바이너리를 직접 설치하면 Dockerfile이 더 깔끔해지는 경우가 많다.
npm 생태계는 순수 JavaScript 문제에 강점이 있다. 오래전부터 존재하고 고성능을 자랑하는 네이티브 구현이 필요한 작업에서는 바이너리를 직접 호출하는 것이 일반적으로 더 신뢰할 수 있는 선택이다.
Takeaways
- 번들된 pdfjs‑dist 충돌을 피하세요 서로 다른 버전을 포함하는 여러 패키지를 로드하지 않음으로써.
- OS 수준 도구 (
pdftoppm,tesseract)를 활용하여 PDF 래스터화와 OCR을 수행합니다. - Node.js 레이어를 가볍게 유지: 몇 번의
execSync호출과 최소한의 코드만으로 무겁고 충돌이 잦은 npm 래퍼를 대체할 수 있습니다. - Docker 이미지 단순화: 대형이고 불안정한 npm 래퍼를 가져오는 대신 필요한 바이너리를 직접 설치합니다.