본문으로 건너뛰기

안동민 개발노트

본문 시작

외부 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를 통합하는 흐름과, 이 방식의 이점 및 주의사항을 함께 정리합니다.

HTML 다이어그램: /docs/next/ch11/ch11-4/1.html

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

아래 다이어그램은 브라우저 직접 호출과 Route Handler 경유 호출의 차이를 보안, CORS, 응답 가공 관점에서 비교합니다.

HTML 다이어그램: /docs/next/ch11/ch11-4/2.html

클라이언트(브라우저)에서 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 API 키 발급
  • 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 부분을 실제 발급받은 키로 대체하세요.

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');

  // 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 (클라이언트 컴포넌트 예시)
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를 호출하는 문제가 아니라, 비밀 키를 어디에 둘지, 어떤 값을 검증할지, 실패를 어떤 형태로 돌려줄지, 응답을 얼마나 캐싱할지를 함께 정하는 서버 경계 설계입니다.

이 기준이 정리되어 있어야 클라이언트 컴포넌트는 화면 상태 처리에 집중할 수 있습니다.

HTML 다이어그램: /docs/next/ch11/ch11-4/3.html

고려 사항

  • API 키 관리: 개발 환경(.env.local)에서는 process.env.VAR_NAME으로 접근하고, 배포 환경(Vercel, AWS 등)에서는 해당 플랫폼의 환경 변수 설정 기능을 사용하여 API 키를 안전하게 관리해야 합니다.
  • 보안: 외부 API 호출 시 민감한 데이터를 전송해야 한다면, HTTPS를 사용하는지, 추가적인 인증(OAuth 등)이 필요한지 확인해야 합니다.
  • 속도 제한 및 캐싱: 자주 호출되는 외부 API의 경우, Route Handler 내에서 응답을 캐싱하여 외부 API 호출 횟수를 줄이고 성능을 향상시킬 수 있습니다.
  • 오류 처리 및 재시도: 외부 API 호출이 실패할 경우를 대비하여 견고한 오류 처리 로직과 필요한 경우 재시도(Retry) 메커니즘을 구현해야 합니다.
  • 환경별 설정: 개발, 스테이징, 프로덕션 환경마다 다른 외부 API 엔드포인트나 키를 사용해야 할 경우, 환경 변수를 통해 유연하게 관리해야 합니다.

아래 다이어그램은 외부 API를 운영할 때 먼저 정해야 할 장애, 비용, 보안 기준을 한 화면에 정리합니다.

HTML 다이어그램: /docs/next/ch11/ch11-4/4.html

다음 다이어그램은 외부 API를 직접 노출하지 않고 Next.js Route Handler에서 비밀 키, 캐싱, 오류 응답을 통제하는 기준입니다.

HTML 다이어그램: /docs/next/ch11/ch11-4/6.html

아래 다이어그램은 클라이언트 요청, Route Handler fetch, 외부 API 응답 가공이 이어지는 구조를 압축해 보여줍니다.

HTML 다이어그램: /docs/next/ch11/ch11-4/7.html

Next.js Route Handler를 통한 외부 API 통합은 보안, 성능, 개발 효율성 측면에서 많은 이점을 제공합니다.

이를 통해 더욱 강력하고 확장 가능한 웹 애플리케이션을 구축할 수 있습니다.

아래 다이어그램은 외부 API와의 통합에서 요청 진입점, 응답 형태, 오류 처리를 함께 확인합니다.

HTML 다이어그램: /docs/next/ch11/ch11-4/8.html