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) → 외부 APIRoute 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 한 곳에 모이니까 나중에 수정할 때도 편했다. 처음부터 이렇게 만들었으면 좋았을 텐데, 클라이언 트에서 직접 호출하는 게 더 빨리 돌아가 보여서 그냥 썼던 게 문제였다.
보안 관련 설정은 나중에 고치는 것보다 처음에 구조를 잡아두는 게 낫다는 걸 다시 확인했다.