외부 API와의 통합
외부 날씨 API를 서버 Route Handler로 감싸 비밀 API 키를 서버에서 관리하고 브라우저에 필요한 응답만 전달합니다.
대부분의 현대 웹 애플리케이션은 혼자 동작하기보다, 여러 외부 API(Application Programming Interface)와 연동해 데이터를 가져오거나 기능을 활용합니다. 예를 들어 날씨 정보를 위해 기상 API를 쓰거나, 결제를 위해 PG(Payment Gateway) API를 호출할 수 있습니다. Next.js의 Route Handler는 이런 외부 API와 통합할 서버 환경을 제공하며, 브라우저에서 외부 API를 직접 호출하는 대신 Route Handler를 거치면 여러 이점을 얻을 수 있습니다. 이 글에서는 Route Handler로 외부 API를 통합하는 흐름과, 이 방식의 이점과 주의사항을 함께 정리합니다.
왜 Route Handler를 통해 외부 API를 호출하나요?
브라우저에서 JavaScript로 외부 API를 직접 호출할 수도 있지만, Route Handler를 거치면 비밀 값과 응답 형식을 서버에서 관리할 수 있습니다.
가장 큰 이점은 보안입니다. 이 실습의 OpenWeather 키처럼 서버에서 관리해야 하는 비밀 키는 클라이언트 코드에 드러내지 않습니다. Route Handler에서 외부 API를 호출하면 API 키를 서버 환경 변수(.env.local)에 저장하고 서버에서만 읽을 수 있어, 브라우저 번들에 키가 노출되는 것을 막을 수 있습니다.
CORS(Cross-Origin Resource Sharing) 문제도 피할 수 있습니다. 브라우저의 동일 출처 정책(Same-Origin Policy) 때문에 다른 도메인의 API를 직접 호출하는 데는 제약이 있지만, Route Handler가 서버에서 보내는 외부 요청은 브라우저의 CORS 제약을 받지 않습니다. Route Handler가 외부 API에서 데이터를 가져오면, 클라이언트는 같은 출처의 Route Handler에만 요청을 보내면 됩니다.
외부 API가 돌려주는 데이터의 형식이 클라이언트가 원하는 형식과 다를 수 있는데, Route Handler에서 데이터를 가져온 뒤 필요한 형태로 가공하거나 걸러 클라이언트에게 알맞은 데이터를 줄 수 있습니다. 여러 외부 API를 조합해야 할 때도 클라이언트가 각각 호출하는 대신 Route Handler에서 한 번에 처리해 네트워크 왕복(round trip) 횟수를 줄일 수 있습니다. 외부 API 호출에서 생기는 네트워크 오류나 API 오류를 한곳에서 처리하고 기록할 수 있으며, 호출 횟수를 제한하는 외부 API라면 Route Handler에서 호출을 관리해 속도 제한(rate limiting)을 더 효율적으로 다루고 재시도 로직도 구현할 수 있습니다.
외부 API 호출을 위한 기본 도구: fetch API
Node.js 환경의 Next.js Route Handler에서는 웹 표준 fetch API나 axios 같은 라이브러리로 외부 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;
}
}fetch는 404나 500 같은 오류 상태를 받아도 Promise를 거부하지 않고 정상적으로 Response를 돌려줍니다. 그래서 상태 코드가 200번대일 때만 true인 response.ok를 직접 검사해 예외로 바꿔야 합니다. catch는 네트워크 실패와 이렇게 던진 오류를 함께 받고, 기록한 뒤 다시 throw하므로 fetchData를 호출한 쪽도 실패를 알 수 있습니다.
실습: 날씨 정보를 가져오는 Route Handler 생성
OpenWeatherMap API로 특정 도시의 현재 날씨 정보를 가져오는 Route Handler를 만들어 보겠습니다. 먼저 OpenWeatherMap 웹사이트에 가입하고, 로그인한 뒤 API keys 메뉴에서 키를 만듭니다. 이때 그 키에 Current Weather 사용 권한이 있는지, 현재 요금제와 호출 한도는 어떤지 확인하고 발급받은 키를 복사해 둡니다.
다음으로 프로젝트 루트에 .env.local 파일을 만들고 발급받은 OpenWeatherMap API 키를 추가합니다. YOUR_OPENWEATHER_API_KEY_HERE 부분은 실제로 발급받은 키로 바꾸세요.
# .env.local
OPENWEATHER_API_KEY=YOUR_OPENWEATHER_API_KEY_HERE이 예제는 도시 이름 q를 받는 Current Weather 호출을 다루는 실습입니다. OpenWeather 공식 문서는 내장된 도시 이름 조회를 아직 제공하지만, 사용 중단을 권고하는 기능으로 분류하고 더 이상 갱신하지 않는다고 설명합니다. 새로 연동할 때는 별도의 Geocoding API로 좌표를 얻는 방식도 검토합니다.
이제 src/app/api/weather/route.ts에 Route Handler를 구현합니다.
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')?.trim();
// 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 notFound = response.status === 404;
console.error('OpenWeatherMap API Error:', { status: response.status });
// 제공자 인증·호출 한도 오류를 애플리케이션 사용자의 오류로 그대로 전달하지 않습니다.
return NextResponse.json(
{ message: notFound ? '도시를 찾을 수 없습니다.' : '날씨 제공 서비스의 응답을 처리할 수 없습니다.' },
{ status: notFound ? 404 : 502 }
);
}
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에서 오류 발생:', {
name: error instanceof Error ? error.name : 'UnknownError',
});
return NextResponse.json(
{ message: '서버에서 날씨 정보를 가져오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}성공한 응답은 제공자의 필드 구조를 가정해 필요한 값만 골라냅니다. 빈 도시 이름은 400, 키 누락은 500, 제공자의 "도시 없음" 응답은 404, 그 밖의 비정상 HTTP 응답은 502로 바꿉니다. 네트워크 예외와 파싱 예외는 이 예제의 일반 오류 분기인 500으로 처리합니다.
날씨 조회의 서버 요청과 응답 경계
city와 서버 키가 정상인 원 GET의 공통 관찰 단계에서 fetch·HTTP·JSON·필드 조건별 종료를 비교합니다. 200·404·502·500을 같은 실행의 연속 응답으로 잇지 않고 서버 키와 공개 응답 경계를 유지합니다.
NEXT · 11.4 · 외부 API
날씨 API의 비밀 키는 서버 요청에만 더합니다
유효한 도시·서버 키·제공자 응답의 성공 경로입니다. 오른쪽 화살표는 요청, 반대쪽은 응답입니다. 서버가 city를 q로 전달하며 appid를 추가하고 JSON을 가공합니다.
빈 city는 400, 키 누락은 500입니다. 제공자 404는 404, 그 밖의 비정상 HTTP 응답은 502, 네트워크·파싱 예외는 500으로 처리합니다. 공개 아이콘 이미지는 브라우저가 제공자에서 직접 가져옵니다. 응답 스키마 검증·인증·속도 제한·재시도는 구현하지 않았습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- city와 서버 키가 정상인 원 GET의 try 블록이다. fetch 응답을 받으면 response.ok를 검사한다. HTTP 실패의 404는 404로, 그 밖은 502로 끝난다. HTTP 정상일 때만 JSON을 읽고 sys/main/weather 필드를 가공하여 200으로 응답한다. fetch/JSON 파싱/필드 접근 예외는 catch의 500으로 합류한다. HTTP 200이라고 유효한 스키마가 보장되는 것은 아니다. 아래 원 그림은 성공 요청/응답 방향과 서버 키 경계이며 이 제어 분기는 새로운 서버 보안 기능이나 재시도를 나타내지 않는다.
- 서버는 도시 이름에 서버의 appid를 더해 외부 API를 호출하고 성공 응답에서 필요한 필드를 골라 브라우저로 보낸다.
- 요청과 응답은 서로 다른 연결선으로 표시하며 비밀 키는 서버에서 외부 API로만 보낸다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 날씨 조회의 서버 요청과 응답 경계 |
| 6 | city와 서버 키가 정상인 원 GET의 try 블록이다. fetch 응답을 받으면 response.ok를 검사한다. HTTP 실패의 404는 404로, 그 밖은 502로 끝난다. HTTP 정상일 때만 JSON을 읽고 sys/main/weather 필드를 가공하여 200으로 응답한다. fetch/JSON 파싱/필드 접근 예외는 catch의 500으로 합류한다. HTTP 200이라고 유효한 스키마가 보장되는 것은 아니다. 아래 원 그림은 성공 요청/응답 방향과 서버 키 경계이며 이 제어 분기는 새로운 서버 보안 기능이나 재시도를 나타내지 않는다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| provider 응답 대기 city와 모듈에서 읽은 서버 키가 정상인 GET이 q·appid·metric·kr로 외부 fetch를 실행합니다. | 공통 전제: city·서버 key 정상 · 서버 요청: q·appid·units metric·lang kr · 브라우저 키 공개: 없음 | 외부 Promise의 응답 또는 거부를 기다립니다. 빈 city 400과 키 누락 500은 이 선택 범위 이전의 독립 검사입니다. |
| HTTP 응답과 거부 판정 fetch 거부는 catch의 500으로 끝나고, 응답을 받으면 response.ok를 검사합니다. | fetch 거부: 500 종료 · HTTP 실패 404: 404 종료 · 다른 HTTP 실패: 502 종료 · HTTP 정상: JSON 읽기·필드 가공으로 진행 | 같은 fetch 결과 판정에서 실패 경로는 종료하고 정상 HTTP만 JSON 처리를 시작합니다. 종료한 실패가 다음 가공 명령을 실행하지 않습니다. |
| 정상 HTTP의 자료 판정 JSON을 읽고 sys·main·weather 필드에 접근할 수 있는지에 따라 공개 자료 또는 catch 오류가 정해집니다. | HTTP 정상·자료 정상: 공개 날씨 필드 준비 · JSON·필드 예외: 500 종료 · 앞선 실패: 404·502·fetch 거부 500은 이미 종료 · HTTP 200: 유효한 스키마 보장 아님 | 필드 가공까지 성공해야 processedData를 만들며 HTTP 성공만으로 200 공개 응답을 확정하지 않습니다. |
| 조건별 응답 완료 자료 가공 성공은 processedData의 200이며 각 실패는 해당 오류 응답으로 반환을 마칩니다. | 자료 가공 성공: 200·필요한 날씨 필드 · provider 도시 없음: 404 · 다른 provider HTTP 실패: 502 · fetch·JSON·필드 예외: 500 | 각 조건에서 하나의 응답으로 끝납니다. 성공 반환 뒤 예외 경로를 이어 실행하지 않으며 retry·스키마 검증은 원 예제에 없습니다. |
날씨 조회의 서버 요청과 응답 경계
city와 서버 키가 정상인 원 GET의 공통 관찰 단계에서 fetch·HTTP·JSON·필드 조건별 종료를 비교합니다. 200·404·502·500을 같은 실행의 연속 응답으로 잇지 않고 서버 키와 공개 응답 경계를 유지합니다.
NEXT · 11.4 · 외부 API
날씨 API의 비밀 키는 서버 요청에만 더합니다
유효한 도시·서버 키·제공자 응답의 성공 경로입니다. 오른쪽 화살표는 요청, 반대쪽은 응답입니다. 서버가 city를 q로 전달하며 appid를 추가하고 JSON을 가공합니다.
빈 city는 400, 키 누락은 500입니다. 제공자 404는 404, 그 밖의 비정상 HTTP 응답은 502, 네트워크·파싱 예외는 500으로 처리합니다. 공개 아이콘 이미지는 브라우저가 제공자에서 직접 가져옵니다. 응답 스키마 검증·인증·속도 제한·재시도는 구현하지 않았습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- city와 서버 키가 정상인 원 GET의 try 블록이다. fetch 응답을 받으면 response.ok를 검사한다. HTTP 실패의 404는 404로, 그 밖은 502로 끝난다. HTTP 정상일 때만 JSON을 읽고 sys/main/weather 필드를 가공하여 200으로 응답한다. fetch/JSON 파싱/필드 접근 예외는 catch의 500으로 합류한다. HTTP 200이라고 유효한 스키마가 보장되는 것은 아니다. 아래 원 그림은 성공 요청/응답 방향과 서버 키 경계이며 이 제어 분기는 새로운 서버 보안 기능이나 재시도를 나타내지 않는다.
- 서버는 도시 이름에 서버의 appid를 더해 외부 API를 호출하고 성공 응답에서 필요한 필드를 골라 브라우저로 보낸다.
- 요청과 응답은 서로 다른 연결선으로 표시하며 비밀 키는 서버에서 외부 API로만 보낸다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 날씨 조회의 서버 요청과 응답 경계 |
| 6 | city와 서버 키가 정상인 원 GET의 try 블록이다. fetch 응답을 받으면 response.ok를 검사한다. HTTP 실패의 404는 404로, 그 밖은 502로 끝난다. HTTP 정상일 때만 JSON을 읽고 sys/main/weather 필드를 가공하여 200으로 응답한다. fetch/JSON 파싱/필드 접근 예외는 catch의 500으로 합류한다. HTTP 200이라고 유효한 스키마가 보장되는 것은 아니다. 아래 원 그림은 성공 요청/응답 방향과 서버 키 경계이며 이 제어 분기는 새로운 서버 보안 기능이나 재시도를 나타내지 않는다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| provider 응답 대기 city와 모듈에서 읽은 서버 키가 정상인 GET이 q·appid·metric·kr로 외부 fetch를 실행합니다. | 공통 전제: city·서버 key 정상 · 서버 요청: q·appid·units metric·lang kr · 브라우저 키 공개: 없음 | 외부 Promise의 응답 또는 거부를 기다립니다. 빈 city 400과 키 누락 500은 이 선택 범위 이전의 독립 검사입니다. |
| HTTP 응답과 거부 판정 fetch 거부는 catch의 500으로 끝나고, 응답을 받으면 response.ok를 검사합니다. | fetch 거부: 500 종료 · HTTP 실패 404: 404 종료 · 다른 HTTP 실패: 502 종료 · HTTP 정상: JSON 읽기·필드 가공으로 진행 | 같은 fetch 결과 판정에서 실패 경로는 종료하고 정상 HTTP만 JSON 처리를 시작합니다. 종료한 실패가 다음 가공 명령을 실행하지 않습니다. |
| 정상 HTTP의 자료 판정 JSON을 읽고 sys·main·weather 필드에 접근할 수 있는지에 따라 공개 자료 또는 catch 오류가 정해집니다. | HTTP 정상·자료 정상: 공개 날씨 필드 준비 · JSON·필드 예외: 500 종료 · 앞선 실패: 404·502·fetch 거부 500은 이미 종료 · HTTP 200: 유효한 스키마 보장 아님 | 필드 가공까지 성공해야 processedData를 만들며 HTTP 성공만으로 200 공개 응답을 확정하지 않습니다. |
| 조건별 응답 완료 자료 가공 성공은 processedData의 200이며 각 실패는 해당 오류 응답으로 반환을 마칩니다. | 자료 가공 성공: 200·필요한 날씨 필드 · provider 도시 없음: 404 · 다른 provider HTTP 실패: 502 · fetch·JSON·필드 예외: 500 | 각 조건에서 하나의 응답으로 끝납니다. 성공 반환 뒤 예외 경로를 이어 실행하지 않으며 retry·스키마 검증은 원 예제에 없습니다. |
위 그림은 날씨 조회에서 서버가 보내는 요청과 받는 응답, 그리고 제공자의 HTTP 상태와 가공 중 예외를 구별해 보여 줍니다. 아래쪽의 성공 경로에서 WeatherPage가 도시 이름을 city로 보내면, Next.js 서버의 weather/route.ts가 서버의 키를 읽어 q와 appid를 붙여 OpenWeather를 호출하고, 받은 원본 JSON에서 필요한 필드만 골라 날씨 JSON으로 브라우저에 돌려줍니다. 비밀 키는 서버에서 외부 API로만 보내집니다.
위쪽의 흐름은 city와 서버 키가 정상일 때 try 블록 안에서 일어나는 분기입니다. fetch 응답을 받으면 response.ok를 검사해, HTTP 실패 가운데 404는 404로, 그 밖은 502로 끝납니다. HTTP가 정상일 때만 JSON을 읽고 sys, main, weather 필드를 가공해 200으로 응답하며, fetch 자체의 예외나 JSON 파싱, 필드 접근의 예외는 catch의 500으로 모입니다. 그림의 단계도 제공자 응답 대기, HTTP 응답과 거부 판정, 정상 HTTP의 데이터 판정, 조건별 응답 완료의 순서입니다. HTTP 200이라고 유효한 스키마가 보장되지는 않으며, 그림의 설명처럼 응답 스키마 검증, 인증, 속도 제한, 재시도는 구현하지 않았습니다.
다음 분기는 유효한 키와 제공자 응답을 전제로 확인할 내용이며, 이 문서에서 새로 측정한 결과가 아닙니다. 개발 서버(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를 호출해 날씨 정보를 화면에 표시해 보겠습니다. 입력값과 버튼 클릭을 다루려면 useState와 이벤트 핸들러가 필요하므로 파일 맨 위에 "use client"를 둡니다. 아래 코드에서는 상태 네 개(city, weather, loading, error)와 fetchWeather 함수를 중심으로 보면 됩니다. fetchWeather는 요청 전에 이전 결과와 오류를 지우고, URLSearchParams로 도시 이름을 쿼리 문자열에 맞게 인코딩해 보냅니다. 응답이 실패하면 Route Handler가 담아 보낸 message를 꺼내 오류로 던지고, 성공이든 실패든 finally에서 로딩 상태를 끕니다.
"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.trim()) {
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"
aria-label="도시 이름"
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>
);
}클라이언트에서는 Route Handler의 상대 경로(/api/weather?city=Seoul)를 쓰므로, 개발 환경(localhost)과 배포 환경에서 URL이 저절로 맞춰집니다. 오류 처리와 로딩 상태 관리로 사용자에게 더 나은 경험을 줍니다.
WeatherData 타입 표기는 응답의 런타임 검증을 하지 않습니다. 서버와 클라이언트 모두 제공자 응답의 필드 구조를 가정하므로, 실제 연동에 필요한 스키마 검증은 따로 정합니다. 또한 날씨 JSON은 Route Handler를 거치지만, 공개된 날씨 아이콘 이미지는 <img>가 제공자에게서 직접 가져옵니다.
고려 사항
API 키는 개발 환경에서는 .env.local에 두고 process.env.VAR_NAME으로 읽으며, Vercel이나 AWS 같은 배포 환경에서는 그 플랫폼의 환경 변수 설정 기능으로 안전하게 관리합니다. 개발, 스테이징, 프로덕션 환경마다 다른 외부 API 엔드포인트나 키를 써야 한다면 이것도 환경 변수로 나누어 관리합니다. 외부 API에 민감한 데이터를 보내야 한다면 HTTPS를 쓰는지, OAuth 같은 추가 인증이 필요한지 확인합니다.
자주 호출되는 외부 API는 Route Handler 안에서 응답을 캐싱해 호출 횟수를 줄이고 성능을 높일 수 있습니다. 외부 API 호출이 실패할 때를 대비해 오류 처리 로직을 탄탄하게 만들고, 필요하다면 재시도(retry) 장치도 구현합니다.
지금의 코드는 응답 가공과 기본 오류 처리를 보여 줄 뿐입니다. 사용자 인증, 호출 횟수 제한, 명시적인 캐시 정책, 타임아웃과 재시도는 구현하지 않았으므로, Route Handler를 거친다는 사실만으로 이런 기능이 생기지는 않습니다.
정리
- 외부 API를 Route Handler로 감싸면 비밀 키를 서버에만 두고, 브라우저의 CORS 제약을 피하며, 필요한 필드만 가공해 돌려줄 수 있습니다.
- API 키는
.env.local이나 배포 플랫폼의 환경 변수에 두고, 서버 코드에서만process.env로 읽습니다. - 제공자의 응답은
response.ok로 먼저 검사하고, 제공자의 오류 상태를 애플리케이션의 상태 코드로 바꾸어 돌려줍니다. - HTTP 200이어도 응답 스키마가 보장되지 않으므로, 타입 표기와 별개로 필요한 런타임 검증을 정합니다.
- 클라이언트는 상대 경로로 Route Handler를 호출하며, 이미지처럼 브라우저가 직접 가져오는 외부 자원은 서버를 거치지 않습니다.
연습 문제
1. 제공자 상태와 앱의 응답
city와 키가 정상이라고 가정합니다. 제공자가 429를 돌려주면 브라우저에 429가 그대로 전달될까요? 제공자의 HTTP 상태가 200이어도 JSON에 sys가 없다면 어떤 상태가 될까요? 정상 응답의 temperature 24를 화면에 표시할 때 단위는 무엇이며, 날씨 JSON과 공개 아이콘 이미지가 거치는 경로는 같을까요?
해설 보기
429는 404가 아닌 비정상 응답이므로 이 예제는 502로 바꿉니다. HTTP 200 뒤에 sys.country를 읽을 수 없으면 가공 중에 예외가 나서 catch의 500으로 끝납니다. TypeScript의 WeatherData 표기만으로 필드 구조가 검증되지는 않습니다.
요청의 units가 metric이므로 temperature는 섭씨이고, 화면에는 24°C로 표시합니다. 날씨 JSON은 /api/weather를 거치지만, 아이콘 img는 공개된 제공자 URL을 브라우저가 직접 요청합니다. 그러니 모든 외부 요청을 서버가 대신한다거나, 지금 코드에 타임아웃이나 호출 제한이 구현되어 있다고 넓혀 생각하지 않습니다.
2. 키도 없고 도시 이름도 비어 있을 때
서버에 키가 없고, city도 공백뿐인 요청이 들어왔습니다. 응답은 400일까요, 500일까요? .env.local에 키를 추가한 뒤 개발 서버를 다시 시작하지 않으면 어떻게 될까요?
해설 보기
400입니다. 첫 번째 city 검사에서 끝나므로 외부 API를 호출하지 않으며, city가 정상이어야 키 누락의 500까지 진행합니다.
키는 함수 안이 아니라 모듈 최상위의 const에서 읽습니다. 그래도 next dev는 .env.local 같은 환경 파일을 감시하다가 내용이 바뀌면 터미널에 Reload env: .env.local을 출력하고, 환경 변수를 다시 읽은 뒤 서버 모듈 캐시를 비웁니다. 그래서 다음 요청에서 이 모듈이 다시 평가되어 새 키를 읽고, city가 정상이면 키 누락의 500 대신 외부 API 호출로 진행합니다. 터미널에 이 줄이 나오지 않거나 새 값이 반영되지 않으면 개발 서버를 다시 시작합니다. 반면 next start로 띄운 프로덕션 서버는 시작할 때 읽은 값을 계속 쓰므로, 값을 바꾸면 서버를 다시 시작해야 합니다.
3. 클라이언트 컴포넌트에서 키를 직접 쓰기
WeatherPage에서 Route Handler를 거치지 않고 process.env.OPENWEATHER_API_KEY로 OpenWeather를 직접 호출하도록 바꾸려고 합니다. 이 방식은 어떤 문제가 있을까요?
해설 보기
NEXT_PUBLIC_ 접두사가 없는 환경 변수는 브라우저 번들에 들어가지 않으므로, 클라이언트 컴포넌트에서는 값이 undefined가 되어 호출이 실패합니다. 그렇다고 NEXT_PUBLIC_OPENWEATHER_API_KEY로 바꾸면 키가 번들에 그대로 들어가 누구나 볼 수 있게 됩니다. 비밀 키를 쓰는 호출은 이 글처럼 Route Handler 같은 서버 코드에 두고, 클라이언트는 그 Route Handler만 호출해야 합니다.
이것으로 11장을 마칩니다. 11장에서는 Route Handler로 API를 만들고, HTTP 메서드별로 요청과 응답을 다루고, Proxy로 공통 진입 규칙을 처리하고, 외부 API를 서버에서 안전하게 통합하는 방법을 정리했습니다. 다음 장의 이미지 최적화부터는 next/image로 이미지를 최적화하는 방법을 알아봅니다.