Build Deploy Observability

3줄 요약

  • 프론트 배포는 HTML/CSS/JS 파일을 올리는 일처럼 보이지만, env, cache, API origin, asset version, error tracking이 함께 얽힌 운영 문제다.
  • Vite의 import.meta.env 같은 값은 빌드 시점에 bundle에 들어가므로 secret이 아니며, 정적 배포에서는 런타임 변경도 자동 반영되지 않는다.
  • 개인 프로젝트라도 dev/stage/prod API URL, CORS, Cookie domain, Sentry 같은 에러 수집 기준을 초기에 정하면 운영 대응이 쉬워진다.

핵심 정리

  • Vite SPA는 정적 파일을 만들고 CDN이나 정적 호스팅에 배포하는 흐름이 단순하다.
  • Next.js는 SSR, SSG, image optimization, server route 등 기능이 늘어나는 대신 배포 런타임 이해가 필요하다.
  • 브라우저 env 값은 공개 설정이다. secret은 서버 환경 변수, 백엔드, 또는 배포 플랫폼의 server-side secret에 둔다.
  • CDN cache는 성능을 높이지만 오래된 asset이나 HTML이 남아 배포 후 버그를 만들 수 있다.
  • Sentry 같은 error tracking은 사용자 브라우저에서 실제로 터진 오류와 trace를 모으는 데 필요하다.

백엔드 개발자가 헷갈리는 지점

  • 운영 서버 env만 바꾸면 이미 배포된 프론트 설정도 바뀐다고 생각하기 쉽다.
    • 판단 기준은 프론트가 정적 build artifact인지, runtime server가 있는지다.
    • Vite 정적 배포는 보통 다시 build/deploy해야 한다.
  • 브라우저 env에 secret을 넣어도 숨겨진다고 생각하기 쉽다.
    • 판단 기준은 브라우저로 내려가는 모든 값은 사용자에게 보인다는 점이다.
    • secret은 프론트 bundle에 넣지 않는다.
  • 캐시는 백엔드 API만의 문제라고 보기 쉽다.
    • 판단 기준은 HTML, JS asset, image, API cache가 모두 사용자 경험에 영향을 준다는 점이다.

개인 프로젝트 기준

확인 질문

  • Vite의 VITE_ env 값은 secret으로 써도 되는가?
    • 안 된다. 브라우저 bundle에 포함되는 공개 설정으로 봐야 한다.
  • 정적 배포 후 API URL을 바꾸려면 무엇을 확인해야 하는가?
    • 해당 값이 빌드 시점에 박힌 값인지, 런타임 설정으로 주입되는 값인지 확인해야 한다.
  • 프론트 observability가 필요한 이유는 무엇인가?
    • 사용자 브라우저에서 발생한 에러, 환경, 경로, 재현 정보를 서버 로그만으로는 알기 어렵기 때문이다.

01. 빌드 배포 환경 운영 상세

이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Vite와 Next.js의 배포 사고방식은 어떻게 다른가?
  • 프론트 env 변수는 왜 secret이 될 수 없는가?
  • CDN cache와 error tracking은 개인 프로젝트에서도 왜 도움이 되는가?

개요

프론트엔드 배포는 코드를 build해서 hosting에 올리는 것으로 끝나지 않는다. 브라우저가 어떤 API origin을 호출하는지, Cookie domain이 맞는지, asset cache가 오래된 파일을 가리키지 않는지, production error를 어디서 확인할지까지 포함한다. 백엔드 개발자에게 익숙한 런타임 env와 프론트 정적 build env의 차이를 특히 조심해야 한다. 운영 장애는 코드보다 환경, cache, 도메인 설정에서 시작되는 경우가 많다.

원리

Vite는 build 시점에 import.meta.env를 정적으로 치환한다. 따라서 정적 hosting에 올라간 JS bundle 안에는 그 시점의 값이 들어 있다. 배포 서버의 환경 변수를 나중에 바꾸어도 이미 만들어진 bundle은 바뀌지 않는다.

Next.js는 선택한 rendering 방식에 따라 달라진다. static export나 SSG는 build artifact 성격이 강하고, SSR은 요청 시 서버 런타임에서 값을 읽을 수 있다. 이 차이는 API base URL, 인증 Cookie, cache 정책에 직접 영향을 준다.

코드로 보는 핵심 예시

프론트 env는 공개 설정으로 다룬다.

const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
 
if (!apiBaseUrl) {
  throw new Error("VITE_API_BASE_URL is required");
}
 
function buildApiUrl(path: string) {
  if (/^https?:\/\//i.test(path)) {
    throw new Error("apiFetch path must be relative");
  }
 
  const baseUrl = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
  return new URL(path.replace(/^\/+/, ""), baseUrl);
}
 
export async function apiFetch(path: string, init?: RequestInit) {
  const url = buildApiUrl(path);
 
  const response = await fetch(url.toString(), {
    ...init,
    credentials: "include",
  });
 
  if (!response.ok) {
    throw new Error(`API request failed: ${response.status}`);
  }
 
  return response;
}

secret이 필요한 작업은 백엔드로 보낸다.

// 프론트는 결제 secret을 직접 들고 있지 않고 백엔드 endpoint만 호출한다.
await apiFetch("payments/checkout", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ orderId }),
});

환경 표를 미리 정리하면 배포 디버깅이 빨라진다.

| env | front origin | api origin | cookie domain | cors credentials |
| --- | --- | --- | --- | --- |
| dev | http://localhost:5173 | http://localhost:8080 | localhost | true |
| prod | https://app.example.com | https://api.example.com | .example.com | true |

백엔드와 맞춰야 할 지점

  • API origin과 CORS allowed origin을 환경별로 맞춘다.
  • Cookie Domain, SameSite, Secure, HttpOnly 설정을 배포 도메인 기준으로 확인한다.
  • 프론트 asset cache는 파일명 hash를 사용하고, HTML cache는 짧게 가져간다.
  • 사용자 에러 신고와 서버 로그를 연결할 traceId나 request id를 응답에 포함할지 정한다.
  • source map을 공개 배포할지, 에러 수집 도구에만 업로드할지 보안과 디버깅 기준을 정한다.

개인 프로젝트 기준

  • Vite SPA는 Netlify, Vercel, Cloudflare Pages 같은 정적 배포로 시작하기 쉽다.
  • SSR, SEO, 동적 metadata, 서버 route가 필요하면 Next.js 배포 모델을 별도로 학습한다.
  • production error 수집을 위해 Sentry를 붙이고, release version을 함께 기록한다.
  • 환경 변수는 .env.local, .env.production처럼 나누되 secret은 프론트에 두지 않는다.

실전 팁

  • 운영에서 예전 API URL을 호출하면 사용자의 JS bundle이 최신인지 확인한다.
  • VITE_API_BASE_URL/api/ 같은 path prefix가 들어가면 new URL("/payments", baseUrl)은 prefix를 버리고 host root로 이동한다. API client는 상대 path 규칙을 정하고 helper에서 한 번만 조합한다.
  • CDN purge, immutable asset, HTML no-cache 정책을 구분한다.
  • source map을 public에 둘지, Sentry에 업로드할지 정한다.
  • release version을 에러 이벤트에 함께 남기면 “어느 배포부터 깨졌는가”를 훨씬 빨리 찾을 수 있다.
  • 관련 함정은 env 변수가 런타임이 아니라 빌드 타임에 박히는 문제는 왜 중요할까?에서 다시 확인한다.

위험 신호!

  • VITE_SECRET_KEY 같은 이름으로 secret을 프론트에 둔다.
  • dev에서는 Cookie가 되는데 prod에서는 SameSite, Secure, domain 때문에 로그인 유지가 안 된다.
  • 배포 후 일부 사용자만 오래된 JS를 계속 받는다.
  • production error를 사용자가 캡처해 주기 전까지 알 방법이 없다.

확인 질문

확인 질문

  • Vite 정적 배포에서 env 값은 언제 결정되는가?
    • build 시점에 결정되어 bundle에 포함된다.
  • 브라우저 bundle에 넣으면 안 되는 값은 무엇인가?
    • API secret, private key, DB credential처럼 사용자에게 노출되면 안 되는 값이다.
  • HTML cache와 hashed JS asset cache는 왜 다르게 다루는가?
    • HTML은 최신 asset을 가리켜야 하므로 짧게 cache하고, hash가 붙은 asset은 길게 cache할 수 있다.

참고 문서