cyh-lab.com
← 블로그 목록으로

cyh-lab.com 개발기 · 2026-08-20

Next.js Route Handler로 API 키를 클라이언트에서 숨기는 방법

브라우저 네트워크 탭에 API 키가 그대로 찍혀 있었다. NEXT_PUBLIC_ 환경변수의 함정과 Route Handler로 옮긴 과정을 정리했다.

작성자: cyh-lab.com 운영자

chart.cyh-lab.com에서 주식 시세를 가져올 때 처음엔 클라이언트에서 직접 API를 호출했다. 그러다 브라우저 네트워크 탭을 열어보니 요청 헤더에 API 키가 그대로 노출되고 있었다. 당연한 거였는데 만들면서 미처 생각을 못 했다. Route Handler로 옮기면서 해결한 과정을 기록해둔다.

문제: 클라이언트에서 직접 fetch하면 키가 보인다

처음 코드는 이런 구조였다.

// 클라이언트 컴포넌트에서 직접 호출
const res = await fetch(`https://api.example.com/stock/${ticker}`, {
  headers: {
    Authorization: `Bearer ${process.env.NEXT_PUBLIC_API_KEY}`,
  },
})

NEXT_PUBLIC_ 접두어가 붙은 환경변수는 빌드 시에 번들에 포함된다. 브라우 저 소스코드에서 키 값을 직접 볼 수 있다. 네트워크 탭에서는 요청 헤더까지 그대로 나온다.

해결: Route Handler를 프록시로 쓴다

Next.js App Router의 Route Handler는 서버에서만 실행된다. API 키를 서버 환경변수로 놓고, 클라이언트는 Route Handler를 통해서만 데이터를 받도록 구조를 바꿨다.

클라이언트 → /api/stock/[ticker] (Route Handler) → 외부 API

Route Handler 파일은 app/api/stock/[ticker]/route.ts에 만든다.

// app/api/stock/[ticker]/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(
  request: NextRequest,
  { params }: { params: { ticker: string } }
) {
  const { ticker } = params

  const res = await fetch(`https://api.example.com/stock/${ticker}`, {
    headers: {
      Authorization: `Bearer ${process.env.API_KEY}`,  // NEXT_PUBLIC_ 없음
    },
    next: { revalidate: 300 },
  })

  if (!res.ok) {
    return NextResponse.json({ error: 'Failed to fetch' }, { status: res.status })
  }

  const data = await res.json()
  return NextResponse.json(data)
}

클라이언트에서는 외부 API 대신 이 Route Handler를 호출한다.

// 클라이언트 컴포넌트
const res = await fetch(`/api/stock/${ticker}`)
const data = await res.json()

이렇게 하면 API_KEY 환경변수는 서버에서만 읽히고, 브라우저로 노출되지 않는다.

환경변수 구분

Next.js 환경변수에는 두 종류가 있다.

NEXT_PUBLIC_이 붙은 것은 클라이언트 번들에 포함된다. 브라우저에서 읽혀야 하는 값(GA 측정 ID, 공개 API 엔드포인트 등)에 쓴다.

붙지 않은 것은 서버에서만 읽힌다. API 키, DB 연결 문자열처럼 외부에 노출되면 안 되는 값에 쓴다.

# .env.local
NEXT_PUBLIC_SITE_URL=https://chart.cyh-lab.com   # 클라이언트에서 접근 가능
API_KEY=sk-...                                     # 서버에서만 접근 가능

처음에 NEXT_PUBLIC_API_KEY로 만들었던 게 문제의 시작이었다.

추가로 챙긴 것들

요청 유효성 검사. Route Handler가 외부에 공개되는 엔드포인트라는 점을 잊으면 안 된다. 허용된 ticker 형식인지, 요청 빈도가 지나치게 높지는 않은지 기본적인 검증을 추가했다.

const validTicker = /^[A-Z]{1,5}$/.test(ticker)
if (!validTicker) {
  return NextResponse.json({ error: 'Invalid ticker' }, { status: 400 })
}

캐싱. Route Handler에서 fetch할 때 next: { revalidate: 300 }을 붙이면 5분마다 외부 API를 다시 호출한다. 클라이언트 요청이 몰려도 외부 API 호출 횟수를 줄일 수 있다.

바꾸고 나서

API 키가 노출되는 문제는 해결됐고, 외부 API 호출 관련 로직이 Route Handler 한 곳에 모이니까 나중에 수정할 때도 편했다. 처음부터 이렇게 만들었으면 좋았을 텐데, 클라이언 트에서 직접 호출하는 게 더 빨리 돌아가 보여서 그냥 썼던 게 문제였다.

보안 관련 설정은 나중에 고치는 것보다 처음에 구조를 잡아두는 게 낫다는 걸 다시 확인했다.