에디터에 문서 인식 도구를 설치하는 데는 1분이면 됩니다. 그다음 무엇에 쓸지 아는 데는 더 오래 걸립니다. 이 글은 설치 안내를 건너뛰고, 여권이 얽힌 일에서 개발자들이 실제로 Claude Code와 Cursor에 맡기는 세 가지 작업으로 바로 들어갑니다.
- 여권 스캔 이미지 폴더를 CSV로 바꾸기. 중간에 멈춰도 이어서 할 수 있는 방식으로.
- 만들고 있는 가입 양식과 여권 한 권을 대조하기.
- 스캔 결과를 한 번만 기록해 테스트 픽스처로 쓰기. 그러면 테스트 스위트가 실제 서비스를 호출할 일이 없습니다.
세 가지 모두 doc.cheap의 MCP 서버, 즉 @doc-cheap/mcp 패키지(MIT, Node 20 이상)를 사용합니다. 이 글은 그 서버 뒤에 있는 여권·신분증 OCR API인 doc.cheap의 자체 블로그이므로, 제품 선택에 관한 부분은 그 점을 감안해 읽어 주세요. 패턴 자체(울타리를 친 이미지 디렉터리, 파일마다 하나의 키, 한 번 기록한 픽스처)는 이런 종류의 어떤 도구에도 적용됩니다.
서버가 에디터에 제공하는 것
여섯 가지 도구가 있고, 각 도구에는 클라이언트가 먼저 사용자에게 물어볼지 정하기 전에 읽는 힌트가 붙어 있습니다.
| 도구 | 하는 일 | 크레딧 차감? |
|---|---|---|
scan_document |
여권, 신분증, 운전면허증을 읽어 구조화된 JSON으로 반환 | 문서가 인식된 경우에만 |
check_balance |
남은 크레딧과 이번 달 카운터 | 아니요 |
search_docs |
서버에 포함된 API 문서를 오프라인으로 검색 | 아니요 |
list_scans |
계정에 저장된 스캔을 최신순으로 나열 | 아니요 |
get_scan |
저장된 스캔 하나를 id로 다시 읽기 | 아니요 |
delete_scan |
저장된 스캔 하나를 영구 삭제 | 아니요 |
scan_document는 이미지를 image_base64, image_url, 또는 로컬 서버에서는 image_path로 받고, 선택 옵션으로 expect_country, return_portrait, retain_hours, reference, idempotency_key를 받습니다. 아래 워크플로의 대부분은 이 다섯 가지 옵션이 떠받칩니다.
키를 설정하지 않으면 서버는 공개 샌드박스 키를 씁니다. 주소당 총 10건까지 문서를 무료로 인식하고, 요청은 주소당 시간당 최대 10건입니다. 워크플로 2와 3에는 이것으로 충분합니다. 워크플로 1에는 본인 키가 필요하며, 가입하면 매달 100회의 무료 인식이 제공됩니다.
MCP 서버를 에이전트에 연결해 본 적이 없다면, MCP로 에이전트가 신분증을 읽게 하기에서 기본 사항과 개인정보 문제를 더 자세히 다룹니다.
워크플로 1: 스캔 폴더를 CSV로 (Claude Code)
작업. 누군가 여권 스캔 폴더(호텔에 밀린 투숙객 자료, 행사 참석자 명단, 종이 기록물의 이전 작업)를 건네며 스프레드시트를 원합니다. 파일당 한 행, 필요한 필드, 그리고 읽지 못한 파일마다 분명한 메모가 담긴 것입니다.
설정. 프로젝트 루트에 .mcp.json을 둡니다. Claude Code는 이를 프로젝트 전용 서버 목록으로 읽기 때문에, 설정이 저장소와 함께 따라다닙니다.
{
"mcpServers": {
"doc-cheap": {
"command": "npx",
"args": ["-y", "@doc-cheap/mcp"],
"env": {
"DOC_CHEAP_API_KEY": "${DOC_CHEAP_API_KEY:-}",
"DOC_CHEAP_IMAGE_ROOT": "/Users/you/guest-backlog/scans"
}
}
}
}
그 폴더에서 처음 시작할 때 Claude Code는 .mcp.json의 서버를 승인할지 묻습니다.
핵심은 두 줄입니다.
- **
DOC_CHEAP_API_KEY**는 셸에서 가져오므로 키가 커밋되지 않습니다. 변수가 설정되지 않았으면 값이 비고, 서버는 샌드박스 키로 돌아갑니다. - **
DOC_CHEAP_IMAGE_ROOT**는 서버가 파일을 읽을 수 있는 유일한 디렉터리입니다. 이 값이 없으면image_path는 곧바로 거부됩니다. 이 값이 있으면 경로를 먼저 심볼릭 링크를 따라 해석하고, 디렉터리 밖으로 벗어나는 것(.., 바깥을 가리키는 링크)은 거부합니다. 상대 경로image_path는 이 디렉터리를 기준으로 하므로, 에이전트는 파일 이름만 넘기면 됩니다.
이 울타리는 다른 어디보다 여기서 중요합니다. 파일 이름은 에이전트가 정하고, 에이전트는 여러분이 만들지 않은 폴더를 읽고 있기 때문입니다. 이미지가 대화를 거치는 일도 없습니다. 서버가 파일을 읽어 API로 바로 보냅니다.
프롬프트.
먼저 check_balance를 호출해 줘. 그다음 스캔 디렉터리의 모든 JPEG와 PNG에 대해 image_path는 파일 이름, reference도 파일 이름, idempotency_key는 "backlog-" 뒤에 파일의 SHA-256을 붙인 값, return_portrait는 false, retain_hours는 24로 해서 scan_document를 호출해 줘. file, status, billed, kind, country, number, surname, given_names, birth_date, expiry_date, is_expired, mrz_status 열로 backlog.csv를 작성해 줘. status가 recognized가 아니면 데이터 열은 비워 둬. 마지막에 인식되지 않은 파일과 그 이유를 나열해 줘.
각 옵션이 있는 이유:
- **
reference**는 결과의meta.reference로 그대로 돌아오고list_scans의 모든 행에 표시되므로, 저장된 각 스캔을 열어 보지 않고도 원래 파일까지 추적할 수 있습니다. idempotency_key덕분에 배치를 안전하게 재개할 수 있습니다. 실서비스 키에서는 같은 키로 다시 보내면 다시 인식하고 과금하는 대신 처음 결과를 돌려줍니다. 300개 중 140번째 파일에서 세션이 끊기면 같은 프롬프트를 다시 실행하세요. 처음 139개는 비용 없이 재생 결과로 돌아오고, 작업은 거기서부터 이어집니다. 무작위 UUID가 아니라 파일 해시로 만든 키이기 때문에 새 세션에서도 같은 키를 재현할 수 있습니다. Claude Code는shasum -a 256으로 이를 계산할 수 있습니다.- **
retain_hours: 24**는 멱등성 가이드가 설명하는 절충입니다. 재생은 저장된 결과를 돌려주므로,retain_hours: 0으로 한 스캔은 재생할 수 없습니다. 24시간 안에 그 키로 재시도하면idempotency_replay_unavailable로 거부됩니다. 하루 보관 기간이면 재시도는 무료로 유지되면서도 다음 날에는 아무것도 남지 않습니다. - **
return_portrait: false**는 소지자 사진을 잘라 낸 이미지를 빼 줍니다. CSV에는 필요 없기 때문입니다.
돌아오는 결과. 각 행의 status는 recognized, no_document_found, unreadable, unsupported_document, rejected 중 하나이고, billed는 그 파일이 크레딧을 썼는지 알려 줍니다. 과금되는 것은 recognized뿐이므로, 흐릿한 사진이 스무 장 섞인 폴더는 파일 수보다 크레딧이 스무 개 덜 듭니다. billed 열은 배치가 쓴 양을 직접 센 기록이기도 해서, 청구서와 맞춰 볼 필요가 없습니다.
습관 하나 더. 이제 CSV에는 개인정보가 들어 있습니다. 에이전트가 파일을 쓰기 전에 .gitignore에 추가하세요. 쓴 다음이 아니라요.
워크플로 2: 양식을 만들면서 여권 한 권 대조하기 (Cursor)
작업. 가입 양식이나 체크인 양식을 만들고 있고, 필드 이름과 형식, 유효성 검사를 정하기 전에 실제 스캔이 무엇을 돌려주는지 보고 싶은 상황입니다.
설정. 가볍게 살펴보는 정도라면 Cursor는 설치가 필요 없는 호스팅 서버를 쓸 수 있습니다. 프로젝트의 .cursor/mcp.json에 다음과 같이 적습니다.
{
"mcpServers": {
"doc-cheap": {
"url": "https://mcp.doc.cheap/mcp"
}
}
}
키 헤더가 없으면 호스팅 서버는 샌드박스 키를 씁니다. 호스팅 서버는 여러분 컴퓨터의 파일을 볼 수 없으므로, 거기서는 scan_document가 이미지를 image_base64나 공개된 https: image_url로 받습니다. 견본이 로컬 파일이라면 대신 서버를 로컬에서 실행하세요. Cursor는 변수를 자체 문법으로 씁니다. 프로젝트 폴더는 ${workspaceFolder}, 셸 변수는 ${env:NAME}이므로, 워크플로 1의 항목을 그대로 복사하지 않고 다음처럼 씁니다.
{
"mcpServers": {
"doc-cheap": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@doc-cheap/mcp"],
"env": {
"DOC_CHEAP_IMAGE_ROOT": "${workspaceFolder}/specimens"
}
}
}
}
DOC_CHEAP_API_KEY 줄이 없으면 이 항목도 샌드박스 키를 쓰고, 서버는 프로젝트의 specimens 폴더에서만 이미지를 읽습니다. 여기서는 합성 문서나 견본 문서를 쓰고, 본인 여권은 절대 쓰지 마세요.
프롬프트.
td3-specimen.jpg를 scan_document로 스캔해 줘. 결과를 components/OnboardingForm.tsx의 필드와 비교해서, 양식의 모든 필드가 스캔 결과로 채워지도록 변경안을 제안해 줘. 확실하지 않은 필드는 search_docs로 확인해 줘.
비교하면 보통 드러나는 것들과, 결과 구조가 그에 답하는 방식:
- 이름은 나뉘어서 옵니다.
holder.surname과holder.given_names는 따로 있고,holder.full_name이 둘을 합칩니다. "성명" 입력란 하나만 두면 나중에 백오피스에서 필요할 성과 이름의 구분을 잃습니다. - 날짜는 이미 ISO
YYYY-MM-DD입니다.holder.birth_date와document.expiry_date는<input type="date">에 바로 들어갑니다. 이 입력란의 값도 같은 형식을 쓰기 때문입니다.DD.MM.YYYY나 MRZ 고유의 여섯 자리 날짜를 파싱할 필요가 없습니다. - 국가는 코드와 세부 정보로 옵니다.
holder.nationality는 문서에 적힌 세 글자 코드이고,holder.nationality_detail이 ISO alpha-3과 alpha-2 형식을 더해 줍니다. 국가 선택기가 두 글자 코드를 쓴다면 직접 표를 만들지 말고 세부 정보 객체에서 변환하세요. - 만료 여부는 계산되어 있습니다.
document.is_expired와document.days_remaining(만료 후에는 음수)으로 날짜 계산 없이 양식에서 만료된 여권을 경고할 수 있습니다. - 어떤 값이든 null일 수 있습니다. 모든 키는 항상 존재하고, 알 수 없는 값은
null이며 키가 빠지는 일은 없습니다. 모든 필드가 온다고 가정한 양식은 처음 만나는 낡은 여권에서 깨집니다. - MRZ 판정은 필드와 별개입니다.
mrz.status는passed,failed,absent중 하나입니다. 기계 판독 영역을 읽었는지, 검증 숫자가 맞는지를 알려 줄 뿐, 문서가 진짜라는 뜻은 아닙니다. 결과의authenticity그룹은not_checked로 표시됩니다.
이 과정에서 search_docs는 쓸 만한 가치가 있습니다. 서버에 포함된 문서를 읽고, 오프라인으로 동작하며, 비용이 들지 않습니다. 그래서 "unsupported_document는 무슨 뜻인가" 또는 "신분증에는 어떤 필드가 있나" 같은 질문에 추측이 아니라 문서에 근거한 답을 얻습니다.
워크플로 3: 테스트 픽스처를 한 번만 기록하기
작업. 여러분의 코드는 API를 호출하지만, 그 테스트까지 호출해서는 안 됩니다. 실행할 때마다 여권을 실제 서비스로 보내는 테스트 스위트는 느리고, CI에 키가 필요하며, 샌드박스에서는 금세 한도에 걸립니다. 시간당 10건의 요청 제한, 그리고 같은 문서를 너무 여러 번 보내면 나오는 document_repeated 거부입니다.
해법은 코드가 처리해야 할 결과를 각각 한 번씩 기록해 JSON으로 저장하고, 테스트는 그 파일을 대상으로 돌리는 것입니다.
프롬프트(Claude Code와 Cursor 어느 쪽이든):
td3-specimen.jpg와 blank-page.png를 scan_document로 스캔하고, 각 구조화된 결과를 test/fixtures/scan-
.json으로 저장해 줘. 개인 정보 값(이름, 번호, 생년월일, MRZ 행)은 모두 같은 유형의 자리표시자로 바꾸고, images 객체의 데이터 URL은 지워 줘.
도구의 구조화된 결과는 HTTP API가 돌려주는 것과 같은 Scan 객체이므로, 픽스처는 프로덕션에서 코드가 받게 될 것과 일치합니다. 마스킹한 픽스처는 다음과 같습니다.
{
"meta": { "status": "recognized", "billed": false, "reference": null },
"document": { "kind": "passport", "country": "<ICAO3>", "number": "<NUMBER>", "expiry_date": "<YYYY-MM-DD>", "is_expired": false, "days_remaining": 0 },
"holder": { "surname": "<SURNAME>", "given_names": "<GIVEN NAMES>", "birth_date": "<YYYY-MM-DD>", "nationality": "<ICAO3>" },
"mrz": { "status": "passed", "reason": null, "lines": ["P<…", "…"], "text": "P<…" }
}
(여기서는 줄였습니다. 실제 결과에 있는 키는 모두 남겨 두세요. 그래야 코드가 없는 키를 읽을 때 테스트가 실패합니다.)
픽스처를 정직하게 만드는 세 가지:
- 실패도 기록하세요. 빈 페이지는
billed: false와 함께no_document_found를 돌려주고, 데이터 그룹은null입니다. 스캔에는 항상 소지자가 있다고 가정한 코드를 잡아내는 것이 바로 이 파일입니다. - 샌드박스에서
billed는 예측값입니다. 샌드박스에서는 아무것도 과금되지 않으며, 이 플래그는 같은 스캔이 실서비스 키였다면 과금되었을지를 알려 줍니다. 결제 테스트가 검증해야 할 것이 바로 이것입니다. - 실서비스 키에서는 다시 스캔하지 말고 다시 읽으세요.
list_scans는 reference나 시각으로 이전 스캔을 찾고,get_scan은 처음 반환된 그대로 돌려줍니다. 이미지를 다시 인식하지도, 과금하지도 않습니다. 차이점은 두 가지입니다.images의 모든 칸은null이고quality는not_checked로 표시됩니다. 잘라 낸 이미지는 절대 저장되지 않기 때문입니다.
에디터 대신 스크립트로 기록하고 싶다면, 같은 호출을 일반 HTTP로 하면 다음과 같습니다.
curl -X POST https://api.doc.cheap/v1/scans \
-H "Authorization: Bearer sk_sandbox_public" \
-H "Content-Type: application/json" \
-d "{\"image\": \"$(base64 < specimens/td3-specimen.jpg | tr -d '\n')\"}" \
-o test/fixtures/scan-recognized.json
커밋하기 전에 파일을 마스킹하세요.
워크플로별 필요한 것
| 키 | 서버 | 이미지가 API로 가는 출처 | |
|---|---|---|---|
| 폴더를 CSV로 | 본인 키(10건 초과 시) | 로컬, npx |
울타리를 친 디렉터리 |
| 양식 대조 | 샌드박스로 충분 | 호스팅 또는 로컬 | URL 또는 울타리를 친 디렉터리 |
| 테스트 픽스처 | 샌드박스로 충분 | 어느 쪽이든 | 울타리를 친 디렉터리, 한 번 |
Windsurf, VS Code, Gemini CLI, Kiro를 포함한 에디터별 설정은 Claude와 Cursor를 위한 OCR MCP 서버 페이지와 MCP 서버 가이드에 있습니다. 아직 서비스를 고르는 중이라면 여권 OCR API 비교에서 각 서비스가 공개한 내용을 기준으로 doc.cheap을 다른 서비스와 나란히 볼 수 있습니다.
이 워크플로 중 하나가 여러분의 에디터에서 제대로 동작하지 않거나, 글로 쓸 만한 네 번째 워크플로가 있다면 admin@doc.cheap으로 알려 주세요.
여러분께 질문: 에디터의 에이전트에게 주로 무엇을 맡기시나요 – 문서를 한 번에 하나씩, 아니면 폴더 전체를? 아래 댓글로 알려 주세요.