FCM Service Worker + Vite

Jay·2024년 5월 7일
1
post-thumbnail

최근 FCM과 vite를 사용한 react 프로젝트에서 알림 기능을 구현하며 고민했던 내용의 비슷한 예시와 문서를 찾지 못해 진행한 삽질을 기록해 보려 합니다.

목표

API key이다 보니 파일에 하드 코딩이 아닌 환경 변수를 활용하여 노출되지 않으며 개발 모드와 빌드 모드에서 service worker가 정상적으로 설치되고 동작하도록 하는 것 이 목표입니다.

API key이지만 꼭 환경 변수를 사용하며 숨겨야 하는 것은 아닙니다.
firebase공식 문서

문제

FCM은 root 경로에서 firebase-messaging-sw.js를 찾기 때문에 자연스럽게 public 폴더에 service worker를 배치하게 되었습니다.

하지만 public 경로에서는 import.meta.env를 사용할 수 없었습니다.

그렇다고 src 경로에 놓으면 FCM은 root 경로에서 firebase-messaging-sw.js를 찾기 때문에 404에러와 함께 service worker를 찾지 못합니다.

첫 번째 시도

// /public/firebase-messaging-sw.js

if (typeof importScripts === "function") {
  importScripts("https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js");
  importScripts(
    "https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js"
  );

  const firebaseApp = firebase.initializeApp({
    apiKey: import.meta.env.VITE_FCM_API_KEY,
    authDomain: import.meta.env.VITE_FCM_AUTH_DOMAIN,
    projectId: import.meta.env.VITE_FCM_PROJECT_ID,
    storageBucket: import.meta.env.VITE_FCM_STORAGE_BUCKET,
    messagingSenderId: import.meta.env.VITE_FCM_MESSAGING_SENDER_ID,
    appId: import.meta.env.VITE_FCM_APP_ID,
    measurementId: import.meta.env.VITE_FCM_MEASUREMENT_ID,
  });

  firebase.messaging(firebaseApp);

  self.addEventListener("install", () => {
    // eslint-disable-next-line no-console
    console.log("installed SW!");
  });
}

우선 firebase-messaging-sw.js를 public 내부에 배치했습니다.

그러고 dotenv 패키지와 정규식을 활용하여 import.meta.env.~ 문자열 내용을 환경 변수 내용으로 변경하는 custom plugin을 구현했습니다.

// config/vitePlugins.ts

import "dotenv/config";

export function fcmSwEnvPlugin() {
  return {
    name: "rollup-plugin-fcm-sw-env",
    transform(code: string, id: string) {
      if (id.endsWith("/firebase-messaging-sw.js")) {
        // Replace process.env variables with their actual values
        return code.replace(
          new RegExp(`process.env.(\\w+)`, "g"),
          (_, varName) => `"${process.env[varName]}"`
        );
      }
      return null;
    },
  };
}

firebase-messaging-sw.js가 root(public)에 있으며 파일 자체를 수정하여 환경 변수를 직접 주입했기 때문에 개발 모드에서 정상적으로 동작했습니다.

하지만 파일을 직접 수정했기 때문에 매번 github에 푸시 하기 전에 되돌리기를 하며 항상 신경 써야 했습니다.

또한 빌드 모드에서는 public에 있는 파일을 custom plugin으로 조작이 불가능했습니다.
그래서 firebase-messaging-sw.js에는 환경 변수가 적용되지 않고 import.meta.env.~와 같은 문자열이 남아 있습니다.

개발 모드 : 성공
빌드 모드 : 실패

두 번째 시도

빌드 시 public에 있는 파일을 조작할 수 없기 때문에 custom plugin은 사용할 수 없으며 firebase-messaging-sw.jssrc 경로로 옮기기로 했습니다.

// src/firebase-messaging-sw.js

if (typeof importScripts === "function") {
  importScripts("https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js");
  importScripts(
    "https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js"
  );
  
  const firebaseApp = firebase.initializeApp({
    apiKey: import.meta.env.VITE_FCM_API_KEY,
    authDomain: import.meta.env.VITE_FCM_AUTH_DOMAIN,
    projectId: import.meta.env.VITE_FCM_PROJECT_ID,
    storageBucket: import.meta.env.VITE_FCM_STORAGE_BUCKET,
    messagingSenderId: import.meta.env.VITE_FCM_MESSAGING_SENDER_ID,
    appId: import.meta.env.VITE_FCM_APP_ID,
    measurementId: import.meta.env.VITE_FCM_MEASUREMENT_ID,
  });
  
  firebase.messaging(firebaseApp);

  self.addEventListener("install", () => {
    // eslint-disable-next-line no-console
    console.log("installed SW!");
  });
}

이렇게 진행하게 된다면 개발과 빌드 모드 모두 firebase-messaging-sw.js의 경로 문제를 가지게 됩니다.

개발 모드의 경우 firebase-messaging-sw.jssrc 내부에 있게 되고 빌드 모드의 경우 dist의 root 경로에 output 되지 않고 메인 애플리케이션 코드와 같이 번들링 됩니다.

그렇기 때문에 모두 다 firebase-messaging-sw.js를 찾지 못하게 됩니다.

// vite.config.ts

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  build: {
    target: "es2022",
    rollupOptions: {
      input: {
        "main": "./index.html",
        "firebase-messaging-sw": "./src/firebase-messaging-sw.js",
      },
      output: {
        entryFileNames: (chunkInfo) => {
          return chunkInfo.name === "firebase-messaging-sw"
            ? "[name].js" // Output service worker in root
            : "assets/[name]-[hash].js"; // Others in `assets/`
        },
      },
    },
  },
});

그래도 rollupOptions을 활용하여 메인 애플리케이션과 service worker 파일을 구분하여 두 개의 entry point와 output을 설정 가능했습니다.

개발 모드에 문제도 해결해 보기 위해서 Vite의 server.proxy를 활용해 요청 경로를 변경했지만 다음과 같은 에러가 발생하여 실패했습니다.

GET /firebase-messaging-sw.js /src/firebase-messaging-sw.jsUncaught SyntaxError: Cannot use 'import.meta' outside a module (at firebase-messaging-sw.js:1:8)

개발 모드 : 실패
빌드 모드 : 성공

해결

첫 번째와 두 번째 시도에서 사용한 방법을 함께 적용하여 문제를 해결했습니다.

빌드 모드의 경우 두 번째 시도에서 성공했기 때문에 위 방법을 그대로 유지합니다.

개발 모드의 경우 fs와 정규식을 활용하여 import.meta.env.~ 문자열을 환경 변수로 변경하여 개발 모드에서만 사용하게 될 /public/firebase-messaging-sw.js 파일을 직접 생성하는 custom plugin을 구현합니다. 이렇게 진행하면 service worker 파일이 매번 생기기 때문에 .gitignore/public/firebase-messaging-sw.js를 추가합니다.

최종 코드

service worker

// src/firebase-messaging-sw.js

if (typeof importScripts === "function") {
  importScripts("https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js");
  importScripts(
    "https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js"
  );

  const firebaseApp = firebase.initializeApp({
    apiKey: import.meta.env.VITE_FCM_API_KEY,
    authDomain: import.meta.env.VITE_FCM_AUTH_DOMAIN,
    projectId: import.meta.env.VITE_FCM_PROJECT_ID,
    storageBucket: import.meta.env.VITE_FCM_STORAGE_BUCKET,
    messagingSenderId: import.meta.env.VITE_FCM_MESSAGING_SENDER_ID,
    appId: import.meta.env.VITE_FCM_APP_ID,
    measurementId: import.meta.env.VITE_FCM_MEASUREMENT_ID,
  });

  firebase.messaging(firebaseApp);

  self.addEventListener("install", () => {
    console.log("installed SW!");
  });
}

custom plugin

// config/vitePlugins.ts

import "dotenv/config";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

export function fcmSwEnvPlugin() {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  const srcDir = path.resolve(__dirname, "../src");
  const fcmSwCode = fs.readFileSync(
    `${srcDir}/firebase-messaging-sw.js`,
    "utf8"
  );

  const transformedCode = fcmSwCode.replace(
    new RegExp(`import.meta.env.(\\w+)`, "g"),
    (_, varName) => `"${process.env[varName]}"`
  );
  const finalCode =
    "// IMPORTANT: This file only exists for dev mode purposes. Do not modify this file. Any changes should be made in `src/firebase-messaging-sw.js`.\n\n" +
    transformedCode;

  const outputPath = path.resolve("public", "./firebase-messaging-sw.js");
  fs.writeFileSync(outputPath, finalCode);

  return {
    name: "rollup-plugin-fcm-sw-env",
  };
}

custom plugin에서 fs가 아닌 rollup에서 파일을 읽어 생성하는 방법도 시도해봤지만 프로젝트 내에서 firebase-messaging-sw.js는 어디에서도 import 하고 있지 않기 때문에 tree-shaking에서 제외됩니다. 그렇기 때문에 rollup에서는 service worker를 불필요한 import를 진행하지 않는 이상 파일 찾지 못하기 때문에 조작이 불가능합니다.

vite.config

// vite.config.ts

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { fcmSwEnvPlugin } from "./config/vitePlugins";

export default defineConfig(({ command }) => {
  if (command === "serve") {
    return {
      plugins: [react(), tsconfigPaths(), fcmSwEnvPlugin()],
    };
  } else {
    // command === 'build'
    return {
      plugins: [react(), tsconfigPaths()],
      build: {
        target: "es2022",
        rollupOptions: {
          input: {
            "main": "./index.html",
            "firebase-messaging-sw": "./src/firebase-messaging-sw.js",
          },
          output: {
            entryFileNames: (chunkInfo) => {
              return chunkInfo.name === "firebase-messaging-sw"
                ? "[name].js" // Output service worker in root
                : "assets/[name]-[hash].js"; // Others in `assets/`
            },
          },
        },
      },
    };
  }
});

gitignore

// .gitignore

# FCM (this file is only used for dev mode purposes)
public/firebase-messaging-sw.js

마무리하며

사실 FCM에서도 꼭 API key를 숨겨놓을 필요가 없다고 했기 때문에 firebase-messaging-sw.js직접 API key를 작성해 놓으면 지금까지의 모든 과정이 필요 없이 쉽게 해결할 수 있는 문제입니다.

하지만 API key라고 하니 숨기고 싶은 알 수 없는 욕구와 구글링을 진행해도 저희와 같은 고민을 하고 있는 글이 보이지 않으며 함께 프로젝트를 진행하는 팀원도 같은 생각을 하고 있어 직접 도전해 본 케이스입니다.

profile
병아리 개발자

0개의 댓글