요즘 LLM은 더 이상 텍스트만 읽지 않습니다. 영수증 사진을 던지면 항목과 금액을 표로 정리하고, 에러가 뜬 스크린샷을 보여주면 무엇이 잘못됐는지 짚어내며, 매출 차트 이미지에서 수치를 읽어 JSON으로 돌려줍니다. 이런 멀티모달(multimodal) 입력은 OCR 파이프라인을 직접 짜거나 차트 좌표를 파싱하던 작업을 단 한 번의 API 호출로 압축해 줍니다. 이 글에서는 Claude 비전 API를 중심으로, 이미지 블록을 텍스트와 함께 넘기는 법부터 문서·스크린샷·차트 분석, 비전과 구조화 출력을 결합한 이미지→JSON 추출, 그리고 토큰 비용과 해상도까지 Python과 PHP(Laravel) 실전 코드로 정리합니다.

멀티모달 입력의 기본: 이미지 블록 + 텍스트 블록

Claude에 이미지를 넘기는 방식은 간단합니다. 평소 messagescontent에 문자열 하나를 넣던 자리에, 블록 리스트를 넣고 그 안에 이미지 블록과 텍스트 블록을 섞으면 됩니다. 이미지 소스는 두 가지입니다.

  • base64 — 로컬 파일이나 메모리상의 바이트를 직접 인코딩해서 보냄. media_type(예: image/jpeg, image/png, image/gif, image/webp)을 함께 지정.
  • url — 공개적으로 접근 가능한 이미지 URL을 그대로 전달. Anthropic이 서버에서 받아옴.

가장 기본적인 형태부터 보겠습니다. 로컬 이미지를 base64로 인코딩해 "이 이미지에 무엇이 있는지" 묻는 코드입니다.

import base64
from anthropic import Anthropic

client = Anthropic()  # ANTHROPIC_API_KEY 환경변수 사용

with open("invoice.jpg", "rb") as f:
    image_b64 = base64.standard_b64encode(f.read()).decode("utf-8")

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/jpeg",
                        "data": image_b64,
                    },
                },
                {"type": "text", "text": "이 이미지에 무엇이 보이나요? 한국어로 간단히 설명해 주세요."},
            ],
        }
    ],
)

for block in msg.content:
    if block.type == "text":
        print(block.text)

이미지 블록이 먼저, 텍스트 질문이 뒤에 오는 배치를 권장합니다. Claude가 이미지를 먼저 "본" 다음 질문 맥락을 받도록 하는 편이 안정적입니다. URL 방식은 더 간단합니다. base64 인코딩 단계 없이 소스만 바꾸면 됩니다.

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "url",
                        "url": "https://example.com/screenshots/dashboard.png",
                    },
                },
                {"type": "text", "text": "이 대시보드에서 가장 눈에 띄는 이상 징후를 짚어 주세요."},
            ],
        }
    ],
)

base64 vs URL 선택 기준: 사용자가 업로드한 파일이나 사내 비공개 이미지는 base64가 정석입니다(외부에 URL을 노출할 필요가 없음). 반대로 이미 CDN에 올라가 있는 공개 이미지라면 URL이 페이로드를 가볍게 만듭니다. 단, base64는 요청 본문 크기를 키우므로 매우 큰 이미지는 요청 크기 제한(413 오류)에 걸릴 수 있습니다 — 이 경우 전송 전에 리사이즈하세요.

활용 1: 영수증·문서에서 데이터 추출

비전의 가장 실용적인 용도는 반정형 문서 추출입니다. 영수증, 명함, 송장, 계약서 스캔본처럼 레이아웃이 제각각인 이미지를 사람이 일일이 옮겨 적던 일을 자동화할 수 있습니다. 그냥 "표로 정리해 줘"라고 해도 되지만, 시스템에 넣을 거라면 다음 섹션의 구조화 출력과 묶는 것이 정답입니다.

참고로 Claude의 read 계열 도구 및 Files API는 PDF도 직접 다룰 수 있지만, 단순 이미지(JPEG/PNG 등)는 위에서 본 이미지 블록으로 충분합니다. 스캔 PDF를 페이지 이미지로 변환했다면 각 페이지를 이미지 블록으로 넘기면 됩니다.

활용 2: 스크린샷 분석 — UI·에러·로그

개발 현장에서 특히 강력한 용도입니다. 깨진 화면, 콘솔 에러, 그래프가 이상한 모니터링 대시보드를 캡처해 넘기면 Claude가 원인 후보를 짚어 줍니다. Opus 4.7부터 도입된 고해상도 비전 덕분에 작은 글씨가 많은 스크린샷의 텍스트 판독 정확도가 크게 올라갔습니다. 모델이 반환하는 좌표는 실제 이미지 픽셀과 1:1로 매핑되므로, 스크린샷 위 특정 요소의 위치를 묻는 작업에도 적합합니다.

import base64
from anthropic import Anthropic

client = Anthropic()

with open("error_screen.png", "rb") as f:
    shot = base64.standard_b64encode(f.read()).decode("utf-8")

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1500,
    system="당신은 시니어 백엔드 엔지니어입니다. 스크린샷의 에러를 분석하고 원인과 해결책을 단계별로 제시하세요.",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {"type": "base64", "media_type": "image/png", "data": shot},
                },
                {"type": "text", "text": "이 에러 화면의 원인이 무엇이고, 어떻게 고쳐야 하나요?"},
            ],
        }
    ],
)

print(msg.content[0].text)

여기서 system 파라미터로 역할을 지정한 점에 주목하세요. 비전 작업에서도 시스템 프롬프트로 "어떤 관점에서 이미지를 해석할지"를 지시하면 답변 품질이 눈에 띄게 좋아집니다.

활용 3: 차트·그래프 읽기

막대그래프, 선형 차트, 파이 차트 같은 시각화 이미지에서 수치를 읽어내는 것도 비전의 강점입니다. 원본 데이터가 없고 차트 이미지만 있을 때 — 예를 들어 PDF 리포트나 경쟁사 자료 캡처 — Claude에게 데이터 포인트를 transcribe하도록 시킬 수 있습니다. 단, 차트 판독은 본질적으로 추정이 섞이므로, 정밀도가 중요하면 "읽을 수 없는 값은 null로 표기하라"고 명시하고 결과를 검증하는 절차를 두세요.

차트 읽기야말로 자유 텍스트보다 JSON으로 강제 추출할 때 가치가 큽니다. 바로 다음 섹션에서 다룹니다.

비전 + 구조화 출력: 이미지 → JSON 추출

이미지에서 뽑아낸 데이터를 그대로 DB에 넣거나 다른 시스템에 흘려보내려면, 답변이 항상 같은 스키마의 유효한 JSON이어야 합니다. Claude의 구조화 출력 기능(output_config)을 쓰면 모델 응답을 지정한 JSON 스키마로 강제할 수 있습니다. 이것이 비전과 결합할 때 진가가 드러납니다 — 영수증 사진 한 장이 곧바로 파싱 가능한 레코드가 됩니다.

영수증에서 가맹점명, 날짜, 항목 리스트, 합계를 추출하는 예제입니다.

import base64, json
from anthropic import Anthropic

client = Anthropic()

with open("receipt.jpg", "rb") as f:
    receipt = base64.standard_b64encode(f.read()).decode("utf-8")

schema = {
    "type": "object",
    "properties": {
        "merchant": {"type": "string"},
        "date": {"type": "string"},
        "items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "quantity": {"type": "integer"},
                    "price": {"type": "number"},
                },
                "required": ["name", "quantity", "price"],
                "additionalProperties": False,
            },
        },
        "total": {"type": "number"},
        "currency": {"type": "string"},
    },
    "required": ["merchant", "date", "items", "total", "currency"],
    "additionalProperties": False,
}

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=2048,
    output_config={"format": {"type": "json_schema", "schema": schema}},
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {"type": "base64", "media_type": "image/jpeg", "data": receipt},
                },
                {"type": "text", "text": "이 영수증의 내용을 스키마에 맞춰 추출하세요. 읽을 수 없는 값은 합리적으로 추정하지 말고 빈 문자열 또는 0으로 두세요."},
            ],
        }
    ],
)

# 응답의 첫 텍스트 블록이 유효한 JSON
data = json.loads(msg.content[0].text)
print(data["merchant"], data["total"], data["currency"])
for item in data["items"]:
    print(f"  - {item['name']} x{item['quantity']}: {item['price']}")

몇 가지 주의점이 있습니다. 구조화 출력 스키마는 모든 객체에 additionalProperties: false가 필요하고, minimum·maxLength 같은 수치/길이 제약은 지원되지 않습니다(필요하면 클라이언트 측에서 검증). 또한 구조화 출력은 claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5, claude-fable-5에서 지원됩니다. 더 엄격하게 검증하고 싶다면 client.messages.parse(...)를 쓰면 스키마 검증까지 자동으로 해 줍니다.

Laravel(PHP)에서 비전 + 구조화 출력

독자 중 Laravel 개발자가 많으니 PHP SDK 예제를 짚고 가겠습니다. 설치는 composer require anthropic-ai/sdk이고, 핵심은 메서드 인자가 camelCase(maxTokens, outputConfig)라는 점입니다. 이미지 블록 내부의 media_type/type은 API 와이어 포맷 그대로 snake_case로 둡니다.

<?php

use Anthropic\Client;

$client = new Client(apiKey: getenv("ANTHROPIC_API_KEY"));

$imageData = base64_encode(file_get_contents(storage_path("app/receipts/receipt.jpg")));

$schema = [
    "type" => "object",
    "properties" => [
        "merchant" => ["type" => "string"],
        "date" => ["type" => "string"],
        "total" => ["type" => "number"],
        "currency" => ["type" => "string"],
    ],
    "required" => ["merchant", "date", "total", "currency"],
    "additionalProperties" => false,
];

$message = $client->messages->create(
    model: "claude-opus-4-8",
    maxTokens: 2048,
    outputConfig: [
        "format" => [
            "type" => "json_schema",
            "schema" => $schema,
        ],
    ],
    messages: [
        [
            "role" => "user",
            "content" => [
                [
                    "type" => "image",
                    "source" => [
                        "type" => "base64",
                        "media_type" => "image/jpeg",
                        "data" => $imageData,
                    ],
                ],
                [
                    "type" => "text",
                    "text" => "이 영수증을 스키마에 맞춰 JSON으로 추출하세요.",
                ],
            ],
        ],
    ],
);

// content는 폴리모픽 블록 배열 — 항상 타입을 확인하고 접근
foreach ($message->content as $block) {
    if ($block->type === "text") {
        $data = json_decode($block->text, true);
        break;
    }
}

// $data['merchant'], $data['total'] ...

PHP에서는 $message->content가 폴리모픽 블록 배열이므로, 첫 요소에 무작정 ->text로 접근하지 말고 $block->type === "text"를 확인한 뒤 꺼내는 습관이 중요합니다(다른 블록 타입이 먼저 올 수 있음).

여러 이미지 비교하기

한 번의 요청에 이미지 블록을 여러 개 넣을 수 있습니다. "전후 비교", "A안과 B안 중 어느 디자인이 나은지", "두 차트의 추세 차이" 같은 작업에 유용합니다. 이때 각 이미지에 라벨 텍스트 블록을 붙여 주면 Claude가 어떤 이미지를 가리키는지 명확해집니다.

import base64
from anthropic import Anthropic

client = Anthropic()

def load(path):
    with open(path, "rb") as f:
        return base64.standard_b64encode(f.read()).decode("utf-8")

before = load("dashboard_before.png")
after = load("dashboard_after.png")

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1500,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "다음은 배포 전(이미지 1)과 배포 후(이미지 2) 모니터링 대시보드입니다."},
                {"type": "text", "text": "이미지 1 (배포 전):"},
                {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": before}},
                {"type": "text", "text": "이미지 2 (배포 후):"},
                {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": after}},
                {"type": "text", "text": "두 시점 사이에 어떤 지표가 악화됐나요? 수치 변화를 근거로 설명해 주세요."},
            ],
        }
    ],
)

print(msg.content[0].text)

핵심은 텍스트 라벨로 이미지를 명시적으로 번호 매기는 것입니다. "이미지 1", "이미지 2"처럼 앵커를 달아 두면 답변에서 어느 쪽을 말하는지 혼동이 줄어듭니다. 여러 이미지를 넣을수록 토큰 비용이 합산되니, 비교에 꼭 필요한 이미지만 넣으세요.

이미지 토큰 비용과 해상도

이미지는 공짜가 아닙니다. 각 이미지는 해상도에 비례해 입력 토큰을 소모하며, 이 토큰은 모델의 입력 단가로 과금됩니다. 비용을 통제하려면 두 가지를 알아야 합니다.

  • 고해상도 처리(Opus 4.7+): 긴 변 기준 최대 약 2576픽셀까지 처리하며, 풀해상도 이미지 한 장은 이전 세대 대비 더 많은 토큰(최대 약 4,784 토큰)을 쓸 수 있습니다. 작은 글씨나 조밀한 차트의 판독 정확도가 필요할 때는 이 고해상도가 유리하지만, 그만큼 비용이 올라갑니다.
  • 불필요한 고해상도는 줄이기: 사진 속 큰 객체를 식별하는 정도라면 굳이 풀해상도를 보낼 이유가 없습니다. 전송 전에 클라이언트 측에서 리사이즈하면 토큰을 아낄 수 있습니다.

대략적인 추정에 의존하지 말고, 실제 비용을 알려면 토큰 카운팅 API로 측정하세요. tiktoken 같은 OpenAI용 토크나이저는 Claude에 부정확하니 쓰지 마세요.

import base64
from anthropic import Anthropic

client = Anthropic()

with open("chart.png", "rb") as f:
    img = base64.standard_b64encode(f.read()).decode("utf-8")

count = client.messages.count_tokens(
    model="claude-opus-4-8",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": img}},
                {"type": "text", "text": "이 차트의 데이터 포인트를 모두 추출하세요."},
            ],
        }
    ],
)

print(f"입력 토큰(이미지 포함): {count.input_tokens}")

현재 입력 단가는 모델별로 다릅니다 — Opus 4.8은 100만 입력 토큰당 $5, Sonnet 4.6은 $3, Haiku 4.5는 $1입니다. 단순 비전 분류·판독처럼 무거운 추론이 필요 없는 작업이라면 Haiku 4.5로 내려서 비용을 크게 낮출 수 있습니다. 반대로 복잡한 문서 추론이나 다중 이미지 비교는 Opus 4.8의 정확도가 값을 합니다. 대량의 영수증을 비실시간으로 처리한다면 Message Batches API로 묶어 비용을 더 절감할 수 있습니다.

실무 팁 요약

  • 이미지 먼저, 질문 나중: content 배열에서 이미지 블록을 앞에, 텍스트 지시를 뒤에 두면 안정적입니다.
  • 추출은 무조건 구조화 출력: 시스템에 넣을 데이터라면 자유 텍스트가 아니라 output_config로 JSON 스키마를 강제하세요.
  • 추정 금지를 명시: 영수증·차트 추출 시 "읽을 수 없으면 추정하지 말고 비워 두라"고 지시하면 환각을 줄일 수 있습니다.
  • 여러 이미지엔 라벨: "이미지 1/2"처럼 텍스트 앵커로 번호를 매기세요.
  • 비용은 측정해서 관리: count_tokens로 실제 토큰을 재고, 정확도가 과하면 리사이즈하거나 더 저렴한 모델로 내리세요.

자주 묻는 질문 (FAQ)

Q. base64와 URL 중 어느 방식을 써야 하나요?
사용자 업로드 파일이나 사내 비공개 이미지는 base64가 적합합니다. 외부에 URL을 노출할 필요가 없고, 메모리상의 바이트를 바로 인코딩해 보낼 수 있습니다. 반대로 이미 CDN·공개 스토리지에 올라가 있는 이미지라면 URL 방식이 요청 본문을 가볍게 만들어 유리합니다. 단, base64는 요청 크기를 키우므로 매우 큰 이미지는 전송 전에 리사이즈하는 편이 좋습니다.

Q. 이미지에서 뽑은 데이터를 항상 같은 형식의 JSON으로 받고 싶습니다.
output_config={"format": {"type": "json_schema", "schema": ...}}로 구조화 출력을 켜면 응답이 지정한 스키마의 유효한 JSON으로 강제됩니다. 스키마의 모든 객체에는 additionalProperties: false가 필요하며, minimum·maxLength 같은 제약은 미지원이라 클라이언트 측에서 검증해야 합니다. 자동 검증까지 원하면 client.messages.parse(...)를 사용하세요.

Q. 이미지 한 장이 토큰을 얼마나 쓰나요?
이미지는 해상도에 비례해 입력 토큰을 소모하며 모델 입력 단가로 과금됩니다. Opus 4.7 이상의 고해상도(긴 변 최대 약 2576px)에서는 한 장이 최대 약 4,784 토큰까지 쓸 수 있습니다. 정확한 수치는 추정하지 말고 client.messages.count_tokens(...)로 측정하세요. tiktoken은 Claude에 부정확하므로 사용하지 마세요.

Q. 한 요청에 이미지를 여러 장 넣어 비교할 수 있나요?
가능합니다. content 배열에 이미지 블록을 여러 개 넣고, 각 이미지 앞에 "이미지 1:", "이미지 2:" 같은 텍스트 라벨을 붙이면 모델이 어떤 이미지를 가리키는지 명확해집니다. 전후 비교, 디자인 A/B, 차트 추세 차이 분석 등에 유용합니다. 다만 이미지 수만큼 토큰 비용이 합산되니 꼭 필요한 이미지만 포함하세요.

Q. 비용을 줄이려면 어떤 모델을 써야 하나요?
단순한 비전 판독·분류처럼 무거운 추론이 필요 없으면 Haiku 4.5(입력 100만 토큰당 $1)로 충분한 경우가 많습니다. 복잡한 문서 추론이나 다중 이미지 비교에는 Opus 4.8이 정확도 면에서 값을 합니다. 대량의 이미지를 실시간이 아니게 처리한다면 Message Batches API로 묶어 추가 비용 절감이 가능합니다.


AI 개발이 필요하신가요?

DevTeam은 OpenAI·Claude 등 AI API 연동, 챗봇, RAG, 업무 자동화를 설계부터 배포까지 구현합니다. AI 연동 개발 또는 무료 견적 문의.

개발 파트너가 필요하신가요?

DevTeam은 MVP·웹·앱·AI 개발을 설계부터 배포·운영까지 한 팀이 책임집니다.