외부 API와의 통합
외부 날씨 API를 서버 Route Handler로 감싸 API 키와 CORS를 보호하고 브라우저에 필요한 응답만 전달합니다.
대부분의 현대 웹 애플리케이션은 독립적으로 동작하기보다, 다양한 외부 API(Application Programming Interface)와 연동해 데이터를 가져오거나 기능을 활용합니다.
예를 들어 날씨 정보를 위해 기상청 API를 쓰거나, 결제를 위해 PG(Payment Gateway) API를 호출할 수 있습니다.
Next.js의 Route Handler는 이러한 외부 API와의 통합을 위한 서버 환경을 제공합니다.
클라이언트(브라우저)에서 직접 외부 API를 호출하는 대신, Next.js Route Handler를 경유하면 여러 가지 중요한 이점을 얻을 수 있습니다.
이 절에서는 Next.js Route Handler로 외부 API를 통합하는 흐름과, 이 방식의 이점 및 주의사항을 함께 정리합니다.
외부 API 통합에서 API 라우트는 단순 전달자가 아니다. 비밀 키, CORS, 입력 검증, 응답 가공을 브라우저 밖의 서버 경계로 옮기는 장치다.
- Client
/api/weather?city=Seoul 브라우저는 같은 출처의 내부 엔드포인트만 호출한다.
- Next API Route
process.env.API_KEY 서버에서 키를 읽고 입력값을 검증한 뒤 외부 API를 호출한다.
- External API
weather provider 원본 응답은 필요한 필드만 남긴 JSON으로 가공되어 돌아온다.
| 문제 | 직접 호출 | API 라우트 경유 |
|---|---|---|
| API 키 | 브라우저 번들에 노출될 수 있다. | 서버 환경 변수에서만 읽는다. |
| CORS | 브라우저 정책에 막힐 수 있다. | 서버에서 호출해 제약을 줄인다. |
| 응답 형태 | 외부 스키마가 화면 코드에 새어 나온다. | 서비스에 필요한 필드만 반환한다. |
왜 Route Handler를 통해 외부 API를 호출하는지?
아래 다이어그램은 브라우저 직접 호출과 Route Handler 경유 호출의 차이를 보안, CORS, 응답 가공 관점에서 비교합니다.
Next API Route를 프록시로 두면 브라우저는 내부 엔드포인트만 호출하고, 비밀키와 외부 API 세부사항은 서버에 남는다.
- 클라이언트/api/weather 같은 내부 경로 호출
비밀키 없이 필요한 파라미터만 보낸다
- 프록시 라우트환경 변수로 외부 API 호출
입력 검증과 rate limit를 함께 적용
- 외부 응답필요한 데이터만 재가공
원본 오류를 서비스용 상태 코드로 바꾼다
클라이언트(브라우저)에서 JavaScript를 사용하여 직접 외부 API를 호출할 수도 있지만, Next.js Route Handler를 경유하면 비밀 값과 응답 계약을 서버에서 통제할 수 있습니다.
보안 (API 키 숨김): 대부분의 외부 API는 API 키(API Key)를 사용해 인증합니다.
이 API 키는 민감한 정보이므로 절대 클라이언트 사이드 코드에 노출되어서는 안 됩니다.
Route Handler에서 외부 API를 호출하면, API 키를 서버 환경 변수(.env.local)에 저장하고 서버 측에서만 접근할 수 있어 브라우저 번들 노출을 막을 수 있습니다.
CORS(Cross-Origin Resource Sharing) 문제 회피: 브라우저의 보안 정책(동일 출처 정책, Same-Origin Policy) 때문에 다른 도메인의 API를 직접 호출하는 데 제약이 있습니다.
Route Handler의 서버 측 외부 요청은 브라우저의 CORS 제약을 받지 않습니다.
Route Handler가 외부 API로부터 데이터를 가져온 후, 클라이언트는 동일 출처(Same-Origin)인 Next.js Route Handler에만 요청을 보내면 됩니다.
데이터 가공 및 전처리: 외부 API에서 반환되는 데이터 형식이 클라이언트에서 원하는 형식과 다를 수 있습니다.
Route Handler에서 데이터를 가져온 후 필요한 형태로 가공하거나 필터링하여 클라이언트에게 최적화된 데이터를 제공할 수 있습니다.
성능 최적화: 여러 외부 API를 호출하여 데이터를 조합해야 하는 경우, 클라이언트에서 각각 호출하는 것보다 Route Handler에서 한 번에 처리하여 클라이언트-서버 간의 네트워크 왕복(Round Trip) 횟수를 줄일 수 있습니다.
에러 처리 및 로깅: 외부 API 호출 시 발생할 수 있는 네트워크 오류나 API 에러를 Route Handler에서 중앙 집중적으로 처리하고 로깅할 수 있습니다.
속도 제한(Rate Limiting) 관리: 일부 외부 API는 호출 횟수에 제한을 둡니다.
Route Handler에서 호출을 관리하면 이러한 속도 제한을 더 효율적으로 제어하고 재시도 로직 등을 구현할 수 있습니다.
외부 API 호출을 위한 기본 도구: fetch API
Node.js 환경의 Next.js Route Handler에서는 웹 표준 fetch API 또는 axios와 같은 라이브러리를 사용하여 외부 API를 호출할 수 있습니다.
여기서는 기본 제공되는 fetch API를 사용한 예시를 살펴보겠습니다.
fetch API의 기본 사용법은 다음과 같습니다.
// fetch API 기본 사용법
async function fetchData(url: string) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json(); // JSON 응답 파싱
// const textData = await response.text(); // 텍스트 응답 파싱
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}실습: 날씨 정보를 가져오는 Route Handler 생성
OpenWeatherMap API를 사용하여 특정 도시의 현재 날씨 정보를 가져오는 Next.js Route Handler를 만들어 보겠습니다.
사전 준비- OpenWeatherMap 웹사이트(https://openweathermap.org/)에 가입합니다.
- 로그인 후 API keys 섹션에서 새로운 API 키를 생성합니다. (일반적으로
Current Weather Data에 대한Free플랜으로도 충분합니다.) - 발급받은 API 키를 복사해 둡니다.
환경 변수 설정:
프로젝트 루트에 .env.local 파일을 생성하고 발급받은 OpenWeatherMap API 키를 추가합니다.
# .env.local
OPENWEATHER_API_KEY=YOUR_OPENWEATHER_API_KEY_HERE주의: YOUR_OPENWEATHER_API_KEY_HERE 부분을 실제 발급받은 키로 대체하세요.
src/app/api/weather/route.ts)
import { NextRequest, NextResponse } from 'next/server';
const OPENWEATHER_API_KEY = process.env.OPENWEATHER_API_KEY;
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather';
export async function GET(request: NextRequest) {
// 1. 쿼리 파라미터에서 도시 이름 가져오기
const searchParams = request.nextUrl.searchParams;
const city = searchParams.get('city');
// 2. 필수 파라미터 검증
if (!city) {
return NextResponse.json(
{ message: '도시 이름(city) 쿼리 파라미터가 필요합니다.' },
{ status: 400 } // Bad Request
);
}
// 3. API 키 유효성 검사 (서버 환경 변수 확인)
if (!OPENWEATHER_API_KEY) {
console.error('OpenWeatherMap API Key is not set in environment variables.');
return NextResponse.json(
{ message: '서버 설정 오류: API 키가 누락되었습니다.' },
{ status: 500 } // Internal Server Error
);
}
try {
// 4. OpenWeatherMap 외부 API 호출 URL 생성 (단위: 섭씨, 언어: 한국어)
const apiUrl = new URL(BASE_URL);
apiUrl.search = new URLSearchParams({
q: city,
appid: OPENWEATHER_API_KEY,
units: 'metric',
lang: 'kr',
}).toString();
// 5. 외부 API 호출
const response = await fetch(apiUrl);
// 6. 외부 API 응답 처리
if (!response.ok) {
const errorData = await response.json();
console.error('OpenWeatherMap API Error:', errorData);
// 외부 API의 오류 메시지를 클라이언트에 전달 (또는 일반화된 메시지)
return NextResponse.json(
{ message: `날씨 정보를 가져오는 데 실패했습니다. ${errorData.message || response.statusText}` },
{ status: response.status }
);
}
const weatherData = await response.json();
// 7. 클라이언트에 필요한 형태로 데이터 가공 (선택 사항)
const processedData = {
city: weatherData.name,
country: weatherData.sys.country,
temperature: weatherData.main.temp,
feelsLike: weatherData.main.feels_like,
humidity: weatherData.main.humidity,
description: weatherData.weather[0].description,
icon: `https://openweathermap.org/img/wn/${weatherData.weather[0].icon}.png`,
};
// 8. 가공된 데이터 반환
return NextResponse.json(processedData, { status: 200 });
} catch (error) {
console.error('Route Handler에서 오류 발생:', error);
return NextResponse.json(
{ message: '서버에서 날씨 정보를 가져오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}개발 서버(npm run dev)를 실행합니다.
웹 브라우저나 Postman/Insomnia에서 다음 URL로 접속하여 테스트합니다.
- 성공 케이스:
http://localhost:3000/api/weather?city=Seoul - 실패 케이스 (도시 이름 누락):
http://localhost:3000/api/weather - 실패 케이스 (유효하지 않은 도시 이름):
http://localhost:3000/api/weather?city=InvalidCityName123
클라이언트 컴포넌트에서 Route Handler 호출
이제 클라이언트 컴포넌트에서 위에서 만든 Route Handler를 호출하여 날씨 정보를 화면에 표시해 보겠습니다.
src/app/weather/page.tsx (클라이언트 컴포넌트 예시)
"use client";
import { useState } from 'react';
interface WeatherData {
city: string;
country: string;
temperature: number;
feelsLike: number;
humidity: number;
description: string;
icon: string;
}
export default function WeatherPage() {
const [city, setCity] = useState('');
const [weather, setWeather] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchWeather = async () => {
if (!city) {
setError('도시 이름을 입력해주세요.');
return;
}
setLoading(true);
setError(null);
setWeather(null); // 이전 날씨 정보 초기화
try {
// Next.js Route Handler를 호출합니다.
const query = new URLSearchParams({ city: city.trim() });
const response = await fetch(`/api/weather?${query}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '날씨 정보를 가져오는 데 실패했습니다.');
}
const data: WeatherData = await response.json();
setWeather(data);
} catch (err) {
setError((err as Error).message);
console.error('클라이언트에서 날씨 API 호출 중 오류:', err);
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '20px auto', border: '1px solid #007bff', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}>
<h1 style={{ color: '#007bff', marginBottom: '20px' }}>날씨 정보 가져오기</h1>
<div style={{ marginBottom: '20px' }}>
<input
type="text"
placeholder="도시 이름을 입력하세요 (예: Seoul)"
value={city}
onChange={(e) => setCity(e.target.value)}
style={{ padding: '10px', marginRight: '10px', border: '1px solid #ccc', borderRadius: '5px', width: '70%' }}
/>
<button
onClick={fetchWeather}
disabled={loading}
style={{ padding: '10px 15px', backgroundColor: '#28a745', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', opacity: loading ? 0.7 : 1 }}
>
{loading ? '로딩 중...' : '날씨 가져오기'}
</button>
</div>
{error && (
<p style={{ color: '#dc3545', fontWeight: 'bold', marginBottom: '15px' }}>오류: {error}</p>
)}
{weather && (
<div style={{ border: '1px solid #eee', borderRadius: '8px', padding: '20px', backgroundColor: '#f9f9f9' }}>
<h2 style={{ color: '#333', marginBottom: '10px' }}>{weather.city}, {weather.country}</h2>
<img src={weather.icon} alt={weather.description} style={{ width: '80px', height: '80px' }} />
<p style={{ fontSize: '1.2em', margin: '10px 0' }}>온도: {weather.temperature}°C (체감: {weather.feelsLike}°C)</p>
<p>습도: {weather.humidity}%</p>
<p>날씨: {weather.description}</p>
</div>
)}
</div>
);
}- 클라이언트에서는 Next.js Route Handler의 상대 경로(
/api/weather?city=Seoul)를 사용합니다. 이는 개발 환경(localhost)과 배포 환경에서 URL이 자동으로 맞춰지도록 합니다. - 에러 처리 및 로딩 상태 관리를 통해 사용자에게 더 나은 경험을 제공합니다.
외부 API 통합은 단순히 fetch를 호출하는 문제가 아니라, 비밀 키를 어디에 둘지, 어떤 값을 검증할지, 실패를 어떤 형태로 돌려줄지, 응답을 얼마나 캐싱할지를 함께 정하는 서버 경계 설계입니다.
이 기준이 정리되어 있어야 클라이언트 컴포넌트는 화면 상태 처리에 집중할 수 있습니다.
외부 API 연동은 정상 응답보다 실패, 지연, 요금, 제한 초과를 먼저 설계해야 운영에서 흔들리지 않는다.
- Timeout응답 지연을 무한 대기하지 않음
짧은 제한과 사용자용 실패 메시지를 둔다
- Quota호출량과 캐시 전략 관리
반복 GET은 캐시하고 초과 시 대체 응답을 준비
- Secret키와 토큰은 서버 환경 변수
그와 응답에 원본 키가 섞이지 않게 한다
고려 사항
- API 키 관리: 개발 환경(
.env.local)에서는process.env.VAR_NAME으로 접근하고, 배포 환경(Vercel, AWS 등)에서는 해당 플랫폼의 환경 변수 설정 기능을 사용하여 API 키를 안전하게 관리해야 합니다. - 보안: 외부 API 호출 시 민감한 데이터를 전송해야 한다면, HTTPS를 사용하는지, 추가적인 인증(OAuth 등)이 필요한지 확인해야 합니다.
- 속도 제한 및 캐싱: 자주 호출되는 외부 API의 경우, Route Handler 내에서 응답을 캐싱하여 외부 API 호출 횟수를 줄이고 성능을 향상시킬 수 있습니다.
- 오류 처리 및 재시도: 외부 API 호출이 실패할 경우를 대비하여 견고한 오류 처리 로직과 필요한 경우 재시도(Retry) 메커니즘을 구현해야 합니다.
- 환경별 설정: 개발, 스테이징, 프로덕션 환경마다 다른 외부 API 엔드포인트나 키를 사용해야 할 경우, 환경 변수를 통해 유연하게 관리해야 합니다.
아래 다이어그램은 외부 API를 운영할 때 먼저 정해야 할 장애, 비용, 보안 기준을 한 화면에 정리합니다.
프록시는 단순 전달자가 아니라 입력을 줄이고, 외부 장애를 흡수하며, 클라이언트가 쓰기 쉬운 응답으로 바꾸는 안정화 계층이다.
- 입력 축소허용 파라미터만 외부로 전달
사용자가 외부 API 전체를 조작하지 못하게 한다
- 장애 흡수timeout, retry, fallback
느린 원격 서버가 앱 전체를 멈추지 않게 한다
- 응답 정규화필요 필드만 반환
외부 스키마 변화가 UI에 바로 번지지 않게 한다
다음 다이어그램은 외부 API를 직접 노출하지 않고 Next.js Route Handler에서 비밀 키, 캐싱, 오류 응답을 통제하는 기준입니다.
클라이언트가 외부 제공자를 직접 알지 않게 서버 계약에서 운영 위험을 네 단계로 줄인다.
- Secret키를 서버에 격리
환경 변수에서만 읽고 응답·로그·번들에 노출하지 않음
- Validate입력 allowlist
도시·페이지·정렬값을 허용된 타입과 범위로 제한
- Budget캐시·rate limit
같은 요청의 외부 quota와 지연 비용을 통제
- Failure오류 계약 변환
제공자 원문을 내부 status와 안전한 메시지로 매핑
아래 다이어그램은 클라이언트 요청, Route Handler fetch, 외부 API 응답 가공이 이어지는 구조를 압축해 보여줍니다.
외부 API 통합의 경계는 “브라우저가 알아야 할 것”과 “서버만 알아야 할 것”을 나누는 지점이다.
- 공개 입력검색어, 페이지, 선택 값만 전달
외부 토큰이나 내부 정책은 숨긴다
- 서버 정책인증, quota, 캐시, 매핑
외부 호출 전 서비스 규칙을 적용한다
- 제공자 계약외부 응답을 내부 모델로 변환
UI는 제공자별 필드명에 의존하지 않는다
Next.js Route Handler를 통한 외부 API 통합은 보안, 성능, 개발 효율성 측면에서 많은 이점을 제공합니다.
이를 통해 더욱 강력하고 확장 가능한 웹 애플리케이션을 구축할 수 있습니다.
아래 다이어그램은 외부 API와의 통합에서 요청 진입점, 응답 형태, 오류 처리를 함께 확인합니다.
프록시 보안은 키를 숨기는 것에서 끝나지 않는다. 호출 권한, 입력 allowlist, 응답 마스킹까지 한 흐름으로 묶어야 한다.
- 사용자 확인누가 외부 호출을 요청했는지 검사
그인/권한 기준 없이 프록시를 열어두지 않는다
- 입력 제한허용된 파라미터만 전달
URL, header, method를 사용자가 마음대로 바꾸지 못하게 한다
- 응답 보호민감 필드 제거와 상태 코드 변환
외부 원본 메시지를 그대로 노출하지 않는다