트렌딩 딥다이브 · 2026-07-22 · TrendShift Daily #9 · ARCSHIPS

arcships/light-ocr 딥다이브
— 파이썬 없이 Node.js에서 도는 오프라인 OCR 엔진

light-ocrC++17로 쓴 PP-OCRv6 Small OCR 코어비동기 Node-API(N-API) 어댑터를 씌워 npm에 프리빌트 바이너리로 올린 이미지 텍스트 인식(OCR) 라이브러리다. 한 줄 요약: 파이썬도, 네트워크도, 컴파일러도 필요 없이 npm install 한 번으로 Node.js에서 이미지 속 글자를 뽑아낸다.

흔한 OCR 방식과 결정적으로 다른 점 — 실행 시점에 서버로 이미지를 보내지 않고(오프라인), 파이썬 런타임에 의존하지 않는다(대부분의 PaddleOCR·EasyOCR은 파이썬 기반). JPEG/PNG나 원시 픽셀 버퍼를 넣으면 읽기 순서대로 정렬된 각 줄을 텍스트·신뢰도·4점 사각형 좌표와 함께 돌려준다. 게다가 macOS(Core ML)·Linux/Windows(WebGPU)에서 하드웨어 가속을 기본 지원한다.

(저장소 arcships/light-ocr · 코어 C++17 + ONNX Runtime 1.22 · N-API v8 어댑터 · 빌드 순수 CMake(node-gyp 없음) · Node.js 22/24 · npm @arcships/light-ocr · 버전 0.3.2(2026-07-22) · 별점 372 · 라이선스 Apache-2.0 · 2026-07-14 생성된 신생 급상승 레포 · 클론 소스 직접 분석)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — "파이썬 없는 OCR"의 무게
  3. 기술 스택 전체 지도 — C++ 코어·N-API·npm 패키징
  4. 아키텍처 심화 — 스레드 한 개로 C++를 JS에 안전하게 잇기
  5. 디렉토리 구조 해부
  6. 학습 포인트 — 여기서 훔쳐올 설계
  7. 시스템 / 실행 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

"OCR 쓰려고 파이썬 서버를 또 세워야 해?" — 그 부담을 없앤 라이브러리
한 문장으로

light-ocr = 파이썬·클라우드 없이 Node.js 안에서 바로 도는, 설치 한 번짜리 오프라인 OCR — C++ 엔진을 비동기 N-API로 감싸 프리빌트로 배포

대부분의 고품질 OCR(PaddleOCR·EasyOCR 등)은 파이썬 + 무거운 딥러닝 프레임워크를 깔아야 하고, 클라우드 OCR API는 이미지를 남의 서버로 전송한다. Node.js 진영의 순수 JS 대안(Tesseract.js)은 설치는 쉽지만 정확도·속도가 아쉽다.

light-ocr은 그 사이를 파고든다. 정확도 좋은 PaddleOCR PP-OCRv6 Small 모델을 C++17 코어가 ONNX Runtime으로 돌리되, 그걸 Node.js에서 await 한 줄로 부르게 만들었다. 설치 순간에도, 실행 순간에도 인터넷이 필요 없다(모델·바이너리가 패키지에 들어 있음).

가장 작은 사용 예시가 이 라이브러리의 성격을 압축한다. 파이썬도, 별도 서버도, 모델 다운로드 스크립트도 없다:

import { createEngine } from "@arcships/light-ocr";
import { readFile } from "node:fs/promises";

const engine = await createEngine();                 // 모델·네이티브 로드(오프라인)
const result = await engine.recognizeEncoded(await readFile("image.jpg"));
for (const line of result.lines)
    console.log(line.text, line.confidence, line.box);  // 글자 · 신뢰도 · 사각형 좌표
await engine.close();                                   // 명시적 정리
용어
OCR · PP-OCRv6 · ONNX Runtime
OCR(광학 문자 인식)은 이미지 속 글자를 컴퓨터가 읽는 텍스트로 바꾸는 기술이다. PP-OCRv6은 중국 바이두가 만든 오픈소스 OCR 제품군 PaddleOCR의 최신 경량 모델로, "글자 위치 찾기(검출)"와 "그 글자가 뭔지 읽기(인식)" 두 단계를 담당한다. ONNX Runtime은 이런 딥러닝 모델(.onnx 파일)을 파이썬 없이 C++에서 실행해 주는 마이크로소프트의 추론 엔진이다. light-ocr은 이 셋을 묶어 "파이썬 없는 PaddleOCR"을 만든 셈이다.

정리하면 light-ocr의 정체는 두 겹이다. ① C++ OCR 코어(light_ocr::core 정적 라이브러리): 이미지 디코딩 → 글자 영역 검출 → 인식 → 후처리까지 손으로 짠 파이프라인. ② 비동기 N-API 어댑터: 그 C++ 코어를 Node.js의 이벤트 루프를 막지 않고 비동기 Promise로 노출하는 얇지만 정교한 다리. 여기에 CLI(light-ocr)와 AI 에이전트용 스킬까지 얹었다. OCR "정확도"는 바이두 모델에서 오고, 이 레포의 진짜 가치는 그 모델을 Node 생태계에 매끄럽게·안전하게 심는 엔지니어링에 있다.

저자는 GitHub 조직 arcships(커밋 작성자 chenghan)이며, Apache-2.0으로 공개했다. 2026년 7월 14일 생성된 아주 신생 레포인데 별점이 빠르게 붙어 TrendShift Daily 상위에 올랐다. 흥미롭게도 AGENTS.md는 모든 커밋·PR·이슈 제목에 "중국 고전 시구 · 영어 제목" 형식을 요구하는 독특한 규약을 둔다(예: 修正平台標識 · fix platform id).

2왜 주목받는가 — "파이썬 없는 OCR"의 무게

경쟁: Tesseract.js · PaddleOCR(파이썬) · 클라우드 OCR API — 이들과 무엇이 다른가

Node.js/자바스크립트 개발자가 "이미지에서 글자 좀 뽑자"고 마음먹으면, 지금까지 선택지는 셋 다 어딘가 불편했다. light-ocr은 그 셋의 약점을 정확히 겨냥한다.

방식대표장점불편한 점
순수 JSTesseract.js설치 쉬움, 브라우저도 됨정확도·속도 아쉬움, WASM 로딩 무거움
파이썬 엔진PaddleOCR, EasyOCR정확도 최상파이썬+딥러닝 스택 필수 → Node 앱에서 쓰려면 별도 서버·IPC
클라우드 APIGoogle Vision 등서버 부담 0, 정확이미지 외부 전송(프라이버시), 요금, 오프라인 불가
네이티브 임베드
(light-ocr)
light-ocr프리빌트 설치 1회, 오프라인, 좋은 모델, 하드웨어 가속플랫폼 한정(glibc·특정 OS/arch), 라인 단위만

차별점 ① — 설치 한 번, 그 안에 모델·바이너리가 다 들어 있다

npm install @arcships/light-ocr 하면 끝이다. 컴파일러도, 파이썬도, post-install 다운로드도 없다. 비결은 npm 패키징 방식이다. 얇은 자바스크립트 파사드 패키지 하나가, 운영체제·CPU에 맞는 프리빌트 네이티브 패키지(6종)와 모델 패키지optionalDependencies로 두고 런타임에 자동 선택한다. 사용자는 이 복잡함을 전혀 못 느낀다.

용어
프리빌트(prebuilt) · N-API · 네이티브 애드온
네이티브 애드온은 C/C++로 짜서 Node.js에 붙이는 확장(.node 파일)이다. 보통은 설치할 때 사용자 PC에서 직접 컴파일(node-gyp)하는데, 컴파일러가 없으면 실패한다. 프리빌트는 이걸 미리 빌드해 npm에 올려 두는 방식이라 설치가 빠르고 안정적이다. N-API(Node-API)는 Node 버전이 올라가도 다시 컴파일 안 해도 되게 하는 안정적 C 인터페이스로, 한 번 빌드한 바이너리가 Node 22·24에서 그대로 돈다.

차별점 ② — 오프라인·온디바이스: 이미지가 기기를 떠나지 않는다

설치 후에는 네트워크가 전혀 필요 없다. 이미지도, 인식 결과도 전부 로컬에서 처리된다. 개인정보가 담긴 문서·영수증·신분증을 다루는 앱이라면 이건 기능이 아니라 규정 준수(compliance) 요건이다. 클라우드 OCR로는 넘기 어려운 벽을, 라이브러리 하나로 넘는다.

비유

번역기를 쓰는 두 방법. 클라우드 OCR은 편지를 통째로 번역 회사에 우편으로 보내 결과를 받는 것 — 빠르고 정확하지만 편지 내용을 남이 본다. 파이썬 OCR은 집에 번역가를 상주시키는 것 — 정확하지만 그 사람 먹이고 재우는(파이썬 환경 관리) 비용이 크다. light-ocr은 번역 사전 한 권을 책장에 꽂아 두는 것과 같다. 설치 한 번이면 그 뒤로는 집 안에서(오프라인) 언제든 펴 보고, 누구에게도 편지를 보이지 않는다.

차별점 ③ — 하드웨어 가속을 기본으로, 그런데 결과는 CPU와 동일하게

기본 모드(createEngine())는 플랫폼에 맞는 가속기를 자동 선택한다: Apple Silicon macOS 15+는 Core ML(뉴럴 엔진), Linux/Windows x64는 WebGPU. 실측(v0.3.0)으로 Apple M4 Max에서 최대 2.85배, NVIDIA RTX 5060 Ti(WebGPU/Vulkan)에서 5.70배 빨라졌다. 인상적인 건 WebGPU 결과가 CPU FP32와 바이트 단위로 동일했다는 점 — "빠른데 결과가 달라지는" 흔한 함정을 품질 게이트로 막았다.

그래서 트렌딩
"Node 개발자가 늘 아쉬웠던 자리"를 정확히 채웠다

JS 생태계에는 "쓸 만한 정확도 + 쉬운 설치 + 오프라인"을 동시에 주는 OCR이 사실상 없었다. light-ocr은 그 빈자리를 프리빌트 네이티브 모듈이라는 정공법으로 메웠고, 여기에 하드웨어 가속·SHA-256 무결성 검증·에이전트 스킬까지 얹어 "장난감이 아니라 프로덕션용"이라는 인상을 준다. 신생 레포임에도 빠르게 별이 붙은 이유다.

주의 · 오해 방지
"light-ocr이 OCR 모델을 만든 것"은 아니다

인식 정확도의 원천은 바이두의 PP-OCRv6 Small 모델이다. light-ocr은 그 모델을 실행·포장하는 엔지니어링 레이어다. 또한 지원 범위에 한계가 있다: JPEG/PNG만(WebP·GIF·PDF·TIFF 불가), 라인 단위 좌표만(글자 단위 아님), glibc 리눅스만(musl/Alpine 불가), Bun 미검증. "만능 OCR"이 아니라 "특정 환경에서 아주 잘 도는 OCR"로 이해해야 한다.

3기술 스택 전체 지도 — C++ 코어·N-API·npm 패키징

C++ 추론 코어 층 · N-API 어댑터 층 · npm 배포(파사드+플랫폼+모델) 층 · 가속 층

light-ocr은 "프론트/백엔드"로 나눌 웹 앱이 아니다. 스택을 이해하려면 세 개의 층으로 보는 게 빠르다: 실제 OCR을 계산하는 C++ 코어, 그걸 JS에 잇는 N-API 어댑터, 그리고 이 모든 걸 사용자에게 배달하는 npm 패키징. 여기에 가속기(Core ML/WebGPU)가 코어 밑에 끼어든다.

① C++ 코어 층 — light_ocr::core (C++17)

손으로 짠 C++17 정적 라이브러리가 심장이다. 외부 딥러닝 프레임워크 없이, 추론은 ONNX Runtime에 맡기고 이미지 처리·후처리는 직접 구현했다. 의존성은 전부 버전 고정(models/deps.lock.json)이다.

구성요소버전역할
ONNX Runtime (CPU)1.22.0모든 플랫폼 공통 추론 엔진. .onnx 모델 실행
ONNX Runtime WebGPU EP1.24.4 / 플러그인 0.1.0Linux·Windows x64 GPU 가속 실행 공급자
OpenCV4.10.0 (core+imgproc만, 정적)이미지 리사이즈·크롭·원근 변환(warp)
Clipper6.4.2DBNet 후처리에서 폴리곤 확장(unclip) — 글자 박스 넓히기
stb (stb_image)고정 커밋JPEG/PNG 디코딩(헤더 온리)
nlohmann/json3.11.3디스크립터·툴·테스트의 JSON 파싱
용어
추론(inference) · 실행 공급자(Execution Provider, EP)
추론은 이미 학습된 모델에 입력을 넣어 결과를 뽑는 과정이다(학습의 반대). 실행 공급자(EP)는 ONNX Runtime이 "이 연산을 어디서 계산할까"를 정하는 백엔드 플러그인이다. CPUExecutionProvider는 CPU에서, CoreMLExecutionProvider는 애플 뉴럴 엔진에서, WebGpuExecutionProvider는 GPU에서 돌린다. light-ocr의 execution.provider: 'auto'기기에 맞는 EP를 자동 선택하고, 실패하면 CPU로 안전하게 되돌아간다(fallback).

② N-API 어댑터 층 — bindings/node

구성요소내용역할
addon.cpp2321줄N-API 바인딩·엔진별 워커 스레드·요청 큐·완료 디스패처
Node-API버전 8Node 버전 독립 ABI. 한 번 빌드로 22/24 공용
js/index.cjs·.mjs·.d.tsPromise·AbortSignal 래퍼, 타입 정의
load-native.cjs플랫폼 감지 + SHA-256·ABI 계약 검증 후 로드
bin/light-ocr.cjs무의존 CLI(recognize/detect/info)

이 층의 핵심은 뒤(4장)에서 자세히 볼 비동기 모델이다. 무거운 C++ 연산을 JS 메인 스레드 밖에서 돌리면서도, 결과를 안전하게 Promise로 돌려준다.

③ npm 패키징 층 — 파사드 + 플랫폼 + 모델

가장 영리한 부분. 하나의 얇은 파사드 패키지가, 무거운 바이너리·모델을 별도 패키지로 쪼개 두고 런타임에 필요한 것만 고른다. binding.gyp도, node-gyp도, post-install 스크립트도 전혀 없다 — 빌드는 순수 CMake다.

@arcships/light-ocr # ① 파사드(JS만). 사용자는 이것만 설치 │ optionalDependencies (런타임에 맞는 것 1개만 실제 사용) ├─ light-ocr-darwin-arm64 ┐ ├─ light-ocr-darwin-x64 │ ├─ light-ocr-win32-x64 │ ② 플랫폼 프리빌트 (.node 바이너리) 6종 ├─ light-ocr-win32-arm64 │ ├─ light-ocr-linux-x64-gnu │ ├─ light-ocr-linux-arm64-gnu ┘ └─ light-ocr-model-ppocrv6-small # ③ 모델(.onnx) 패키지
용어
파사드 패턴 · optionalDependencies
파사드(facade)는 "복잡한 내부를 감춘 단순한 정문"이라는 뜻이다. 사용자는 @arcships/light-ocr 하나만 설치하지만, 그 뒤에서 OS·CPU에 맞는 바이너리가 골라진다. optionalDependencies는 npm 기능으로, "설치하다 실패해도 전체가 안 죽는" 의존성이다. 그래서 내 맥에는 맥용 바이너리만 깔리고 윈도우용은 조용히 건너뛴다. 리눅스 x64용 .node를 굳이 내 맥에 받을 필요가 없다.

④ 가속 층 — Core ML · WebGPU (FP16 파생 모델)

기본 모델은 CPU용 FP32다. WebGPU 가속을 위해 저자는 이 모델을 FP16으로 변환한 파생 모델을 따로 만들어 두되(검출 4.97MB·인식 10.6MB), GPU가 잘 못 하는 연산(NonMaxSuppression·TopK 등)은 FP32·CPU에 남기는 "op 블록 리스트"를 적용했다. 그 결과가 앞서 말한 "GPU인데 CPU와 바이트 동일"의 비결이다.

플랫폼Auto 모드 가속
macOS Apple SiliconCore ML(macOS 15+) → 실패 시 CPU
macOS IntelCPU(인텔 Core ML은 정확도 검증 실패로 제외)
Linux x64 (glibc)WebGPU(Vulkan) → CPU
Windows x64WebGPU(D3D12) → CPU
Linux·Windows arm64CPU (WebGPU 플러그인 미제공)

4아키텍처 심화 — 스레드 한 개로 C++를 JS에 안전하게 잇기

엔진마다 전용 워커 스레드 · 단일 완료 디스패처 · DBNet→CTC OCR 파이프라인

이 레포에서 가장 배울 게 많은 부분이 바로 여기다. "무거운 C++ 작업을 Node의 이벤트 루프를 막지 않고 어떻게 돌리는가"라는 오래된 문제를, light-ocr은 교과서와 다른 방식으로 푼다.

흔한 방법이 아니다 — AsyncWorker 대신 전용 스레드

보통 네이티브 애드온은 Napi::AsyncWorker로 작업을 libuv 스레드풀에 던진다. light-ocr은 대신 엔진 하나당 전용 std::thread 워커 한 개를 띄우고, 그 스레드가 FIFO 큐를 blocking으로 지키며 요청을 하나씩 처리한다.

// addon.cpp — 엔진 워커 스레드의 메인 루프 (개념 축약)
void EngineState::run() {
  load_bundle();                          // 모델 1회 로드
  while (true) {
    std::shared_ptr<Request> request;
    {
      std::unique_lock<std::mutex> lock(mutex);
      changed.wait(lock, [this]{         // 큐에 일 생길 때까지 잠듦
        return environment_closing || !queue.empty() || close_requested; });
      if (environment_closing) break;
      if (!queue.empty()) { request = queue.front(); queue.pop_front(); }
      else if (close_requested) break;
    }
    // 디코드 → (ROI 크롭) → core->recognize()/detect() → 결과를 JS 스레드로
    post_completion(context, run_ocr(request));
  }
}
용어
이벤트 루프 · 논블로킹 · 워커 스레드
Node.js는 단일 메인 스레드에서 이벤트 루프를 돌려 수많은 요청을 번갈아 처리한다. 여기서 무거운 C++ 연산(OCR 수백 ms)을 그냥 실행하면 그동안 서버 전체가 멈춘다(블로킹). 그래서 그 일을 워커 스레드(별도 실행 흐름)로 빼고, 끝나면 결과만 메인 스레드로 돌려주면 이벤트 루프는 계속 다른 일을 한다(논블로킹). light-ocr은 "엔진 1개 = 워커 스레드 1개"로 이 격리를 준다.

결과를 JS로 돌려주는 다리 — threadsafe function

워커 스레드에서 계산한 결과를 JS 스레드로 넘기는 건 까다롭다. JS 엔진(V8) 객체는 다른 스레드에서 만지면 안 되기 때문이다. light-ocr은 환경당 단 하나의 napi_threadsafe_function(완료 디스패처)을 두어, 어느 워커든 결과를 이 통로로 밀어 넣으면 안전하게 JS 스레드에서 Promise가 resolve된다. 놀지 않을 땐 이벤트 루프를 잡아 두지 않도록 unref 처리한다.

// 모듈 초기화: 환경당 완료 디스패처 1개 생성
napi_create_threadsafe_function(env, nullptr, nullptr, resource_name,
    kCompletionQueueCapacity /*64*/, 1, context.get(),
    finalize_dispatcher, nullptr, call_js, &context->dispatcher);
napi_unref_threadsafe_function(env, context->dispatcher);  // 유휴 시 루프 안 붙잡음

백프레셔·취소·메모리 회계까지

단순 비동기가 아니라 프로덕션급 흐름 제어가 들어 있다. 큐는 기본 4개(최대 64)로 경계가 있고(backpressure), 대기 중 입력 바이트가 기본 256MiB를 넘으면 거절한다. 각 요청은 네이티브 Promise + BigInt requestId를 돌려주고, JS의 AbortSignalnative.cancel(requestId)를 불러 진행 중 작업을 취소할 수 있다. 이미지 스냅샷은 napi_adjust_external_memory로 회계 처리해 GC가 메모리 압력을 알게 한다.

// js/index.cjs — AbortSignal로 취소 가능한 Promise 래퍼
#recognize(nativeMethod, image, options) {
  const operation = this.#native[nativeMethod](image, nativeOptions); // {requestId, promise}
  return new Promise((resolve, reject) => {
    const onAbort = () => {
      try { this.#native.cancel(operation.requestId); } catch {}
      settle(reject, abortReason(signal));
    };
    operation.promise.then(v => settle(resolve, v),
                           e => settle(reject, normalizeNativeError(e)));
    if (signal) signal.addEventListener('abort', onAbort, { once: true });
  });
}
설계 포인트
C++ 예외가 절대 N-API 경계를 넘지 않는다

워커의 run()은 모든 걸 try/catch로 감싸, C++ 예외가 JS 쪽으로 새어 나가 프로세스를 죽이는 일을 원천 차단한다. 치명적 오류는 napi_fatal_error로만 처리하고, 환경 종료 시 cleanup 훅이 모든 엔진 워커를 안전하게 배수(drain)한다. "네이티브 모듈은 크래시 위험"이라는 통념을, 방어적 설계로 정면 반박하는 부분이다.

그 안에서 도는 OCR 파이프라인 — 검출 → 인식 → 디코드

실제 core->recognize() 내부는 고전적 2-스테이지 OCR이다. 글자가 어디 있는지 찾고(검출, DBNet), 그 자리를 잘라 무슨 글자인지 읽는다(인식, CRNN/SVTR + CTC).

입력 이미지(JPEG/PNG) │ stb 디코드 + EXIF 회전 보정 + (선택)ROI 크롭 ▼ ① 검출 DETECTION ─ DBNet ├ 리사이즈(최대 변 960) → NCHW 텐서화 ├ ONNX Runtime 실행 → 확률맵(probability map) ├ db_postprocess: 이진화 → 윤곽선 → Clipper unclip(박스 확장) → 사각형(quad) └ (선택)tiled: 큰 이미지를 타일로 쪼개 작은 글자까지 → IoU/IoS NMS로 병합 ▼ sort_reading_order # 사람이 읽는 순서로 줄 정렬 ▼ ② 인식 RECOGNITION ─ CRNN/SVTR ├ 글자 영역 원근 변환 크롭(perspective warp) → 폭 기준 배치 묶기 ├ ONNX Runtime 실행 → 시퀀스 로짓(logits) └ decode_ctc: CTC 그리디 디코드(blank 제거·중복 축약) → 글자 + 신뢰도 ▼ 결과 OcrResult { lines:[{ text, confidence, box[4점] }], timingUs(단계별 µs) }
용어
DBNet · CTC 디코드
DBNet(Differentiable Binarization)은 "이 픽셀이 글자일 확률"을 그린 맵을 내놓는 글자 검출 방식이다. 그 맵을 이진화해 윤곽을 따고, Clipper로 살짝 넓혀(unclip) 글자를 감싸는 사각형을 만든다. CTC(Connectionist Temporal Classification)는 인식 단계에서 쓴다. 모델은 가로로 훑으며 "h-h-e-l-l-l-o" 같은 중복·공백 섞인 열을 내는데, CTC 디코드가 공백(blank)을 지우고 중복을 합쳐 "hello"로 만든다. 글자 길이를 미리 몰라도 되는 게 핵심이다.

로드 시점의 공급망 보안 — SHA-256 + ABI 계약

네이티브 바이너리·모델을 npm으로 배포하는 건 공급망 공격의 표적이 될 수 있다. light-ocr은 load-native.cjs가 로드 전에 runtime-descriptor.json(스키마 2.0)을 검증한다: 아티팩트마다 SHA-256 해시·바이트 수 대조, 경로 탈출·심볼릭 링크 거부, 그리고 JS가 계산한 정책과 네이티브가 내보낸 runtimeContract를 맞춰 보는 ABI 계약 핸드셰이크까지. "신뢰할 수 있는 바이너리만 로드"를 코드로 강제한다.

5디렉토리 구조 해부

public 헤더 + src(C++ 코어) + bindings/node(어댑터) + models(락파일) + 무거운 검증 자료
light-ocr/ ├── CMakeLists.txt # 루트 빌드: light_ocr::core 정적 라이브러리 + 툴/테스트/node ├── CMakePresets.json · cmake/ ├── README.md · README.zh-CN.md · AGENTS.md · LICENSE(Apache-2.0) · NOTICE ├── include/light_ocr/ # ★ public C++ API: core.hpp · types.hpp · error.hpp ├── src/ # ★ C++17 코어 (~380KB) │ ├── core/engine.cpp # 1286줄 — OCR 파이프라인 오케스트레이터 │ ├── detection/ # db_postprocess.cpp(DBNet 후처리) · tiled.cpp(타일 검출/병합) │ ├── recognition/ctc_decode.cpp# CTC 그리디 디코드 │ ├── inference/ # ★ 추론 백엔드 추상화 │ │ ├── backend.hpp # InferenceSession 인터페이스 │ │ ├── onnxruntime/ # ORT 세션 · WebGPU EP 등록 │ │ ├── coreml/backend.mm # Apple Core ML (Objective-C++) │ │ └── selection.hpp # Auto 공급자 선택 + 생성 추적 │ ├── preprocess/ # image.cpp(디코드/EXIF/크롭) · tensor.cpp(NCHW 텐서화) │ ├── model/ · geometry/ · result/ │ └── util/ # sha256.cpp · checked_math.hpp(오버플로 안전 산술) ├── bindings/node/ # ★ N-API 어댑터 (~364KB) │ ├── CMakeLists.txt · package.json(@arcships/light-ocr 0.3.2) │ ├── src/addon.cpp # 2321줄 — 바인딩·워커·큐·디스패처 │ │ + win_delay_load_hook.cpp # Electron 등 호스트 심볼 해석 │ ├── js/ (index.cjs/.mjs/.d.ts · load-native.cjs · exif.cjs) │ ├── bin/light-ocr.cjs # CLI 진입점 │ └── test/ # node --test 스위트 ├── models/ # bundles.lock.json · deps.lock.json · webgpu-fp16 파생 모델 ├── .agents/skills/local-ocr/ # AI 에이전트용 스킬(SKILL.md) ├── contracts/ · oracle/ · corpus/ # 계약·기준값·테스트 이미지 ├── reports/ # 실기기 WebGPU 검증 리포트(대용량) └── tools/ · tests/ # 벤치·검증·leak_check · unit/integration/fuzz

구조가 말하는 핵심 세 가지. 첫째, C++ 코어(src/)와 Node 어댑터(bindings/node/)가 깔끔히 분리돼 있어, C++ 라이브러리만 떼어 다른 언어에 붙일 수도 있다. 둘째, src/inference/추상 인터페이스(backend.hpp) + 구체 구현(onnxruntime·coreml)으로 갈려 있어 가속기 추가가 쉽다. 셋째, 신생 레포치고 검증 자료(reports/·corpus/·fuzz)가 이례적으로 무겁다 — 품질에 진심이라는 신호다.

핵심 파일왜 중요한가
src/core/engine.cpp (1286줄)디코드→검출→정렬→인식→디코드 전 과정을 엮는 지휘자
bindings/node/src/addon.cpp (2321줄)N-API 표면·워커 스레드·큐·완료 디스패처 — 이 레포의 백미
src/inference/onnxruntime/backend.cppORT 세션 생성, WebGPU EP 라이브러리 등록·해시 재검증
src/detection/db_postprocess.cpp확률맵 → 윤곽 → Clipper unclip → 사각형 박스
js/load-native.cjs플랫폼 감지 + SHA-256/ABI 계약 검증 후 .node 로드
include/light_ocr/core.hpppublic API. 모든 함수가 예외 대신 Result<T> 반환
함정 · 착시
"OCR 라이브러리"라 열었는데 딥러닝 코드가 안 보인다

이 레포엔 신경망 학습 코드도, 모델 구조 정의도 전혀 없다. 모델은 이미 학습된 .onnx 파일(별도 패키지)이고, 코드는 그걸 실행하고 전후처리할 뿐이다. "OCR 정확도 로직"을 찾지 말고, 추론 엔진 호출 + 이미지 처리 + Node 바인딩이라는 세 축으로 머릿속 모델을 잡아야 길을 잃지 않는다.

6학습 포인트 — 여기서 훔쳐올 설계

N-API 비동기 패턴 · 네이티브 모듈 패키징 · 공급망 무결성 · OCR 파이프라인

배울 것 ① — 교과서와 다른 N-API 비동기 패턴

이 레포 최고의 학습 자산. AsyncWorker/libuv 스레드풀 대신 엔진당 전용 스레드 + 참조 카운트된 단일 threadsafe function 디스패처를 쓰는 방식은, 오래 도는 C++ 작업을 JS에 노출하는 정교한 실전 레퍼런스다. 네이티브 Promise, BigInt requestId 취소, 경계 있는 FIFO 백프레셔, napi_adjust_external_memory 회계 — 하나하나가 프로덕션 네이티브 모듈의 필수 요소다.

실습 아이디어
"무거운 C++ 함수"를 논블로킹 Promise로 감싸는 최소 애드온

피보나치·소수 판정처럼 CPU를 오래 먹는 C++ 함수를, 전용 스레드 + threadsafe function으로 감싸 await addon.compute(n)이 이벤트 루프를 막지 않게 만들어 보라. light-ocr의 addon.cpp 워커 루프를 축소 복제하면, "왜 그냥 동기 함수를 export하면 안 되는가"를 몸으로 이해하게 된다.

배울 것 ② — 네이티브 모듈을 현대적으로 패키징하기

파사드 + 플랫폼별 optionalDependency 바이너리 + 별도 모델 패키지, 전부 프리빌트(node-gyp·binding.gyp 없이 CMake만), Electron용 delay-load 훅까지. 이는 "설치할 때 컴파일"하던 옛 방식과 극명히 대비되는, npm에 네이티브 코드를 배달하는 모범 청사진이다. C/C++/Rust로 짠 무언가를 JS 생태계에 팔고 싶은 사람에게 그대로 참고서가 된다.

배울 것 ③ — 추상 인터페이스로 백엔드를 갈아 끼우기

InferenceSession이라는 순수 추상 인터페이스 아래 ORT·Core ML 구현이 갈려 있고, execution.provider: 'auto'가 기기에 맞는 걸 골라 실패 시 CPU로 안전 강등한다. "하드웨어마다 다른 백엔드를, 상위 코드는 모르게 추상화하는" 이 구조는 플러그형 아키텍처의 좋은 표본이다.

배울 것 ④ — 성능을 얻되 정확성은 계약으로 지키기

WebGPU FP16로 5배 빨라지면서도 CPU FP32와 바이트 동일을 품질 게이트로 요구한 점은 인상적이다. GPU가 약한 연산은 CPU에 남기는 "경계 있는 CPU 파티션", 14장 이미지 전수 대조 — "빠른데 슬그머니 결과가 달라지는" 가속의 함정을 어떻게 규율하는지 보여 준다.

배울 것 ⑤ — 네이티브 배포의 공급망 보안

아티팩트별 SHA-256 검증, 버전 있는 디스크립터 스키마, 경로 탈출·심링크 방어, JS↔네이티브 ABI 계약 핸드셰이크. npm으로 신뢰 가능한 바이너리를 배포하는 법의 레퍼런스다. 여기에 예외 대신 Result<T>, noexcept 추론 경계, 오버플로 안전 checked_math까지 — 방어적 시스템 C++의 표본이기도 하다.

용어
ABI 계약 · Result<T> 패턴
ABI(응용 이진 인터페이스)는 컴파일된 코드끼리 "함수를 어떻게 부르고 데이터를 어떻게 주고받을지" 약속한 규격이다. JS가 기대하는 규격과 네이티브 .node가 실제 제공하는 규격이 어긋나면 조용히 crash가 나는데, light-ocr은 로드 시 둘을 명시적으로 대조(핸드셰이크)해 그 사고를 막는다. Result<T>는 "성공값 T 또는 오류"를 한 타입에 담아 반환하는 패턴으로, 예외를 던지지 않아 C++ 예외가 언어 경계를 넘는 위험 자체를 없앤다.

7시스템 / 실행 요구사항

사용자는 Node 22/24만 · 소스 빌드엔 CMake+C++17 · 가속은 플랫폼별
용도요구사항
설치·실행(사용자)Node.js 22 또는 24. 프리빌트라 컴파일러·파이썬·다운로드 불필요. 플랫폼 바이너리+모델 자동 선택
지원 플랫폼macOS arm64/x64 · Linux x64/arm64(glibc만, musl/Alpine 불가) · Windows x64/arm64
하드웨어 가속Core ML: Apple Silicon + macOS 15+ · WebGPU: Linux x64(Vulkan)·Windows x64(D3D12). arm64·인텔맥은 CPU
입력 형식JPEG · PNG 또는 원시 픽셀(gray8/rgb8/bgr8/rgba8). WebP·GIF·PDF·TIFF 미지원
소스 빌드CMake ≥ 3.24 · Ninja · C++17 컴파일러. ONNX Runtime 1.22.0/OpenCV 4.10.0 등은 의존성 캐시로 페치. Node 애드온은 -DLIGHT_OCR_BUILD_NODE=ON
GPU 메모리경량 모델(FP16 검출 4.97MB·인식 10.6MB)이라 부담 작음. WebGPU는 Linux에서 DRM 렌더 노드, Windows에서 dxcompiler.dll 필요

실행 옵션이 이 라이브러리의 "손잡이"를 보여 준다. createEngine({ execution: { provider: 'auto'|'cpu'|'apple'|'webgpu', precision: 'auto'|'fp32'|'fp16', sessionFallback: 'error'|'cpu' } })로 가속을 세밀 제어하고, detection: { strategy: 'bounded'|'tiled' }고해상도 속 작은 글자를 타일 방식으로 잡는다. 인식은 recognitionBatchSize·recognitionScoreThreshold로, 리소스는 queueCapacity·maxPendingInputBytes로 조인다.

주의
"어디서나 되는" 라이브러리가 아니다 — 배포 전 매트릭스 확인

가장 흔한 실전 함정은 musl 리눅스(Alpine 도커 이미지)다. 많은 컨테이너가 Alpine 기반인데 light-ocr은 glibc만 지원해 unsupported_platform을 던진다. 도커로 배포한다면 node:22-slim(Debian/glibc) 계열을 써야 한다. 또 arm64 서버·인텔 맥은 GPU 가속이 없어 CPU로만 돈다는 점도 성능 계획에 반영해야 한다.

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

설치·인식부터 CLI 파이프라인, 나만의 비동기 애드온까지
과제 1난이도 ★☆☆☆☆

설치 한 번으로 영수증 텍스트 뽑기

npm install @arcships/light-ocr(Node 22/24) 후, 1장의 코드 예시 그대로 아무 영수증·명함 JPEG를 넣어 result.lines를 출력한다. 각 줄의 text·confidence·box(4점 좌표)를 눈으로 확인하고, optionalDependencies내 OS용 바이너리만 깔았는지 node_modules/@arcships/를 열어 확인한다.

과제 2난이도 ★★☆☆☆

CLI로 이미지 폴더 일괄 OCR + JSONL 파이프라인

내장 CLI(npx light-ocr recognize --format jsonl img.png)로 폴더 속 이미지를 순회해 줄 단위 JSONL로 내보내고, jq로 신뢰도 0.9 이상만 거른다. --region x,y,w,h로 특정 영역만, --provider cpu|webgpu로 백엔드를 바꿔 종료 코드(0/64~72)가 어떻게 오류를 구분하는지 관찰한다.

과제 3난이도 ★★★☆☆

가속 벤치마크 — CPU vs WebGPU/Core ML, 결과 동일성 검증

같은 이미지 세트를 provider: 'cpu''auto'로 각각 돌려 속도(result.timingUs)를 비교하고, 두 결과의 text완전히 같은지 대조한다. light-ocr이 자랑하는 "가속하되 CPU와 바이트 동일"을 직접 재현하며, 왜 그게 어려운 약속인지 체감한다.

과제 4난이도 ★★★★☆

동시 요청 + AbortSignal 취소 스트레스 테스트

엔진 하나에 수십 개 recognize를 동시에 던져 큐 백프레셔(기본 4, queueCapacity)와 maxPendingInputBytes 거절을 유발해 본다. 일부 요청에 AbortSignal을 걸어 중간 취소하고, 그 사이 이벤트 루프가 안 막히는지(다른 타이머가 계속 도는지) 확인한다. light-ocr의 비동기 설계가 왜 필요한지 몸으로 아는 과제.

과제 5난이도 ★★★★★

나만의 "전용 스레드 + threadsafe function" 애드온 만들기

light-ocr을 참고해, 무거운 C++ 함수를 엔진당 전용 std::thread + 단일 napi_threadsafe_function으로 감싸는 최소 네이티브 모듈을 CMake만으로(node-gyp 없이) 빌드한다. 네이티브 Promise 반환, requestId 취소, 경계 있는 큐까지 넣고, SHA-256으로 .node 무결성을 검증한 뒤 로드하는 load 게이트를 흉내 내 본다. 이 레포의 핵심 엔지니어링을 스스로 재현하는 캡스톤.

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

4주 코스 — OCR 파이프라인 → ONNX 추론 → N-API 비동기 → 네이티브 배포
주차주제 · 목표핵심 자료
1주차 OCR 파이프라인 이해. 검출(DBNet)과 인식(CRNN/SVTR)의 2단계 구조, CTC 디코드, 원근 변환 크롭, 읽기 순서 정렬, 타일 검출·NMS src/core/engine.cpp · db_postprocess.cpp · PaddleOCR PP-OCRv6 문서
2주차 ONNX Runtime 추론. .onnx 모델 실행, 실행 공급자(CPU/CoreML/WebGPU), FP16 변환·op 블록 리스트, NCHW 텐서화 src/inference/ · models/*.lock.json · ONNX Runtime 공식 문서
3주차 Node-API 비동기. 워커 스레드, napi_threadsafe_function, 네이티브 Promise, AbortSignal 취소, 외부 메모리 회계, 예외 안전 경계 bindings/node/src/addon.cpp · js/index.cjs · Node-API 공식 문서
4주차 네이티브 모듈 배포. CMake로 .node 빌드, 파사드+플랫폼별 프리빌트 패키징, SHA-256·ABI 계약 검증, Electron delay-load bindings/node/CMakeLists.txt · load-native.cjs · prebuildify/napi 생태계
곁들이면 좋은 배경
"언제 네이티브고 언제 WASM·클라우드인가"까지 알아야 진짜 이해

네이티브 애드온은 빠르지만 플랫폼 매트릭스(OS×arch×libc)라는 배포 부담을 진다. 브라우저·엣지가 목표면 WASM(Tesseract.js·ONNX Runtime Web)이, 확장성·무관리가 목표면 클라우드 OCR이 낫다. light-ocr이 택한 "서버·데스크톱 Node + 오프라인" 자리를 세 대안과 비교해 보면, 기술 선택이 곧 트레이드오프 선택임이 선명해진다.

10핵심 키워드 사전

이 저장소를 읽을 때 반복해서 만나는 말들
키워드
OCR · PP-OCRv6 Small
이미지 속 글자를 텍스트로 바꾸는 기술. light-ocr은 바이두 PaddleOCR의 경량 모델 PP-OCRv6 Small(검출 ~9.88MB·인식 ~21MB FP32)을 실행한다. 모델은 별도 npm 패키지로 배포.
키워드
ONNX Runtime · Execution Provider
학습된 .onnx 모델을 파이썬 없이 C++에서 실행하는 마이크로소프트 추론 엔진. EP는 연산을 어디서 계산할지 정하는 백엔드(CPU/CoreML/WebGPU)로, light-ocr의 provider: 'auto'가 자동 선택·CPU 폴백한다.
키워드
N-API · 네이티브 애드온 · 프리빌트
N-API는 Node 버전이 바뀌어도 재컴파일 없이 도는 안정 인터페이스. 네이티브 애드온은 C/C++로 짠 .node 확장. 프리빌트는 이를 미리 빌드해 npm에 올려 설치 시 컴파일을 없앤 방식.
키워드
napi_threadsafe_function
다른 스레드(워커)에서 JS 스레드로 안전하게 콜백을 넘기는 N-API 장치. light-ocr은 환경당 이걸 1개 두어, 어느 워커든 계산 결과를 이 통로로 밀어 Promise를 resolve한다. 유휴 시 unref로 이벤트 루프를 안 붙잡는다.
키워드
이벤트 루프 · 백프레셔(backpressure)
Node의 단일 메인 스레드가 요청을 번갈아 처리하는 구조가 이벤트 루프. 백프레셔는 처리 속도보다 요청이 빨리 쌓일 때 큐 크기·바이트를 제한해 거절하는 흐름 제어로, light-ocr은 큐 기본 4개·대기 256MiB 상한을 둔다.
키워드
DBNet · CTC 디코드
DBNet은 "글자일 확률맵"으로 글자 위치를 검출하는 방식(→ 이진화 → 윤곽 → Clipper unclip → 사각형). CTC는 인식 단계에서 모델이 낸 중복·공백 섞인 열을 blank 제거·중복 축약으로 최종 텍스트로 만드는 디코드 알고리즘.
키워드
Core ML · WebGPU · FP16
하드웨어 가속 백엔드. Core ML은 애플 뉴럴 엔진, WebGPU는 Vulkan(리눅스)·D3D12(윈도우) GPU를 쓴다. FP16(반정밀 부동소수)은 GPU에서 빠른 대신 정밀도가 낮아, light-ocr은 약한 연산만 CPU·FP32로 남겨 결과 동일성을 지킨다.
키워드
파사드 + optionalDependencies
사용자는 얇은 파사드 패키지 하나만 설치하고, OS·CPU에 맞는 프리빌트 바이너리(6종)와 모델 패키지가 optionalDependencies로 붙어 런타임에 자동 선택된다. 설치 실패가 전체를 죽이지 않는 게 핵심.
키워드
ABI 계약 · SHA-256 검증
로드 시 JS가 기대한 규격과 네이티브 .node가 제공하는 규격(ABI)을 대조하는 핸드셰이크, 그리고 아티팩트마다 SHA-256 해시로 위변조를 검증하는 게이트. 신뢰 가능한 바이너리만 로드하도록 강제한다.
키워드
Result<T> · noexcept 경계
"성공값 또는 오류"를 한 타입에 담아 예외를 던지지 않고 반환하는 패턴. C++ 예외가 N-API 언어 경계를 넘어 프로세스를 죽이는 사고를 막는다. light-ocr의 public API 전역 규약.

11참고 링크

원본과 배경 기술을 직접 확인하고 싶다면

저장소 · 배포

핵심 배경 기술

C++ · 빌드 · OCR 이론