Vercel에서 외부 API 프록시 설정
Vercel에서 외부 API를 프록시하여 CORS 문제를 해결하고 보안을 강화하는 방법을 알아봅니다.
Vercel에서 외부 API 프록시 설정
Introduction
클라이언트에서 외부 API를 직접 호출할 때 발생하는 CORS 문제를 해결하고, API 키를 보호하기 위해 프록시 서버를 사용하는 것이 일반적입니다. Vercel 환경에서 외부 API 프록시를 설정하는 방법과 다양한 구현 사례를 살펴보겠습니다.
Environment
- Vercel 프로젝트 (Next.js 14+)
- Edge Runtime
- 외부 API (OpenAI, Stripe 등)
- Redis (캐싱용)
Problem
클라이언트에서 직접 외부 API를 호출할 때 발생하는 문제:
// 클라이언트 코드
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ model: 'gpt-4', messages: [...] })
});
// 에러: CORS 정책 위반
// Access to fetch at 'https://api.openai.com' from origin 'https://your-app.vercel.app'
// has been blocked by CORS policyAnalysis
CORS 문제의 원인:
- 브라우저의 동일 출처 정책(Same-Origin Policy)
- 외부 API가 특정 도메인만 허용하는 경우
- API 키가 클라이언트에 노출되는 보안 문제
프록시의 장점:
- CORS 문제 해결
- API 키 보호
- 요청/응답 변환 가능
- 캐싱 및 레이트 리밋링 구현
Solution
1. Next.js API Routes를 사용한 프록시
// app/api/proxy/openai/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
const body = await request.json();
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const data = await response.json();
return NextResponse.json(data);
}// 클라이언트 코드
const response = await fetch('/api/proxy/openai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }]
})
});2. Edge Runtime 프록시
// app/api/proxy/edge/route.js
export const runtime = 'edge';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const url = searchParams.get('url');
if (!url) {
return new Response('Missing url parameter', { status: 400 });
}
try {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`
}
});
const data = await response.json();
return Response.json(data);
} catch (error) {
return Response.json(
{ error: 'Proxy request failed' },
{ status: 500 }
);
}
}3. 캐싱 기반 프록시
// app/api/proxy/cached/route.js
import { NextResponse } from 'next/server';
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN
});
export async function GET(request) {
const { searchParams } = new URL(request.url);
const endpoint = searchParams.get('endpoint');
const cacheKey = `proxy:${endpoint}`;
const cacheTTL = 300; // 5분
// 캐시 확인
const cached = await redis.get(cacheKey);
if (cached) {
return NextResponse.json(JSON.parse(cached));
}
// 외부 API 호출
const response = await fetch(`https://api.example.com/${endpoint}`, {
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`
}
});
const data = await response.json();
// 캐시 저장
await redis.setex(cacheKey, cacheTTL, JSON.stringify(data));
return NextResponse.json(data);
}4. 요청/응답 변환 프록시
// app/api/proxy/transform/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
const body = await request.json();
// 요청 변환
const transformedRequest = {
model: body.model || 'default',
prompt: body.message,
max_tokens: body.maxLength || 100,
temperature: body.temperature || 0.7
};
const response = await fetch('https://api.ai-service.com/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.AI_API_KEY
},
body: JSON.stringify(transformedRequest)
});
const data = await response.json();
// 응답 변환
const transformedResponse = {
text: data.generated_text,
tokens: data.usage.total_tokens,
model: data.model
};
return NextResponse.json(transformedResponse);
}5. 레이트 리밋링 프록시
// lib/rate-limiter.js
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN
});
export async function checkRateLimit(identifier, limit = 100, window = 60) {
const key = `ratelimit:${identifier}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, window);
}
return {
allowed: current <= limit,
remaining: Math.max(0, limit - current),
reset: Date.now() + (window * 1000)
};
}
// app/api/proxy/rate-limited/route.js
import { NextResponse } from 'next/server';
import { checkRateLimit } from '@/lib/rate-limiter';
export async function GET(request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const rateLimit = await checkRateLimit(ip, 100, 60);
if (!rateLimit.allowed) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{
status: 429,
headers: {
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': rateLimit.reset.toString()
}
}
);
}
const response = await fetch('https://api.external.com/data', {
headers: { 'Authorization': `Bearer ${process.env.API_KEY}` }
});
const data = await response.json();
return NextResponse.json(data, {
headers: {
'X-RateLimit-Remaining': rateLimit.remaining.toString()
}
});
}6. 에러 핸들링 강화
// app/api/proxy/safe/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
try {
const body = await request.json();
// 입력 검증
if (!body.url || !body.url.startsWith('https://')) {
return NextResponse.json(
{ error: 'Invalid URL' },
{ status: 400 }
);
}
// 외부 API 호출
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
const response = await fetch(body.url, {
method: body.method || 'GET',
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`,
'Content-Type': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeout);
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('Proxy error:', error);
return NextResponse.json(
{ error: 'Proxy request failed' },
{ status: 500 }
);
}
}Lessons Learned
- 보안第一: API 키는 절대 클라이언트에 노출되지 않도록 해야 합니다.
- CORS 처리: 프록시는 CORS 문제를 해결하는 가장 효과적인 방법입니다.
- 캐싱 전략: 자주 호출되는 API는 캐싱하여 비용과 지연 시간을 줄여야 합니다.
- 에러 핸들링: 외부 API 실패 시 적절한 에러 응답을 제공해야 합니다.
- 레이트 리밋링: 외부 API의 레이트 리밋을 준수하도록 프록시를 설정해야 합니다.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.