bandmate Firebase API Key노출 트러블 슈팅

Ethan·2026년 1월 20일

보안문제 Firebase API Key노출

🚨 문제 상황 (The Problem)

src 폴더 내의 파일들은 Vite가 빌드하면서 import.meta.env를 통해 환경변수를 착착 넣어주지만, public 폴더에 있는 정적 파일(Static Assets)들은 빌드 과정에서 코드 변환 없이 그대로 복사되어 .env를 사용할 수 없었습니다.

기존 코드 (public/firebase-messaging-sw.js):

//
firebase.initializeApp({
  apiKey: "AIzaSyC5L9JsYGjBmBT0BU_VRe9cnXLng4lg80s", 
  projectId: "bandmate2-f562f",
  // ...
});

이대로 깃허브에 올리면 악의적인 사용자가 내 할당량을 다 써버릴 수도 있다


💡솔루션

"Vite의 빌드 프로세스에 개입(Hooking)"
단순히 파일을 복사하게 두지 말고, Custom Plugin을 만들어 빌드가 끝나는 시점에 파일 내용을 바꿔치기하는 전략

1단계: SW 파일에 '구멍' 뚫기 🕳️

먼저 하드코딩된 키를 삭제하고, 나중에 채워 넣을 플레이스홀더(Placeholder)로 변경

수정된 public/firebase-messaging-sw.js:

firebase.initializeApp({
  apiKey: "%VITE_FIREBASE_API_KEY%", // 👈 여기에 구멍을 뚫었습니다.
  authDomain: "%VITE_FIREBASE_AUTH_DOMAIN%",
  projectId: "%VITE_FIREBASE_PROJECT_ID%",
  storageBucket: "%VITE_FIREBASE_STORAGE_BUCKET%",
  messagingSenderId: "%VITE_FIREBASE_MESSAGING_SENDER_ID%",
  appId: "%VITE_FIREBASE_APP_ID%"
});

2단계: Vite 플러그인 제작 (vite.config.ts) 🛠️

ReplaceEnvInSW 커스텀 플러그인을 사용했습니다.

import { defineConfig, loadEnv } from 'vite';
import fs from 'fs';
import path from 'path';

export default defineConfig(({ mode }) => {
  // 1. 현재 모드(development/production)에 맞는 .env 파일을 로드합니다.
  const env = loadEnv(mode, process.cwd(), '');

  return {
    plugins: [
      react(),
      VitePWA({ ... }),
      
      // Magic Plugin ✨
      {
        name: 'replace-env-in-sw',
        // closeBundle: 빌드가 완료되고 파일이 dist에 쓰여진 후 실행되는 훅
        closeBundle() {
          const swPath = path.resolve(__dirname, 'dist/firebase-messaging-sw.js');
          
          if (fs.existsSync(swPath)) {
            let swContent = fs.readFileSync(swPath, 'utf-8');
            
            // 2. .env에서 로드한 값으로 플레이스홀더를 교체합니다.
            swContent = swContent.replace(/%VITE_FIREBASE_API_KEY%/g, env.VITE_FIREBASE_API_KEY);
            swContent = swContent.replace(/%VITE_FIREBASE_AUTH_DOMAIN%/g, env.VITE_FIREBASE_AUTH_DOMAIN);
            swContent = swContent.replace(/%VITE_FIREBASE_PROJECT_ID%/g, env.VITE_FIREBASE_PROJECT_ID);
            swContent = 0swContent.replace(/%VITE_FIREBASE_STORAGE_BUCKET%/g, env.VITE_FIREBASE_STORAGE_BUCKET);
            swContent = swContent.replace(/%VITE_FIREBASE_MESSAGING_SENDER_ID%/g, env.VITE_FIREBASE_MESSAGING_SENDER_ID);
            swContent = swContent.replace(/%VITE_FIREBASE_APP_ID%/g, env.VITE_FIREBASE_APP_ID);

            fs.writeFileSync(swPath, swContent);
            console.log('🔒 Firebase SW 환경변수 주입 완료!');
          }
        }
      }
    ]
  };
});

📝 구현 원리 (How it works)

  1. loadEnv: Vite가 제공하는 유틸리티로, .env 파일을 읽어 객체 형태로 가져옵니다.
  2. closeBundle Hook: Vite의 빌드 과정 중 "모든 번들링이 끝나고 파일이 생성된 직후"에 실행됩니다. 이때가 dist 폴더에 파일이 존재하는 시점입니다.
  3. fs (File System): Node.js의 파일 시스템 모듈을 이용해 dist/firebase-messaging-sw.js를 물리적으로 읽어서 내용을 수정하고 다시 저장합니다.

🎓 결론

이제 우리는 .env 파일에만 키를 보관하면 됩니다.
빌드할 때마다 Vite가 알아서 보안 처리된 Service Worker를 생성해줍니다

주의: 이 방법은 build 명령어를 실행할 때 적용됩니다. 로컬 개발 서버(dev)에서는 public 폴더가 메모리 상에서 서빙되므로, 개발 환경을 위한 별도의 미들웨어를 추가하거나 로컬 테스트 시에는 dist를 프리뷰하는 것이 좋습니다.

보안은 귀찮지만, 털리는 것보단 낫습니다 안전한 코딩 하세요! 🛡️

profile
코딩하는 알파카

0개의 댓글