Car Image API

고객 사례 · 차량 매물 광고 및 자동차 쇼핑 카탈로그

모든 마켓플레이스 매물에 스튜디오 차량 이미지를 넣는 방법

중고차 매물 사이트와 자동차 쇼핑 카탈로그가 카탈로그 규모로 연식마다 대표 이미지 한 장을 렌더링하는 방법: 차량 ID, 구도, 요청 속도 조절, 캐싱, 그리고 코드.

문제: 판매자가 사진을 올리기 전에도 매물에는 사진이 필요합니다

중고차 매물 사이트나 자동차 쇼핑 카탈로그의 성패는 그리드에 달려 있습니다. 매물이 등록되는 순간 모든 카드에 이미지가 필요하지만, 판매자가 직접 찍은 사진은 나중에야 올라오거나 아예 올라오지 않으며, 올라오더라도 제각각의 각도로 아무 차고 앞에서나 찍혀 있습니다. 스톡 사진은 정반대의 문제가 있습니다. 라이선스된 보도용 사진은 인기 연식의 인기 차종에 한해, 그것도 제조사가 정한 각도와 배경으로만 존재하고 그 밖에는 없습니다. 1990년부터 현재 연식까지 아우르는 카탈로그에는 수만 개의 공백이 생깁니다.

이런 사이트를 만드는 팀이 원하는 것은 가장 좋은 의미에서 평범합니다. 연식별 모델마다 스튜디오 이미지 한 장, 모두 같은 각도와 같은 색상, 카드 배경이 무엇이든 자연스럽게 얹히도록 투명한 배경, 하루에 만 번을 제공해도 될 만큼 가벼운 용량입니다.

카탈로그 연동 방식

현재 API에서 운영 중인 카탈로그 구축 사례는 모두 같은 형태입니다. 매물 데이터를 처음 채우는 자동차 광고 사이트든, 비교 카드를 렌더링하는 자동차 쇼핑 앱이든, 차량마다 대표 이미지 하나를 보여 주는 모바일 앱이든 마찬가지입니다:

  • 백필 후 소량씩. 몇 시간 만에 수천 건을 렌더링해 카탈로그를 채우고, 이후에는 새 연식과 요청이 들어오는 대로 하루 몇 건씩 처리합니다.
  • 앵글 하나, 페인트 하나 또는 짧은 팔레트. 대부분 실버나 화이트 색상의 front-3-4 또는 side를 선택합니다. 사용자가 색상을 고를 수 있는 쇼핑 카탈로그는 차량마다 프리셋 몇 개를 렌더링하며, 각 프리셋은 새 파이프라인이 아니라 차량 이미지 1개가 추가될 뿐입니다.
  • 고정 크기의 WebP. 너비 512~1024픽셀로, 카드에 쓰이는 크기 그대로 제공되므로 프런트엔드에서 이미지를 잘라내거나 레터박스를 넣을 일이 없습니다.
  • 차량 ID로 저장합니다. 렌더링은 팀 자체 객체 스토리지나 CDN에 저장되거나, 페이지에서 서명된 URL을 불러옵니다. 어느 쪽이든 키는 제조사-모델-연식 문자열이 아니라 카탈로그의 고정 veh_… ID입니다.

이 페이지의 나머지 부분에서는 그 파이프라인을 코드와 함께 단계별로 설명합니다. images:read 스코프가 있는 API 키와 카탈로그 규모에 맞는 플랜이 있다고 가정합니다(FAQ 참조).

1단계: 철자가 아닌 id로 카탈로그 열거하기

카탈로그 구축에서 가장 흔한 실패는 렌더링이 아니라 철자입니다. 다른 데이터베이스에서 제조사와 모델 문자열을 생성하는 빌드는 요청 대부분을 404 vehicle_not_found에 쓰게 될 수 있습니다. 무료이긴 하지만 매번 왕복 요청이 낭비되고, 그리드에는 빈칸이 생깁니다. 해결책은 무엇이 존재하는지 카탈로그가 알려 주게 하는 것입니다. GET /api/v1/vehicles는 무료이고 키가 필요 없으며, 반환되는 모든 모델에는 배포나 카탈로그 릴리스가 바뀌어도 변하지 않는 고정 차량 id(veh_ 뒤에 13자)가 있습니다. 이 id를 매물에 저장하고 이를 기준으로 렌더링하세요.

한 연식의 차량 목록 순회하기
# every make with a 2024 model year (free, no key)
curl -s "https://carimage.dev/api/v1/vehicles?year=2024"

# every 2024 model of one make, each with the id you will store
curl -s "https://carimage.dev/api/v1/vehicles?year=2024&makeId=584"
200 OK (일부 생략)
{
  "data": {
    "year": 2024,
    "make_id": 584,
    "models": [
      {
        "name": "718",
        "slug": "718",
        "vehicle_type": "Passenger Car",
        "id": "veh_b4bf090fyb53k"
      },
      {
        "name": "911",
        "slug": "911",
        "vehicle_type": "Passenger Car",
        "id": "veh_78qtwrgh37bkr"
      }
    ]
  }
}

?make=porsche&model=911은 한 모델의 모든 연식을 연식별 id와 함께 나열하고, ?q=는 판매자가 이름을 입력할 때 자유 텍스트로 검색합니다. 같은 카탈로그가 오픈 @meterapp/vehicle-db 패키지로도 공개되어 있어, 드롭다운과 유효성 검사를 동일한 목록으로 오프라인에서 실행할 수 있습니다. 어느 쪽이든 이 단계가 끝나면 id 목록을 갖게 되며, 이후 단계에서는 자동차 이름을 다시 철자로 쓸 일이 없습니다.

이미 다른 데이터베이스의 표기로 차량명을 기재한 매물은 한 번의 호출로 매핑됩니다. POST /api/v1/vehicles/check는 최대 2,000개의 매물을 렌더링할 때와 같은 방식으로 읽고, 각각에 대해 차량 id, 카탈로그에 해당 차량이 없을 때는 가장 가까운 실제 차량, 또는 모든 뷰와 색상에서 건너뛸 미스(miss)로 응답합니다. car-image check listings.csv --out checked.csv는 id를 파일에 다시 기록합니다. 무료이며, 404를 하나씩 겪으며 누락을 찾아내는 수고를 덜어 줍니다.

2단계: 모든 매물에 동일한 요청 형식 사용

카드 형태를 한 번 정해 쿼리 문자열에 담으세요. 아래에서 vehicle= 뒤에 오는 모든 내용은 전체 카탈로그에 대해 한 번만 내리는 프레이밍 결정입니다:

매물 이미지 한 장
curl --fail-with-body -H "Authorization: Bearer $CAR_IMAGE_API_KEY" \
  "https://carimage.dev/api/v1/images/car?vehicle=veh_78qtwrgh37bkr&view=front-3-4&color=silver&w=800&h=600&fit=contain&trim=1&padding=8&format=webp" \
  -o veh_78qtwrgh37bkr.webp
  • w=800&h=600&fit=contain은 차량 전체가 보이고 주변에 투명 여백이 있는 정확히 800×600 크기의 이미지를 제공하므로 모든 카드의 캔버스가 같아집니다. 반면 fit=cover는 박스를 가득 채우고 가운데를 기준으로 잘라 냅니다.
  • trim=1&padding=8은 크기를 맞추기 전에 차량 윤곽에 맞춰 자르고 긴 쪽의 8%를 여백으로 두므로, Suburban과 Miata가 원본 프레임 안에 떠 있지 않고 같은 방식으로 박스를 채웁니다.
  • color=silver가 기본값이자 가장 무난한 중립색입니다. 15가지 프리셋이나 어떤 hex 값이든 동일하게 차량 이미지 1개로 집계됩니다.
  • format=webp는 무손실 알파를 지원하는 가장 작은 인코딩입니다. 브라우저가 URL을 직접 로드하는 경우(서명된 URL, 4단계) format=auto가 클라이언트별로 WebP 또는 PNG를 협상합니다.

응답에는 ETag, X-Image-Source(cache 또는 generated), 그리고 실제로 제공된 카탈로그 차량을 나타내는 X-Vehicle-Id가 포함됩니다. ETag를 파일과 함께 보관하세요. 이후 If-None-Match로 요청하면 304가 반환되고 비용이 들지 않으므로, 야간 재동기화를 무료로 유지할 수 있습니다.

3단계: 한도 내에서 백필 속도 조절하기

백필에는 세 가지 제한이 작용하며, 모두 조용히 실패하지 않고 헤더로 알려 줍니다.

  • 분당 요청 수. Free와 Pro는 키당 120회, Business는 600회, Enterprise는 1,200회이며, 모든 키를 합산한 계정별 한도도 적용됩니다. 인증된 모든 응답에는 RateLimit-Remaining이 포함되고, 429 응답에는 Retry-After가 포함됩니다. 동시에 4~8개의 요청이면 충분합니다.
  • 새 차량 뷰. 아직 준비되지 않은 차량 뷰를 처음 요청하면 어떤 색상으로도 표시할 수 있도록 준비하는 동안 약간의 시간이 걸립니다. 이후 모든 요청은 색상과 관계없이 캐시에서 제공됩니다. 새 뷰 준비에는 하루 렌더 예산 중 계정 할당분이 사용됩니다(Free 2%, Pro 10%, Business 25%, Enterprise 50%). 할당분을 초과하면 새 렌더는 code: account_generation_cap과 reset_at(UTC 자정)이 포함된 429로 응답하며, 요금은 청구되지 않고 캐시된 이미지는 계속 제공됩니다. 대규모 백필은 하루 저녁이 아니라 며칠 저녁에 걸쳐 진행됩니다.
  • 월 차량 이미지 수. Free는 100개, Pro는 2,500개, Business는 10,000개가 포함되며, Enterprise는 협의를 통해 정합니다. 이 한도를 넘으면 이번 달에 아직 집계되지 않은 이미지에 대한 요청은 402(plan_image_limit)로 응답하며 아무것도 집계하지 않습니다. 이번 달에 이미 집계된 이미지는 항상 제공되므로 운영 중인 그리드가 깨지지 않습니다.

렌더링 실패(502) 시 집계된 차량 이미지는 자동으로 환원되고, 같은 새 변형에 대한 동시 요청은 하나의 렌더를 공유하며, 일반 GET은 안전하게 재시도할 수 있습니다. SDK는 Retry-After를 준수하여 429와 503을 자동으로 재시도하며, 402는 재시도하지 않습니다.

TypeScript SDK를 사용한 재개 가능한 백필
import { CarImageClient, CarImageError } from "@meterapp/car-image-sdk";

const client = new CarImageClient({ apiKey: process.env.CAR_IMAGE_API_KEY });

// One make, one model year: the ids are what you store and what you render by.
const { data } = await client.vehicles({ year: 2024, makeId: 584 });
const queue = data.models.flatMap((model) => (model.id ? [model.id] : []));

// Four in flight stays well inside 120 requests a minute, and a cold render
// that takes a few seconds never holds up the rest of the batch.
await Promise.all(
  Array.from({ length: 4 }, async () => {
    for (let id = queue.shift(); id; id = queue.shift()) {
      try {
        const image = await client.getImage({
          vehicle: id, view: "front-3-4", color: "silver",
          width: 800, height: 600, fit: "contain", trim: true, padding: 8, format: "webp",
        });
        await store(id, image.bytes, image.etag); // your object store or CDN, keyed by vehicle id
      } catch (error) {
        // 429 and 503 were already retried with Retry-After; a 404 is a vehicle to request, not a failure.
        if (error instanceof CarImageError && error.status === 404) continue;
        throw error;
      }
    }
  })
);

4단계: 자체 CDN 또는 서명된 URL로 제공하기

구매자에게 이미지를 보여 주는 좋은 방법은 두 가지이며, 카탈로그 구축 사례에서는 두 가지를 모두 사용합니다:

  • 바이트를 저장하세요. 각 렌더를 차량 id별로 자체 객체 스토리지나 CDN에 저장하고 다른 에셋처럼 제공하세요. 수백만 번 렌더링되는 그리드에는 이 방법이 정답입니다. 로드할 때마다 저희에게 호출할 필요가 없으며, 유료 플랜의 라이선스는 제품 제공을 위해 보관하는 사본도 포함합니다.
  • 서명된 URL을 발급하세요. POST /api/v1/image-urls는 호출당 최대 50개의 이미지를 처리하고, 각 URL이 발급될 때 해당 이미지를 이번 달 차량 이미지로 집계하며, 브라우저가 직접 로드할 수 있는 키 없는 URL을 반환합니다. ttl_seconds는 최대 7일까지 설정할 수 있으며, 그보다 오래 게시되는 매물 페이지에는 renew: true를 추가하세요(갱신 시 해당 월에 이미지가 한 번 집계되며, 그 달에 이미 집계된 경우에는 추가로 집계되지 않습니다). 대시보드에서 키를 사이트 오리진으로 제한하면 다른 곳에 복사된 URL은 403을 반환합니다.

일괄 발급 시 목록 페이지마다 Idempotency-Key를 보내세요. 같은 키와 본문으로 재시도한 호출은 첫 번째 응답을 그대로 반환하며 다시 계산되지 않습니다.

listings.json — 한 번에 50개씩, 2단계와 같은 프레이밍
{
  "images": [
    {
      "vehicle": "veh_78qtwrgh37bkr",
      "view": "front-3-4",
      "color": "silver",
      "width": 800,
      "height": 600,
      "fit": "contain",
      "trim": true,
      "padding": 8,
      "format": "auto"
    },
    {
      "vehicle": "veh_b4bf090fyb53k",
      "view": "front-3-4",
      "color": "silver",
      "width": 800,
      "height": 600,
      "fit": "contain",
      "trim": true,
      "padding": 8,
      "format": "auto"
    }
  ]
}

카탈로그에 차량이 없는 경우

먼저 검색하세요. GET /api/v1/vehicles?q=는 이름, 접두사 또는 퍼지 매칭으로 모델을 찾고, POST /api/v1/images/resolve는 자유 형식 문구를 정확한 파라미터로 변환하며 판단에 활용할 수 있는 신뢰도를 함께 제공합니다. 그래도 없는 항목은 요청 게시판에 올리세요. POST /api/v1/requests는 요청을 생성하며, 같은 제조사, 모델, 연식에 대해 열려 있는 요청이 이미 있으면 중복으로 만들지 않고 해당 요청에 추천을 추가합니다. 카탈로그 자체가 오픈 소스이며, 추가된 항목은 모든 고객에게 제공됩니다.

CLI, SDK 및 MCP

전체 파이프라인을 코드 없이 CLI만으로 실행할 수 있으며, 이 API를 사용하는 여러 카탈로그가 실제로 이 방법으로 초기 데이터를 구축했습니다:

npx @meterapp/car-image
# One file per vehicle id. Re-running skips what exists, so a stopped backfill resumes.
while read -r id; do
  [ -f "cdn/$id.webp" ] || npx @meterapp/car-image get --vehicle "$id" \
      --view front-3-4 --color silver --width 800 --height 600 --fit contain \
      --trim --padding 8 --format webp --out "cdn/$id.webp"
done < vehicle-ids.txt

# Or fifty signed URLs in one call, for a page of listings the browser loads itself.
npx @meterapp/car-image url --batch listings.json --ttl 604800 --renew --json
  • SDK: 열거에는 client.vehicles({ year, makeId }), 렌더링에는 client.getImage({ vehicle, … }), 서명된 URL 일괄 생성에는 client.createImageUrls(…)를 사용하세요. 모든 매개변수는 이미지 문서를 참고하세요.
  • MCP: https://carimage.dev/api/mcp에 연결된 어시스턴트는 동일한 카탈로그(search_vehicles, resolve_vehicle)와 동일한 렌더링(get_car_image, create_car_image_urls)을 사용할 수 있습니다. 큐레이터가 차량을 직접 추가할 때는 적합하지만, 수천 건을 일괄 채워 넣을 때는 적합하지 않습니다. 그런 작업에는 CLI나 SDK를 사용하고, 어시스턴트는 누락된 일부 건에 활용하세요. 설정 방법은 MCP 페이지에 있습니다.

자주 묻는 질문

매물 등록에는 어떤 각도를 사용해야 하나요?
front-3-4가 기본값이자 전형적인 대표 각도로, 차량 앞부분이 왼쪽을 향합니다. 차량 왼쪽 면을 보여주는 모든 각도에는 -right 쌍(front-3-4-right, side-right, rear-3-4-right)이 있어, 카드가 레이아웃 바깥이 아닌 안쪽을 향하게 할 수 있습니다. 하나를 골라 계속 사용하세요. 매물 간 일관성이 가치의 대부분을 차지합니다.
매물 20,000건 규모의 카탈로그 비용은 얼마인가요?
각 매물에 차량 하나를 한 가지 도색과 뷰로 보여 준다면 매월 차량 이미지 20,000개이며, 각 이미지는 어떤 크기로든 필요한 만큼 전송됩니다. 차량, 도색, 뷰가 같은 매물은 하나의 이미지로 집계되며 If-None-Match로 재확인하는 것은 무료입니다. Pro에는 매월 차량 이미지 2,500개, Business에는 10,000개가 포함되므로 그 규모의 카탈로그라면 Enterprise, 또는 영구 보유하며 직접 호스팅할 수 있는 1회성 이미지 팩이 적합합니다. 허용량을 초과하는 백필은 402로 응답하며 아무것도 집계되지 않습니다.
파일을 자체 CDN에 보관해도 되나요?
네. 유료 요금제는 요금제가 활성 상태인 동안 귀사 제품 내에서 렌더링 이미지를 사용할 수 있는 라이선스를 제공하며, 자체 서버나 CDN에 저장한 사본도 포함되고 출처 표기도 필요 없습니다. 요금제 종료 후 30일 이내에 저장된 사본은 삭제됩니다(이미지 팩의 이미지는 영구 보관 가능). 라이선스로 허용되지 않는 것은 이미지를 독립적인 라이브러리, 피드 또는 데이터셋으로 제공하는 것입니다. 다음 연식 모델을 렌더링하려면 활성 요금제가 필요합니다.
카탈로그에 새 연식이 추가되면 어떻게 되나요?
@meterapp/vehicle-db 카탈로그는 소스가 게시될 때마다 갱신되며, 기존 id는 절대 변경되지 않습니다. 새 연식을 조회해 아직 없는 id만 렌더링하고 나머지는 그대로 두세요. 요청에는 ETag가 포함되며, If-None-Match를 사용한 반복 요청은 304를 반환하고 비용이 들지 않습니다.
일부 제조사 및 모델 문자열이 404를 반환하는 이유는 무엇인가요?
다른 데이터베이스의 표기 방식을 따랐기 때문입니다. 카탈로그는 이름, 슬러그, 그리고 다양한 배지 및 차체 유형 변형을 허용하지만, 처음 보는 문자열은 404 vehicle_not_found가 됩니다(무료이지만 왕복 요청이 낭비됩니다). 자체 문자열을 변환하는 대신 카탈로그를 열거해 id를 저장하고, 남은 항목은 검색하거나 resolve로 확인하세요.