hot-updater firebase 플러그인 배포

jingjinge·2025년 4월 21일
1

OpenSource

목록 보기
8/9
post-thumbnail

레포: https://github.com/gronxb/hot-updater

지난 글에 이어 최신화된 소식은 아래와 같다.

  1. 개발하던 hot-updater의 firebase plugin을 배포 시작했다.

    https://www.npmjs.com/package/@hot-updater/firebase

  2. hot-updater의 collaborater가 되었다.

  3. 현 시간(2025.04.21.12:30) 기준 stars가 662를 달성했다.

개발적인 측면, 오픈소서의 측면으로 과정을 풀어보고 얻어낸 결과를 좀 풀어볼까 한다.


과정

개발적인 측면

  1. 다양한 추상화 패턴들을 많이 익힐 수 있었다.

팩토리 패턴

  • 고차 함수(currying)을 활용해 관심사를 분리하면서도 효과적인 추상화 패턴을 구현할 수 있었다.

// 선언 및 정의
export function createFirebaseApp({
  region,
}: RegionOptions): (app: Hono<BlankEnv, BlankSchema, "/">) => HttpsFunction {
  return (app: Hono<BlankEnv, BlankSchema, "/">): HttpsFunction => {
    return onRequest(
      {
        region,
      },
      async (req, res) => {
        const host = req.hostname;
        const path = req.originalUrl || req.url;
        const fullUrl = new URL(path, `https://${host}`).toString();
        const request = new Request(fullUrl, {
          method: req.method,
          headers: req.headers as Record<string, string>,
          body:
            req.method !== "GET" && req.method !== "HEAD"
              ? req.body
              : undefined,
        });
        const honoResponse = await app.fetch(request);
        res.status(honoResponse.status);
        for (const [key, value] of honoResponse.headers.entries()) {
          res.setHeader(key, value);
        }
        const body: string = await honoResponse.text();
        res.send(body);
      },
    );
  };
}

// 사용

const hotUpdaterFunction = createFirebaseApp({
  region: HotUpdater.REGION,
})(app);

위와 같은 문법으로 Hono app instance에 대한 파라미터 입력을 따로 받음으로써, 다양한 app instance에도 대응 가능한 효과적인 문법을 구사했다.

어댑터 패턴

이전 글에서도 소개한 적이 있다.

현재 hot-updater에서는 supabase, cloudflare, firebase, aws 등 다양한 서버 인프라를 사용할 수 있게끔 지원하는데, 이 모든 것들은 각자의 사용법이 다 다르며, 세부 구현체가 전부 다 다를 것이다.

이를 사용할때마다 하나하나 바꾸는게 아닌, 공통된 파라미터와 공통된 return값으로 맞추어주는 어댑터를 중간에 하나 놓음으로써 유연성과 일관성을 동시에 가져갈 수 있었다.

import {metro} from '@hot-updater/metro';
import {supabaseDatabase, supabaseStorage} from '@hot-updater/supabase';
import {defineConfig} from 'hot-updater';
import 'dotenv/config';


/*defineConfig라는 어댑터를 통해, build, storage, database의 파라미터에 맞게끔 구현하고
세부 구현은 플러그인 마다 자유롭게 구현할 수 있다. input과 output만 맞춰주면 되는 것!
*/
export default defineConfig({
  build: metro({enableHermes: true}),
  storage: supabaseStorage({
    supabaseUrl: process.env.HOT_UPDATER_SUPABASE_URL!,
    supabaseAnonKey: process.env.HOT_UPDATER_SUPABASE_ANON_KEY!,
    bucketName: process.env.HOT_UPDATER_SUPABASE_BUCKET_NAME!,
  }),
  database: supabaseDatabase({
    supabaseUrl: process.env.HOT_UPDATER_SUPABASE_URL!,
    supabaseAnonKey: process.env.HOT_UPDATER_SUPABASE_ANON_KEY!,
  }),
});

test code

많은 사용자가 사용하게 될 서비스나, 오픈소스 같은 경우 안정성이 너무나도 중요하다, 나의 코드를 믿고 사용한 사용자들이 나의 사소한 실수로 치명적인 버그를 경험한다면, 그 결과로 초래되는 사용자의 금전적, 시간적 손해는 어떤 규모로 발생할지 예상할 수 없다.

hot-updater에서는 vitest를 활용하고 있는데, test code를 통해 개발하는 플러그인의 안정성 뿐만 아니라, 추상화함으로써 모든 플러그인의 요구사항 또한 정립할 수 있었다.

import { setupGetUpdateInfoTestSuite } from "@hot-updater/core/test-utils";


describe("getUpdateInfo", () => {
  const getUpdateInfo = createGetUpdateInfo(firestore);

  beforeEach(async () => {
    await clearCollections();
  });

  setupGetUpdateInfoTestSuite({
    getUpdateInfo,
  });
});

firebase

난 기존에 백엔드를 대체할 saas로 supabase를 많이 써 왔는데, 이번 플러그인을 개발하면서 firebase의 noSQL과 google 생태계를 처음 경험해봤다.

firebase는 사실상 google에 강하게 의존했다

플러그인 개발중 최근에 가장 많이 바뀐 부분은 gcloud를 사용했다는 것이다.

gcloud는 node package가 아닌, 외부에서 따로 설치를 해주어야하는데 사용자가 진행해야 하는 과정이 하나 더 생기는게 나는 탐탁지 않았었고, 이를 최대한 지양하고자 했다.

gcloud를 사용하지 않고, 처음에는 firebase/app을 사용하다가 firebase SDK를 활용한 firebase-admin을 활용하는 것으로 넘어갔는데 너무나도 많은 제약이 있었다.

  1. firebase는 gcloud를 좀 더 편하게 사용할 수 있게 하는 콘솔과 추상화된 메소드에 가까웠다.
    • IAM, functions, storage, db에 대한 직접적인 접근 없이, 만들어진 것을 사용하는 툴에 가까웠다.
      이는 firebase-tools(CLI)만으로는 많은 제약이 있었다.

    • 결국 gcloud를 사용자로 하여금 설치하게끔 유도했는데, 이 과정 하나로 고민하던 것들이 많이 해결 되었다.

      functions 배포 주소 가져오기(firebase functions v2로 migration을 가능하게 했다.)
      IAM 권한 체크, 주입

           
  2. noSQL의 장/단점은 뚜렷했다.
    • 스키마가 존재하지 않는다.
      • 그냥 document에 데이터를 넣으면 들어가고, 필요한걸 가져올 수 있다.
      • 이는 indexing을 강제했다.
        • noSQL은 어떤 것은 이 데이터를 가지고 있을 수도 있고, 없을 수도 있기에 데이터를 정렬 할 수 없다.
          • 이를 해결하기 위해 복합적인 search문에서는 indexing이 반강제적으로 필요하다
          • 결과적으로 현재 코드에서는 noSQL보다는 RDBMS가 훨씬 잘 맞는다는 생각이 들었다.(애초에 사용처가 다르긴 하다고는 알고 있고, 나 자신도 리서치가 더 필요하다.)

프로젝트 설정은 많은 것을 좌우한다.

플러그인의 package.json, tsconfig.json, tsup.config.ts 등을 봐보자

  1. package.json
{
  "name": "@hot-updater/firebase", // package이름
  "type": "module",
  "version": "0.16.3",
  "description": "React Native OTA solution for self-hosted",
  "main": "dist/index.cjs", // 진입점
  "types": "dist/index.d.ts",
  "module": "dist/index.js",
  "exports": { // 어떤 경로에 ESM, CJS를 사용하는지를 설정한다, 각 경로마다 다른 곳에 사용하기에 번들링 또한 다르게 해주어야한다. 
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./iac": {
      "types": "./dist/iac/index.d.ts",
      "import": "./dist/iac/index.js",
      "require": "./dist/iac/index.cjs"
    },
    "./functions": {
      "require": "./dist/firebase/index.cjs"
    }
  },
  "license": "MIT",
  "repository": "https://github.com/gronxb/hot-updater",
  "author": "gronxb <gron1gh1@gmail.com> (https://github.com/gronxb)",
  "bugs": {
    "url": "https://github.com/gronxb/hot-updater/issues"
  },
  "homepage": "https://github.com/gronxb/hot-updater#readme",
  "files": [
    "dist",
    "package.json"
  ],
  "scripts": {
    "build": "tsup",
    "test:type": "tsc --noEmit",
    "test": "vitest",
    "test:run": "vitest run"
  },
  "dependencies": {
    "@hot-updater/core": "0.16.3",
    "@hot-updater/plugin-core": "0.16.3",
    "firebase": "^11.3.1"
  },
  "publishConfig": {
    "access": "public"
  },
  "devDependencies": {
    "@clack/prompts": "^0.10.0",
    "@hot-updater/js": "0.16.3",
    "@types/node": "^22.13.5",
    "es-toolkit": "^1.32.0",
    "execa": "^9.5.2",
    "firebase-admin": "^13.2.0",
    "firebase-functions": "^6.3.2",
    "firebase-functions-test": "^3.4.0",
    "firebase-tools": "^13.32.0",
    "fkill": "^9.0.0",
    "hono": "^4.6.3",
    "mime": "^4.0.4",
    "picocolors": "^1.0.0"
  },
  "peerDependencies": { /* import하지 않지만, 이 package는 꼭 필요하다 명시, 
  init하는 과정에서 사용자 프로젝트에 실제로 설치하기에 dependencies로 가지 않고 이곳에 명시함 
  결과적으로 사용자의 프로젝트에 설치된 것을 사용함으로써, 충돌을 방지함*/
    "firebase-admin": "*"
  }
}
  1. tsconfig.json
{
  /* 
  공통된 tsconfig.json을 사용하기 위해 모노레포 가장 상위의
  tsconfig.base.json을 상속받음
  */
  "extends": "../../tsconfig.base.json",
    //ts 컴파일러가 처리해야할 디렉토리 명시
  "include": ["src", "firebase", "iac"],
    // 컴파일러가 처리하지 않을 곳 명시, 번들링 결과물이 나오는 곳이기에 할 필요가 없다.
  "exclude": ["dist"]
}
  1. tsup.config.json
    tsup은 ts로 구성된 파일들을 번들링 해주는 툴이다. 한 레포지토리에서 다른 곳에서 쓰이는 3가지의 ts를 번들링하기에 따로 설정해주어야 한다.
    package.json의 exports가 제대로 참조하기 위해서는 아래의 설정이 중요하다.
import { defineConfig } from "tsup";

export default defineConfig([
  {
    entry: ["./src/index.ts"],
    format: ["esm", "cjs"],
    outDir: "dist", //dist의 .
    dts: true,
  },
  {
    entry: ["firebase/functions/index.ts"],
    format: ["cjs"],
    publicDir: "firebase/public",
    outDir: "dist/firebase", // dist의 firebase
    external: ["firebase-functions", "firebase-admin"],
    noExternal: ["@hot-updater/core", "@hot-updater/js"],
  },
  {
    entry: ["iac/index.ts"],
    format: ["cjs", "esm"],
    dts: true,
    outDir: "dist/iac", // dist의 iac
    external: ["@hot-updater/firebase"],
    banner: {
      js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`,
    },
  },
]);

덜어냄의 미학

firebase/app을 사용하다가, firebase-admin을 통해 credential을 직접 사용하면서, 굉장히 많은 비효율적인 과정들이 전부 필요 없어지게 되었다.

처음에 hot-updater를 initialize 할 때 과정은 아래와 같았다.

  1. project 선택
  2. webapp 선택
  3. firebase storage rules 배포
  4. firebase db rules 배포, indexing 배포
  5. firebase functions 무조건 배포

SDK를 도입하고 나서는 SDK 자체에 프로젝트 설정값이 들어있고, credential을 통해 hot-updater에서 기존에 필요한 rules가 무의미 해졌다.

바뀐 결과는 아래와 같았다.

  1. credential 정보가 있는 credential 디렉토리 경로 입력
  2. db indexing 배포
  3. firebase functions 변한 것이 있는지 체크 후 배포

공 들인 코드들을 5분정도 들여다보고 다 지웠다. 남겨봤자 legacy가 될 것이 뻔했고 처음부터 구축하는게 맞다고 판단했다.

300줄 정도의 코드였지만, 핵심적인 로직들이 들어가 있었기에 무서웠다.

다시 코드를 작성하고 나니 깔끔한 로직으로 100줄가량의 코드들만 남아있었다.

기존 코드보다 정갈하고 깔끔한 역할을 하는 이쁜 코드들이었다.

내가 기존 코드가 아까워 지우지 못하고 활용하려고 했다면, 아마 더 추가됐을텐데 레거시라고 빠르게 판단하고 지우는 그 행위가 내 앞으로의 많은 시간을 아낄 수 있게끔 만들었다.


오픈소서의 측면

내가 좋아하는 것

나는 과거부터 현재까지 좋아하는 개발이 조금 뚜렷한 사람이다.

사람이 진행하는 많은 input을 최대한으로 줄이고, output을 늘리는 것을 좋아한다.

그럼 그 중간에 비는 input에 대한 처리는 내가 해야하는 것인데, 이것이 오픈 소스 라이브러리의 성격과 같다고 느꼈다. (이에 효과적인 인공지능은 왜 하지 않는가에 대해서는 따로 글을 발행할 예정이다.)

내가 처음 접했던 프로그래밍 언어인 C언어도 누군가 개발해서 배포한 것이고, 현재 대다수의 개발자들은 전부 온라인으로부터 얻어온 프로그래밍 언어를 사용한다.

나도 이것에 기여하고 싶었다.

책임감

해당 firebase plugin을 개발한 이유는 issue에서 원하는 개발자들이 보였기 때문이다.

개발이 거의 끝나간다고 생각했던게 대략 5주전인데, 배포는 3일전에 했다.

눈 앞에 설정한 데드라인보다 더 깊은 곳을 보아야만 했었다.

테스트 코드를 작성해서 안정성을 늘려야만 했고, 변화하는 기존 plugin의 spec에도 대응해야했다.

늦어지는 개발 일정에 기다리시는 분들에게 죄송했고, 죄송함에도 놓칠 수 없는 부분들을 전부 대응해야했기에 심적으로 힘들었던 것 같다.

코드 컨벤션 및 일관성

해당 오픈소스는 내 오픈소스가 아니다. 그에 맞는 컨벤션이 존재했다.

기존에 내 방식대로 코드를 작성할 수 없었고, 코드 스타일을 참고하기 위해 다른 플러그인 코드들을 많이 보았었고 이를 적용했다.

내 프로젝트가 아니기에 만드는 동시에 레거시인 코드를 작성할 수 없었다.

그래서 하나하나 신경써야만 했다. 코드 한 줄 한 줄 마다의 작성 근거가 필요했고, 이를 통해 클린 코드가 무엇인지를 찾아보며 클린코드 책도 구입해서 읽고 있다.


결론

개발은 개발대로 해야하고, 진행하던 프로젝트도 하고 있어서 글 발행을 한번에 해서 이거 써야지.. 써야지 하고 생각하던 것들을 많이 놓친 것 같긴 하다.

결과적으로 내가 만든 패키지를 코드 리뷰와 피드백을 통해 많이 수정하며 결국 배포에 성공했고, 아직까지는 firebase에 대한 별 다른 이슈가 올라오지는 않았다.

대쉬보드 역할을 하는 콘솔에 대한 리팩토링부터 시작이 plugin개발로 이어졌다.

결과적으로 hot-updater라는 오픈소스 라이브러리의 collaborater 되면서 개발자로서 가져야할 책임감이 늘어나게 되었다.

microsoft의 codepush에 대안이기에, 얼마나 많은 사람들이 사용하게 될지 모른다.

이러한 생태계에 기여할 수 있고, 다양한 사람들과 소통하며 유지보수 및 피쳐 개발을 이어가는게 개발자의 묘미 아닐까 생각한다.

1개의 댓글

comment-user-thumbnail
2025년 4월 30일

멋집니다

답글 달기