Claude Vision API로 OCR 없이 비정형 문서를 구조화하는 방법 — 인보이스 자동화부터 멀티 이미지 분석까지
OCR 라이브러리를 붙이고, 정규식으로 파싱하고, 그래도 안 되면 수작업 보정 팀을 두는 클래식한 문서 처리 파이프라인. 떠올리면 쓴웃음이 나옵니다. 저도 스캔 인보이스 처리 자동화를 맡았다가 공급업체마다 다른 레이아웃에 백기를 들고 템플릿을 47개쯤 만들었던 기억이 있거든요. 심지어 한 거래처는 같은 공급업체인데 분기마다 레이아웃이 바뀌었습니다. 그 순간 "이건 사람이 할 짓이 아니다"라는 생각이 들었고, Claude Vision API를 처음 써봤을 때 그 답답함이 꽤 많이 풀리는 느낌이었습니다.
단순히 텍스트를 인식하는 OCR과는 결이 다릅니다. 이미지 안의 맥락과 구조를 함께 이해하는 방식이 완전히 다르거든요. 이 글에서는 Claude Vision API가 기존 비전 파이프라인과 어떻게 다른지, 그리고 인보이스 추출부터 멀티 이미지 비교 분석까지 실무에서 바로 가져다 쓸 수 있는 코드 패턴을 중심으로 이야기합니다.
솔직히 말하면 이게 모든 상황에서 정답은 아닙니다. 비용도 무시할 수 없고, 베타 기능도 섞여 있고, 제약도 분명히 있습니다. 그 부분까지 정직하게 다루겠습니다.
핵심 개념
"비전 모델"이 아니라 "추론하는 모델에 눈을 달았다"는 차이
Claude Vision은 별도의 이미지 인식 모델이 아닙니다. Claude의 언어 추론 아키텍처에 시각 입력이 통합된 구조입니다. 이 차이가 실무에서 체감으로 드러납니다.
전통적인 OCR + NLP 파이프라인은 "글자를 추출하고, 그 글자를 언어 모델이 처리"하는 두 단계입니다. 반면 Claude Vision은 이미지를 보면서 동시에 추론합니다. 손글씨 주석이 달린 계약서 페이지에서 "이 조항이 어떤 의무를 발생시키나요?"라고 물으면, OCR 결과물을 넘겨받아 처리하는 게 아니라 이미지 자체를 보고 법적 맥락까지 파악해서 답합니다. 제가 실제로 써봤을 때 이 차이가 가장 크게 느껴진 건, 금액 옆에 빨간 펜으로 "확인 필요"라고 쓴 메모까지 함께 파악하더라는 거였습니다.
멀티모달(Multimodal): 텍스트, 이미지, 오디오 등 여러 형태의 입력을 함께 처리할 수 있는 AI 모델 특성. Claude Vision은 텍스트와 이미지를 동일 컨텍스트 안에서 처리합니다.
API 구조 — Messages API에 이미지 블록을 추가하는 것뿐
구조 자체는 단순합니다. 기존에 텍스트 메시지만 보내던 messages.create 호출에서 content 배열에 "type": "image" 블록을 추가하면 됩니다.
import anthropic
import base64
client = anthropic.Anthropic() # ANTHROPIC_API_KEY 환경변수를 읽습니다
with open("invoice.png", "rb") as f:
# standard_b64encode는 b64encode의 명시적 이름으로, 동작은 동일합니다
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": image_data,
},
},
{
"type": "text",
"text": "이 인보이스에서 공급업체명, 청구 날짜, 총액을 JSON으로 추출해줘."
},
],
}],
)
print(message.content[0].text)이미지를 텍스트보다 앞에 배치하는 게 성능상 권장되는 순서입니다. 공식 문서에서도 이미지 → 텍스트 순을 권장합니다. 처음에 저도 텍스트를 먼저 쓰다가 나중에 알게 됐는데, 체감 차이가 있더라고요.
3가지 이미지 입력 방식과 선택 기준
| 방식 | 언제 쓰면 좋을까 |
|---|---|
base64 인코딩 |
보안 민감 데이터, 외부 URL이 없는 환경 |
url 참조 |
이미 CDN에 올라간 이미지, 프로토타이핑 |
file_id (Files API, 베타) |
동일 이미지 반복 사용, 배치 처리 비용 최적화 |
Files API 방식이 아직 낯선 분들이 많은데, 같은 이미지를 여러 번 분석하거나 배치 처리할 때 base64로 매번 보내는 것보다 대역폭과 비용을 크게 아낄 수 있습니다. 다만 client.beta.files는 현재 베타 네임스페이스에 있는 기능이라 하위 호환성이 보장되지 않을 수 있다는 점은 미리 알고 쓰시는 게 좋습니다.
# Files API (베타) — 한 번 업로드하고 file_id로 재사용
with open("template_invoice.png", "rb") as f:
response = client.beta.files.upload(
file=("template_invoice.png", f, "image/png"),
)
file_id = response.id # 이 ID를 저장해두면 반복 전송 불필요
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "file",
"file_id": file_id,
},
},
{"type": "text", "text": "레이아웃 구조를 설명해줘."},
],
}],
)토큰 과금 방식 — 이미지 크기가 곧 비용
이미지는 해상도(메가픽셀) 기준으로 토큰이 산정됩니다. 공식 문서 기준으로 대략 1MP당 약 1,380 토큰, 최소 약 300 토큰입니다. 1568px 정사각형 이미지 하나가 약 2,450 토큰 정도입니다.
2026년 6월 기준 모델별 입력·출력 토큰 단가는 아래와 같습니다.
| 모델 | 입력 토큰 단가 | 출력 토큰 단가 | 특징 |
|---|---|---|---|
claude-opus-4-8 |
$5/M | $15/M | 최고 성능, 최대 2576px |
claude-sonnet-4-6 |
$3/M | $15/M | 균형형, 대부분 문서 처리에 적합 |
claude-haiku-4-5 |
$1/M | $5/M | 경량·저비용, 단순 분류 작업 |
Sonnet 기준으로 이미지 하나당 약 $0.007 정도입니다. 하루에 수천 장 처리하는 파이프라인이라면 해상도 최적화가 실제 비용에 직결됩니다. 비교 대상인 GPT-4o의 2026년 6월 기준 입력 단가는 $2.50/M 수준으로 Claude가 상대적으로 비싸긴 하지만, Haiku를 쓰거나 캐싱·배치 API를 조합하면 실질 비용 차이는 크게 줄어듭니다.
지금 써도 될까? — 한 문단 요약
솔직히 정형화된 문서(레이아웃이 고정된 인보이스, 표준 양식)라면 기존 OCR + 규칙 기반 파서가 여전히 빠르고 저렴합니다. Claude Vision이 진가를 발휘하는 건 비정형·다양한 레이아웃의 문서, 맥락 해석이 필요한 이미지, 손글씨나 주석이 섞인 자료입니다. 인물 식별은 불가능하고, PDF 직접 입력도 안 됩니다. 베타 기능도 섞여 있습니다. 이 제약들이 허용 가능한 범위라면, 실무 투입 가치는 충분합니다.
실전 적용
시작 전 준비물:
pip install anthropic으로 SDK를 설치하고, Anthropic Console에서 API 키를 발급받아ANTHROPIC_API_KEY환경변수로 설정해두면 아래 예제들을 바로 돌려볼 수 있습니다.
예시 1: 비정형 인보이스에서 구조화된 데이터 추출
실무에서 가장 자주 맞닥뜨리는 시나리오입니다. 공급업체마다 레이아웃이 달라서 템플릿 기반 파서로는 한계가 있을 때 Claude Vision이 진가를 발휘합니다. 저도 같은 공급업체인데 금액 필드명이 "합계", "청구금액", "Total Amount KRW"처럼 12가지 변형이 있는 걸 보고 결국 이쪽으로 갈아탔습니다.
import anthropic
import base64
import json
import re
client = anthropic.Anthropic()
def extract_invoice_data(image_path: str) -> dict:
with open(image_path, "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
ext = image_path.rsplit(".", 1)[-1].lower()
media_type_map = {
"jpg": "image/jpeg", "jpeg": "image/jpeg",
"png": "image/png", "webp": "image/webp",
}
media_type = media_type_map.get(ext, "image/png")
try:
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": image_data,
},
},
{
"type": "text",
"text": """이 인보이스 이미지에서 다음 정보를 추출해 JSON으로만 응답해줘.
다른 설명 없이 JSON 객체만 반환해야 해.
{
"vendor_name": "공급업체명",
"invoice_number": "인보이스 번호",
"invoice_date": "발행일 (YYYY-MM-DD)",
"due_date": "납부기한 (YYYY-MM-DD, 없으면 null)",
"subtotal": 공급가액 숫자,
"tax": 세액 숫자,
"total": 총액 숫자,
"currency": "통화 코드 (KRW, USD 등)",
"line_items": [
{"description": "품목명", "quantity": 수량, "unit_price": 단가, "amount": 금액}
]
}
필드를 찾을 수 없으면 null로 채워줘."""
},
],
}],
)
except anthropic.APIError as e:
raise RuntimeError(f"API 호출 실패: {e}") from e
raw = message.content[0].text.strip()
# 모델이 ```json ... ``` 형태로 감싸서 응답하는 경우 제거
raw = re.sub(r"^```(?:json)?\n?", "", raw)
raw = re.sub(r"\n?```$", "", raw).strip()
try:
return json.loads(raw)
except json.JSONDecodeError as e:
raise ValueError(f"JSON 파싱 실패 (응답 앞부분: {raw[:100]}...)") from e
result = extract_invoice_data("invoice_sample.png")
print(json.dumps(result, ensure_ascii=False, indent=2))몇 가지 설계 포인트를 짚어보겠습니다.
media_type_map: 확장자별 MIME 타입을 자동으로 판별합니다. 잘못된 타입을 지정하면 API 에러가 납니다.- JSON 스키마를 프롬프트에 명시: 필드명과 타입을 정확히 지정하면 파싱 오류가 크게 줄어듭니다.
null을 미리 지정해두면 필드 누락 시KeyError도 방지됩니다. - 마크다운 코드 블록 제거: 단순히
.split("```")[1]로 자르면json\n...이 남아json.loads()가 실패합니다. 정규식으로 앞뒤를 깔끔하게 제거하는 방식이 더 견고합니다. try/except분리:anthropic.APIError와json.JSONDecodeError를 분리해서 잡아두면 어디서 실패했는지 바로 알 수 있어 디버깅이 편합니다.
예시 2: 배치 처리로 비용 50% 절감
하루에 수백~수천 장 이미지를 처리한다면 Batch API가 훨씬 경제적입니다. 응답이 즉시 오지 않고 몇 시간 내 처리되는 비동기 방식이지만, 가격이 절반입니다. 저는 월말 인보이스 일괄 처리 배치에 이 방식을 써서 운영 비용이 눈에 띄게 줄었습니다.
Batch API: 요청을 묶어서 비동기로 처리하는 API. 즉시 응답이 필요 없는 대량 처리 작업에서 50% 비용 절감 효과가 있습니다. 프롬프트 캐싱(최대 90% 절감)과 함께 쓰면 운영 비용을 크게 낮출 수 있습니다.
import anthropic
import base64
import time
import json
client = anthropic.Anthropic()
def build_batch_request(image_path: str, custom_id: str) -> dict:
with open(image_path, "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
return {
"custom_id": custom_id,
"params": {
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": image_data,
},
},
{
"type": "text",
"text": "인보이스에서 공급업체명과 총액을 JSON으로 추출해줘."
},
],
}],
},
}
image_files = [
("inv_001.png", "invoice-001"),
("inv_002.png", "invoice-002"),
("inv_003.png", "invoice-003"),
]
requests = [build_batch_request(path, cid) for path, cid in image_files]
batch = client.messages.batches.create(requests=requests)
print(f"배치 ID: {batch.id}, 상태: {batch.processing_status}")
# 완료까지 폴링 (운영 환경에서는 웹훅이나 스케줄러를 쓰는 게 더 깔끔합니다)
while True:
status = client.messages.batches.retrieve(batch.id)
if status.processing_status == "ended":
break
counts = status.request_counts
print(f"처리 중 — 완료: {counts.succeeded}, 오류: {counts.errored}, 대기: {counts.processing}")
time.sleep(60)
# 결과 파싱
results = {}
for result in client.messages.batches.results(batch.id):
if result.result.type == "succeeded":
raw = result.result.message.content[0].text.strip()
try:
results[result.custom_id] = json.loads(raw)
except json.JSONDecodeError:
results[result.custom_id] = {"error": "파싱 실패", "raw": raw}
else:
results[result.custom_id] = {"error": result.result.error.type}
print(json.dumps(results, ensure_ascii=False, indent=2))time.sleep(60) 폴링 루프는 "일단 동작 확인"용입니다. 실제 운영 환경에서는 Celery Beat나 AWS EventBridge 같은 스케줄러로 주기적으로 상태를 확인하는 방식이 더 적합합니다.
예시 3: 멀티 이미지 UI 버전 비교 분석
단일 요청에 이미지를 여러 장 포함할 수 있습니다. UI 버전 비교, 차트 시계열 분석, Before/After 검토 같은 시나리오에서 유용합니다. 저는 이걸 QA 자동화에 활용했는데, 디자이너가 "v2에서 버튼 패딩이 바뀌었나요?"라고 물어봤을 때 스크린샷 두 장을 넣고 바로 답변받을 수 있었습니다.
import anthropic
import base64
client = anthropic.Anthropic()
def load_image(path: str) -> dict:
with open(path, "rb") as f:
data = base64.standard_b64encode(f.read()).decode("utf-8")
return {
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": data,
},
}
try:
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
load_image("ui_v1.png"),
{"type": "text", "text": "첫 번째 이미지는 v1 UI입니다."},
load_image("ui_v2.png"),
{
"type": "text",
"text": """두 번째 이미지는 v2 UI입니다.
두 버전의 레이아웃 변경점을 비교하고,
사용성 측면에서 개선되거나 우려되는 부분을 정리해줘."""
},
],
}],
)
print(message.content[0].text)
except anthropic.APIError as e:
print(f"API 오류: {e}")장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 통합 추론 | 별도 비전 모델 없이 이미지 이해 + 언어 추론이 동일 컨텍스트에서 처리됨 |
| 긴 컨텍스트 | 최대 1M 토큰 — 수백 페이지 문서를 한 세션에서 다룰 수 있음 |
| 고해상도 | Opus 4.7+ 기준 최대 2576px, 전문 도면·스크린샷 분석에 실용적 |
| 멀티 이미지 | 단일 요청에 이미지 여러 장 — 비교·시계열 분석 가능 |
| 비용 최적화 | 프롬프트 캐싱(최대 90%) + Batch API(50%) 조합 시 실질 비용 대폭 절감 |
| 보안 | base64 방식은 이미지 데이터가 API 호출 내에만 존재, 외부 URL 미노출 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 인물 식별 불가 | 이미지 속 인물 이름 식별을 거부 (개인정보 보호 정책) | 인물 식별이 필요한 경우 전용 솔루션 필요 |
| PDF 직접 입력 불가 | PDF 파일을 그대로 전달할 수 없음 | pdf2image, pymupdf 등으로 페이지별 이미지 변환 후 전송 |
| 소형 이미지 성능 저하 | 200px 미만 이미지에서 정확도 저하, 저화질·회전 이미지에서 환각 가능 | 전처리(리사이즈, 회전 보정, 대비 향상) 후 전송 |
| 상대적으로 높은 단가 | 2026년 6월 기준 GPT-4o 입력 $2.50/M 대비 Sonnet $3/M, Opus $5/M | Haiku($1/M) 활용, 캐싱·배치 조합으로 절감 |
| GIF 첫 프레임만 처리 | 애니메이션 GIF 분석 불가 | 필요 프레임을 추출해 개별 이미지로 전송 |
| Files API가 베타 | client.beta.files 네임스페이스는 하위 호환성 미보장 |
프로덕션 도입 전 버전 고정 및 릴리스 노트 모니터링 |
환각(Hallucination): AI 모델이 실제로 없는 내용을 있는 것처럼 생성하는 현상. 이미지가 흐리거나 회전되어 있을 때 텍스트를 잘못 읽는 형태로 나타날 수 있습니다. 저는 팩스 수신 문서에서 이 문제를 겪었는데, 전처리 단계에서 해상도 보정을 추가한 뒤 훨씬 안정적으로 동작했습니다.
실무에서 가장 흔한 실수
- 프롬프트에 이미지를 텍스트 뒤에 배치하는 것 — 공식 권장 순서는 이미지 먼저, 텍스트 나중입니다. 처음에 저도 이 순서를 반대로 했다가 나중에 알게 됐는데, 성능 차이가 체감됩니다.
- 고해상도 원본을 그대로 전송하는 것 — 분석 품질 향상이 미미한데 토큰 비용이 크게 늘어납니다. 1568px 이하로 리사이즈하는 것을 권장합니다.
- JSON 응답 파싱 시 마크다운 코드 블록 처리를 빠뜨리는 것 — 모델이 응답을
```json ... ```형태로 감싸는 경우가 있어 그대로json.loads()에 넣으면 파싱 에러가 납니다. 위 예제처럼 정규식으로 앞뒤를 제거하는 방어 코드를 미리 넣어두시면 좋습니다. - 에러 핸들링을 생략하는 것 — API 호출 실패나 JSON 파싱 오류는 실무에서 반드시 발생합니다. 프로토타입 단계부터
try/except를 구조화해두면 나중에 디버깅 시간을 크게 줄일 수 있습니다.
마치며
저도 처음엔 "또 다른 AI API겠지"라고 생각했는데, 공급업체 인보이스 47종을 템플릿 없이 처리하는 걸 보고 생각이 바뀌었습니다. 핵심은 이미지를 "읽는" 게 아니라 "맥락과 의미를 함께 이해한다"는 점이고, 그 차이가 비정형 문서·손글씨 주석·다양한 레이아웃에서 가장 크게 드러납니다.
지금 바로 시작해볼 수 있는 3단계를 제안합니다:
- 첫 이미지 분석 실행: 위 인보이스 추출 예제 코드를 가져다가 가지고 있는 영수증이나 스캔 문서를 넣어 돌려볼 수 있습니다. JSON 스키마를 원하는 필드에 맞게 바꿔보면 바로 감이 옵니다.
- 비용 최적화 적용: 같은 이미지를 반복 사용하는 흐름이라면 Files API(베타)로
file_id를 캐싱하고, 배치 처리가 가능한 작업에는 Batch API를 적용해볼 수 있습니다. 두 가지를 함께 쓰면 운영 비용이 눈에 띄게 달라집니다. - 프로덕션 도입 전 검토: 베타 기능 의존도를 확인하고, 이미지 해상도 전처리와 에러 핸들링을 붙인 뒤 실제 데이터로 정확도를 검증해보시면 안정적인 파이프라인을 구성할 수 있습니다.
참고 자료
- Vision - Claude API 공식 문서
- Models Overview - Claude API Docs
- Best practices for using vision with Claude | Claude Cookbook
- Claude Vision for Document Analysis - A Developer's Guide | GetStream
- Claude Vision API: Image Analysis At Production Scale | Developers Digest
- Claude Vision API: Analyzing Images and Documents at Scale | CallSphere
- Claude API Integration Guide 2025 | Collabnix
- DeepSeek OCR vs Claude Vision: A Deep Dive into Accuracy | Sparkco
- Claude API in Python Cheat Sheet | DataCamp
- Claude Opus 4.7 Guide | Codersera
- AI API Pricing Comparison 2026 | IntuitionLabs
- Claude Vision API 实战完全指南 | ClaudeAPI.com