안동민 개발노트

안동민 개발노트

Route Handler 생성HTTP 메서드 처리API Proxy외부 API와의 통합
본문 시작
  1. 홈
  2. 문서
  3. Next.js
  4. 11장 : Route Handler
  5. 외부 API와의 통합
  1. Next.js
  2. 외부 API와의 통합

외부 API와의 통합

외부 날씨 API를 서버 Route Handler로 감싸 비밀 API 키를 서버에서 관리하고 브라우저에 필요한 응답만 전달합니다.

대부분의 현대 웹 애플리케이션은 독립적으로 동작하기보다, 다양한 외부 API(Application Programming Interface)와 연동해 데이터를 가져오거나 기능을 활용합니다.

예를 들어 날씨 정보를 위해 기상청 API를 쓰거나, 결제를 위해 PG(Payment Gateway) API를 호출할 수 있습니다.

Next.js의 Route Handler는 이러한 외부 API와의 통합을 위한 서버 환경을 제공합니다.

클라이언트(브라우저)에서 직접 외부 API를 호출하는 대신, Next.js Route Handler를 경유하면 여러 가지 중요한 이점을 얻을 수 있습니다.

이 절에서는 Next.js Route Handler로 외부 API를 통합하는 흐름과, 이 방식의 이점 및 주의사항을 함께 정리합니다.


왜 Route Handler를 통해 외부 API를 호출하는지?

클라이언트(브라우저)에서 JavaScript를 사용하여 직접 외부 API를 호출할 수도 있지만, Next.js Route Handler를 경유하면 비밀 값과 응답 계약을 서버에서 통제할 수 있습니다.

보안 (비밀 API 키 숨김): 이 실습의 OpenWeather 키처럼 서버에서 관리할 비밀 키는 클라이언트 코드에 노출하지 않습니다.

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 API 키 발급
  • OpenWeatherMap 웹사이트(https://openweathermap.org/)에 가입합니다.
  • 로그인 후 API keys 섹션에서 키를 생성하고, 해당 키의 Current Weather 사용 권한과 현재 요금제·호출 한도를 확인합니다.
  • 발급받은 API 키를 복사해 둡니다.

환경 변수 설정: 프로젝트 루트에 .env.local 파일을 생성하고 발급받은 OpenWeatherMap API 키를 추가합니다.

# .env.local
OPENWEATHER_API_KEY=YOUR_OPENWEATHER_API_KEY_HERE

주의: YOUR_OPENWEATHER_API_KEY_HERE 부분을 실제 발급받은 키로 대체하세요.

이 예제는 도시명 q를 받는 Current Weather 호출을 읽는 실습입니다. OpenWeather 공식 문서는 내장 도시명 조회를 아직 제공하지만 사용 중단 권고 기능으로 분류하고 업데이트하지 않는다고 설명합니다. 새로운 연동에서는 별도 Geocoding API로 좌표를 얻는 방식도 검토합니다.

Route Handler 구현 (src/app/api/weather/route.ts)
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')?.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으로 처리합니다.

날씨 조회의 서버 요청과 응답 경계

날씨 조회의 서버 요청과 응답 경계

날씨 조회의 성공 경로서버는 도시 이름에 서버의 appid를 더해 외부 API를 호출하고 성공 응답에서 필요한 필드를 골라 브라우저로 보낸다.WeatherPage도시 이름 입력Next.js 서버weather/route.ts키 읽기 · 응답 가공OpenWeather현재 날씨city날씨q · appidJSON
날씨 조회의 성공 경로요청과 응답은 서로 다른 연결선으로 표시하며 비밀 키는 서버에서 외부 API로만 보낸다.WeatherPage도시 이름 입력Next.js 서버weather/route.ts키 읽기 · 응답 가공OpenWeather현재 날씨city날씨 JSONq · appid원본 JSON

테스트 방법: 아래는 유효한 키·제공자 응답을 전제로 확인할 분기이며, 이 문서에서 새로 측정한 결과가 아닙니다.

개발 서버(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 (클라이언트 컴포넌트 예시)
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.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>
  );
}
클라이언트에서 호출 시 유의사항
  • 클라이언트에서는 Next.js 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 호출 횟수를 줄이고 성능을 향상시킬 수 있습니다.
  • 오류 처리 및 재시도: 외부 API 호출이 실패할 경우를 대비하여 견고한 오류 처리 로직과 필요한 경우 재시도(Retry) 메커니즘을 구현해야 합니다.
  • 환경별 설정: 개발, 스테이징, 프로덕션 환경마다 다른 외부 API 엔드포인트나 키를 사용해야 할 경우, 환경 변수를 통해 유연하게 관리해야 합니다.

현재 코드는 응답 가공과 기본 오류 처리를 보여줍니다. 사용자 인증, 호출 횟수 제한, 명시적 캐시 정책, 타임아웃·재시도는 구현하지 않았으므로 Route Handler를 경유한다는 사실만으로 이러한 기능이 생기지는 않습니다.

API Proxy

이전 페이지

이미지 최적화

다음 페이지

이 페이지의 목차

왜 Route Handler를 통해 외부 API를 호출하는지?외부 API 호출을 위한 기본 도구: fetch API실습: 날씨 정보를 가져오는 Route Handler 생성클라이언트 컴포넌트에서 Route Handler 호출고려 사항