JavaScript / Node

Node.js 및 브라우저에서 Veacon API 호출. fetch 네이티브, 재시도 유틸 포함.

Last updated: 2026-04-23

별도 SDK 없이 fetch 네이티브로 사용. Node.js 18+ 또는 모던 브라우저 전제.

환경 변수

bash
# .env.local
VEACON_API_KEY=veacon_pk_live_...

프론트엔드 (브라우저) 에는 절대 API Key 를 넣지 마세요. Next.js API route 또는 서버 함수에서 호출하고, 클라이언트에는 결과 JSON 만 전달하는 패턴을 사용하세요.


1. 기본 호출

ts
type PulseRow = {
  region: string;
  category: string;
  period: string;
  product_type: 'rental' | 'meeting' | 'combined';
  avg_price: number | null;
  median_price: number | null;
  sample_size: number;
  demand_index: number | null;
  confidence: 'low' | 'medium' | 'high';
};

async function getPulse(region: string, category: string, period: string): Promise<PulseRow[]> {
  const params = new URLSearchParams({ region, category, period });
  const r = await fetch(`https://veacon.io/api/v1/markets/pulse?${params}`, {
    headers: { 'X-API-Key': process.env.VEACON_API_KEY! },
  });
  if (!r.ok) {
    const body = await r.json().catch(() => ({}));
    throw new Error(`${body?.error?.code ?? r.status}: ${body?.error?.message ?? 'unknown'}`);
  }
  const { data } = await r.json();
  return data as PulseRow[];
}

const rows = await getPulse('강남권', 'office', '2026-01');
console.log(rows[0].avg_price);

2. 재시도 + Rate Limit 대응

ts
async function fetchWithRetry(url: string, init: RequestInit, maxAttempts = 4): Promise<Response> {
  let attempt = 0;
  while (attempt < maxAttempts) {
    const r = await fetch(url, init);

    if (r.ok) return r;

    if (r.status === 429) {
      const retryAfter = Number(r.headers.get('Retry-After') ?? '1');
      await sleep(retryAfter * 1000);
      attempt++;
      continue;
    }

    if (r.status >= 500 && attempt < maxAttempts - 1) {
      await sleep(Math.min(10_000, 1000 * 2 ** attempt));
      attempt++;
      continue;
    }

    // 4xx (other than 429) — do not retry
    return r;
  }
  throw new Error('Max retries exceeded');
}

function sleep(ms: number) {
  return new Promise((r) => setTimeout(r, ms));
}

3. 쿼터 소진 알림

ts
async function pulse(region: string, category: string, period: string) {
  const r = await fetch(
    `https://veacon.io/api/v1/markets/pulse?region=${encodeURIComponent(region)}&category=${category}&period=${period}`,
    { headers: { 'X-API-Key': process.env.VEACON_API_KEY! } },
  );

  const used = Number(r.headers.get('X-Quota-Used'));
  const limit = Number(r.headers.get('X-Quota-Limit'));

  // 쿼터 80% 도달 시 Slack/이메일 경고
  if (used / limit > 0.8) {
    console.warn(`Veacon quota at ${((used / limit) * 100).toFixed(1)}%`);
    // notifyOps(...)
  }

  return r.json();
}

4. Next.js API Route (프록시 패턴)

클라이언트는 /api/pulse 만 호출 — 실제 API Key 는 서버에 감춰짐.

ts
// app/api/pulse/route.ts
import { NextResponse } from 'next/server';

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const region = searchParams.get('region');
  const category = searchParams.get('category');
  const period = searchParams.get('period');

  if (!region || !category || !period) {
    return NextResponse.json({ error: 'missing params' }, { status: 400 });
  }

  const r = await fetch(
    `https://veacon.io/api/v1/markets/pulse?region=${encodeURIComponent(region)}&category=${category}&period=${period}`,
    { headers: { 'X-API-Key': process.env.VEACON_API_KEY! }, next: { revalidate: 3600 } },
  );

  // 쿼터 헤더 통과 (클라이언트도 보이게)
  const headers = new Headers();
  for (const h of ['X-Quota-Limit', 'X-Quota-Used', 'X-Quota-Resets-At']) {
    const v = r.headers.get(h);
    if (v) headers.set(h, v);
  }

  return NextResponse.json(await r.json(), { status: r.status, headers });
}

5. React hook (클라이언트용)

ts
import { useEffect, useState } from 'react';

export function usePulse(region: string, category: string, period: string) {
  const [state, setState] = useState<
    | { status: 'loading' }
    | { status: 'ok'; data: unknown[] }
    | { status: 'error'; message: string }
  >({ status: 'loading' });

  useEffect(() => {
    const q = new URLSearchParams({ region, category, period });
    fetch(`/api/pulse?${q}`)
      .then(async (r) => {
        const body = await r.json();
        if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`);
        setState({ status: 'ok', data: body.data });
      })
      .catch((e) => setState({ status: 'error', message: e.message }));
  }, [region, category, period]);

  return state;
}

참고