← 블로그 목록
Vercel 배포환경변수CORS서버리스배포 오류

로컬에서는 되는데 Vercel 배포하면 깨지는 5가지: 환경변수·빌드·CORS 원인별 해결

AI 코딩으로 만든 웹앱이 배포 후에만 실패하는 다섯 가지 원인을 환경변수, Node 버전, CORS, 정적 파일 경로, 서버리스 DB 연결로 나눠 정리했습니다.

Vibeollio 팀-

로컬에서는 되는데 배포하면 깨진다

AI 코딩 도구로 만든 웹앱이 로컬에서는 멀쩡하다가 Vercel에 올리는 순간 흰 화면이나 500 에러를 뱉는 경우가 많습니다. 코드가 잘못된 게 아니라, 로컬 개발 서버가 대신 해주던 일들이 프로덕션에는 없기 때문입니다.

자주 걸리는 다섯 가지를 원인부터 정리합니다. 증상만 보고 고치면 같은 문제가 다른 얼굴로 다시 돌아옵니다.

1. 환경변수가 프로덕션에 없다

.env.local은 .gitignore에 들어 있으므로 저장소에 올라가지 않습니다. 당연한 동작인데, 배포 후 API 호출이 전부 실패하는 이유가 대개 이것입니다.

Vercel 대시보드의 Settings → Environment Variables에 같은 값을 다시 넣어야 합니다. 그리고 환경변수를 추가한 뒤에는 재배포해야 반영됩니다. 기존 배포본은 빌드 시점의 값을 그대로 들고 있습니다.

접두사 규칙은 프레임워크마다 다릅니다. 여기서 자주 헷갈립니다.

프레임워크 브라우저에 노출되는 접두사
Next.js NEXT_PUBLIC_
Vite VITE_
Create React App REACT_APP_

접두사가 붙은 변수는 빌드 결과물에 그대로 박혀 브라우저에서 읽을 수 있습니다. 따라서 API 시크릿 키에는 절대 접두사를 붙이면 안 됩니다. 접두사 없는 변수는 서버 사이드(API 라우트, 서버 컴포넌트)에서만 접근됩니다.

2. Node 버전이 로컬과 다르다

로컬에서는 최신 Node로 개발했는데 빌드 환경의 버전이 낮으면, 최신 문법이나 특정 패키지의 엔진 요구사항에서 설치·빌드가 실패합니다.

버전을 저장소에 고정해두는 게 가장 확실합니다.

# .nvmrc
20

package.json의 engines 필드로도 지정할 수 있습니다. 두 방법 모두 팀원과 CI가 같은 버전을 쓰게 만드는 효과가 있으니, 로컬 node --version을 확인해 맞춰두세요.

빌드 로그에 npm error가 뜨면서 의존성 단계에서 멈춘다면 락파일 불일치도 의심해봐야 합니다. npm ci는 package.json과 package-lock.json이 어긋나면 설치를 거부합니다. 로컬에서 npm install로 락파일을 갱신하고 커밋하면 해결됩니다.

3. CORS는 로컬에서 느슨한 게 아니다

흔한 오해부터 짚고 갑니다. CORS 정책은 로컬이라고 완화되지 않습니다. 브라우저는 로컬에서도 동일하게 적용합니다.

로컬에서만 되던 이유는 개발 서버의 프록시 설정 때문입니다. Vite의 server.proxy나 Next.js의 rewrite를 쓰면 브라우저 입장에서는 같은 출처로 보이므로 CORS 자체가 발생하지 않습니다. 그런데 이 설정은 개발 서버 전용이라 프로덕션 빌드에는 없습니다.

해결은 프로덕션에도 같은 구조를 만드는 것입니다. 즉 서버 라우트를 경유시킵니다.

// app/api/quotes/route.ts
export async function GET() {
  const res = await fetch("https://external-api.com/quotes", {
    headers: { Authorization: `Bearer ${process.env.API_KEY}` },
  });
  return Response.json(await res.json());
}

클라이언트는 /api/quotes만 호출합니다. 부수 효과로 API 키가 브라우저에 노출되지 않는다는 이점도 있는데, 사실 이쪽이 더 중요한 이유입니다.

4. 정적 파일 경로가 어긋난다

이미지나 폰트가 배포 후에만 404가 난다면 대개 경로 문제입니다.

정적 파일은 프레임워크가 정한 위치에 둬야 합니다. Next.js와 Vite 모두 프로젝트 루트의 public/ 폴더가 정적 자산 위치입니다. dist나 .next는 빌드 결과물이 들어가는 곳이라 여기에 파일을 넣으면 다음 빌드에서 사라집니다.

참조는 절대 경로로 합니다. public/images/logo.png에 둔 파일은 코드에서 /images/logo.png로 씁니다. ./images/logo.png처럼 상대 경로로 쓰면 현재 URL 깊이에 따라 달라져서, 루트에서는 되고 /blog/abc에서는 깨지는 현상이 생깁니다.

대소문자도 확인하세요. 맥과 윈도우의 기본 파일시스템은 대소문자를 구분하지 않지만 배포 환경의 리눅스는 구분합니다. Logo.png를 logo.png로 참조하면 로컬에서만 됩니다. 배포 후 404가 나는데 경로는 맞아 보인다면 십중팔구 이것입니다.

5. 서버리스 환경에서 DB 커넥션이 고갈된다

Vercel의 함수는 요청이 오면 뜨고 끝나면 사라집니다. 여기에 일반적인 커넥션 풀 코드를 그대로 올리면, 인스턴스마다 풀을 새로 만들어 트래픽이 조금만 늘어도 DB의 최대 연결 수를 넘깁니다.

두 가지를 함께 적용합니다.

먼저 클라이언트 인스턴스를 재사용합니다. 모듈 스코프에 만들어두면 같은 인스턴스가 살아 있는 동안은 재사용됩니다.

// 개발 모드의 핫 리로드에서 인스턴스가 계속 쌓이는 것도 함께 막는다
const globalForDb = globalThis;
export const db = globalForDb.db ?? createClient();
if (process.env.NODE_ENV !== "production") globalForDb.db = db;

그리고 인스턴스당 풀 크기를 작게 잡습니다. 서버리스에서는 인스턴스 수가 늘어나는 구조라 풀을 크게 잡을수록 위험합니다. 연결 수가 계속 문제라면 커넥션 풀러(예: PgBouncer, 또는 DB 제공자가 주는 풀링 전용 접속 문자열)를 앞에 두는 방식을 검토하세요.

배포 전 체크리스트

  • Vercel 대시보드에 필요한 환경변수를 모두 넣고 재배포했는가
  • 시크릿 키에 공개 접두사(NEXT_PUBLIC_ 등)가 붙어 있지 않은가
  • 로컬에서 npm run build가 통과하는가 (dev 서버만 돌려본 상태로 배포하지 않기)
  • 외부 API 호출이 서버 라우트를 경유하는가
  • 정적 파일이 public/에 있고 /로 시작하는 절대 경로로 참조되는가
  • 파일명 대소문자가 코드와 정확히 일치하는가
  • .nvmrc 또는 engines로 Node 버전을 고정했는가

빌드는 성공했는데 화면이 흰색이면 어디를 봐야 하나요?

빌드가 통과했다면 런타임 오류입니다. 브라우저 콘솔을 먼저 열어보세요. 대개 정의되지 않은 환경변수를 참조하다 터지거나, 서버에서 받은 응답 형태가 예상과 달라 렌더 중 예외가 납니다. 서버 쪽 예외라면 Vercel 대시보드의 배포 상세에서 함수 로그를 확인할 수 있습니다.

로컬 빌드는 되는데 배포 빌드만 실패하는 이유는 뭔가요?

환경 차이를 의심하세요. 순서대로 Node 버전, 락파일 상태, 대소문자, 그리고 빌드 시점에 필요한 환경변수입니다. 마지막 항목이 특히 자주 걸립니다. 런타임에만 필요한 변수와 달리, 빌드 중에 읽는 변수는 없으면 빌드 자체가 실패합니다.

배포 후에 환경변수를 바꾸면 바로 반영되나요?

아닙니다. 재배포해야 합니다. 특히 빌드 시점에 결과물로 들어가는 공개 접두사 변수는 반드시 새로 빌드해야 바뀝니다. 값만 고치고 왜 안 바뀌는지 한참 찾는 일이 흔합니다.

배포까지 끝냈다면

배포가 끝나면 그때부터 진짜 문제가 보입니다. 내 브라우저에서는 멀쩡한데 다른 사람 환경에서는 안 되는 것들이 있고, 이건 직접 써본 사람만 알려줄 수 있습니다.

배포한 웹앱을 Vibeollio에 프로젝트 등록해서 공개해보세요. 링크만으로 바로 접속되는 웹앱은 방문자가 설치 없이 그 자리에서 눌러볼 수 있어 반응을 얻기 좋습니다. 댓글로 들어오는 오류 제보가 로컬 테스트보다 빠른 경우도 많습니다.

로컬에서는 되는데 Vercel 배포하면 깨지는 5가지: 환경변수·빌드·CORS 원인별 해결 | Vibeollio