avatar
keyonnaise

React + Typescript 프로젝트에 SSR을 적용해 파이어베이스에 배포하기(1)

firebaseReactTypescriptSSRSEO
Aug 3
·
13 min read

지인들과 같이 사용할 목적으로 만든 블로그가 완성된 기념으로 블로그를 제작하며 겪었던 React와 TypeScript를 이용한 프로젝트를 Firebase에 배포하는 과정을 공유하고자 합니다. 특히 서버 사이드 렌더링(SSR) 적용과 Firebase 배포 경험을 중점적으로 다루겠습니다.

이 글은 이 곳 에서도 읽어 보실 수 있습니다.

Google에서 제공하는 Firebase는 편리한 사용성과 높은 생산성을 갖춘 모바일 및 웹 애플리케이션 개발 플랫폼입니다. 클라이언트 개발에 집중하고 싶었던 저에게 매력적인 선택지였습니다. 그러나 향후 다른 플랫폼으로의 이전 가능성을 고려하여 Firebase Client SDK 대신 Cloud Functions를 활용하여 API 서버를 구축하는 방식을 택했습니다.

시작하기

Firebase 프로젝트 생성

Google 계정으로 로그인 후 Firebase 콘솔(firebase.google.com)에 접속하여 다음과 같이 프로젝트를 설정합니다.

1. 프로젝트 생성: '프로젝트 만들기' 버튼을 클릭하여 새 프로젝트를 생성합니다.
2. Hosting 활성화: 생성된 프로젝트의 사이드바에서 '빌드' > 'Hosting'을 선택하여 Hosting 기능을 활성화합니다.
3. Blaze 요금제 선택: Cloud Functions 배포를 위해 Blaze 요금제를 선택하고 결제 수단을 등록합니다.

Vite를 이용한 React 프로젝트 생성

Firebase 프로젝트 설정을 완료했으므로, 이제 Vite를 사용하여 TypeScript 기반의 React 프로젝트를 생성해 보겠습니다. 터미널에서 다음 명령어를 실행합니다.

$ yarn create vite your-project --template react-ts

위 명령어 실행 후 안내에 따라 프로젝트 디렉토리로 이동하고, 기본 패키지를 설치한 후 개발 서버를 실행합니다.

$ cd your-project
$ yarn
$ yarn dev

이제 브라우저에서 http://localhost:5173/로 접속하면 Vite에서 기본 제공하는 간단한 React 애플리케이션을 확인할 수 있습니다.

Firebase 프로젝트와 연동하기

이제 Firebase CLI를 사용하여 Firebase 프로젝트와 연동하는 과정을 안내해 드리겠습니다.

1. Firebase CLI 설치

터미널에서 다음 명령어를 실행하여 Firebase CLI를 전역적으로 설치합니다.

$ npm install -g firebase-tools

2. Firebase에 로그인

설치된 Firebase CLI를 사용하여 Firebase 계정에 로그인합니다.

$ firebase login

로그인 명령을 실행하면 브라우저 창이 열리고, Google 계정으로 로그인하라는 메시지가 표시됩니다. 화면의 지시에 따라 로그인을 진행하고, Firebase CLI에 대한 액세스를 허용해주세요.

3. 프로젝트 목록 확인

로그인이 완료되면 다음 명령어를 실행하여 현재 연결된 Firebase 프로젝트 목록을 확인할 수 있습니디.

$ firebase projects:list

만약 프로젝트 목록이 정상적으로 출력된다면 Firebase CLI가 올바르게 설치되었고, 계정과 연결되었다는 의미입니다.

(선택사항) 연결된 Firebase 프로젝트를 변경하고 싶다면 firebase use <프로젝트 ID> 명령어를 이용해 변경할 수 있습니다.

4. 프로젝트 초기화

다음 명령어를 실행하여 프로젝트를 초기화합니다. 초기화 과정에서 사용할 Firebase 기능을 선택하고, 필요한 설정을 진행합니다.

$ firebase init
until-1177

Cloud Functions 를 사용해 SSR 구현하기

Firebase 설정이 완료되었으므로, 이제 Cloud Functions를 활용하여 서버 사이드 렌더링(SSR)을 구현해 보겠습니다. 이를 통해 SEO(검색 엔진 최적화)를 개선할 수 있습니다.

1. 필수 라이브러리 설치

루트 경로에 생성된 functions 디렉토리를 삭제하고 다음 명령어를 통해 SSR에 필요한 라이브러리를 설치합니다.

$ yarn add firebase-functions express cors compression isbot

- firebase-functions: Firebase Cloud Functions를 사용하기 위한 라이브러리입니다.
- express: Node.js 웹 애플리케이션 프레임워크입니다.
- cors: 다른 도메인에서 자원을 요청할 수 있도록 CORS(Cross-Origin Resource Sharing)를 설정합니다.
- compression: HTTP 응답 데이터를 압축하여 전송 시간을 단축시켜줍니다.
- isbot: 사용자 에이전트를 분석하여 봇 여부를 판단하는 데 사용됩니다.

2. Cloud Functions 함수 작성

프로젝트 루트 경로에 index.server.ts 파일을 생성하고 다음과 같이 코드를 작성합니다.

import compression from "compression";
import cors from "cors";
import express from "express";
import functions from "firebase-functions";
import { renderer } from "./src/entry-server";

const app = express();

const corsOptions = {
  origin: true,
  credentials: true,
};

app.use(cors(corsOptions));
app.use(compression());

app.get("*", renderer);

export const render = functions.https.onRequest(app);

src 디렉토리에 entry-client.tsx 파일을 생성하고 다음과 같이 코드를 작성합니다.

entry-client.tsx 파일은 클라이언트에서 React 애플리케이션을 렌더링하는 엔트리 포인트입니다. 기존의 root 요소가 비어 있으면 createRoot를 사용해 렌더링하고, 그렇지 않으면 hydrateRoot를 사용해 기존 콘텐츠를 재사용합니다.

// src/entry-client.tsx

import { StrictMode } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import App from '~app';

const root = document.getElementById('root')!;
const children = <App />;

if (root.childElementCount === 0) {
  createRoot(root).render(<StrictMode>{children}</StrictMode>);
} else {
  hydrateRoot(root, children);
}

다음으로 src/entry-server.tsx 파일을 생성하고 서버 사이드 렌더링을 위한 코드를 작성합니다.

이 코드는 Express 요청을 처리하여 React 애플리케이션을 서버에서 렌더링하고, 생성된 HTML을 클라이언트로 스트리밍합니다. isbot 라이브러리를 사용하여 사용자 에이전트가 봇인지 확인하고, 봇일 경우 완성된 HTML을 한 번에 전송합니다.

// src/entry-server.tsx

import { Transform } from "stream";
import { Request, Response } from "express";
import { renderToPipeableStream } from "react-dom/server";
import App from "./App";
import { isbot } from "isbot";

export async function renderer(req: Request, res: Response) {
  let isError = false;

  const stream = renderToPipeableStream(<App />, {
    bootstrapModules: ["/index.js"],

    onShellReady() {
      if (!isbot(req.get("user-agent"))) { // 브라우저일 경우 스트리밍 방식으로 HTML 전송
        res.statusCode = isError ? 500 : 200;

        res.setHeader("content-type", "text/html");
        stream.pipe(transformContent()).pipe(res);
      }
    },

    onShellError() {
      res.statusCode = 500;

      res.setHeader("content-type", "text/html");
      res.send("<h1>Something went wrong</h1>");
    },

    onAllReady() {
      if (isbot(req.get("user-agent"))) { // 봇일 경우 완성된 HTML을 한 번에 전송
        res.statusCode = isError ? 500 : 200;

        res.setHeader("content-type", "text/html");
        stream.pipe(transformContent()).pipe(res);
      }
    },

    onError(error) {
      isError = true;

      console.error(error);
    },
  });
}

function transformContent() {
  let content = "";

  const transform = new Transform({
    transform(chunk, _, callback) {
      content += chunk;

      callback();
    },

    flush(callback) {
      const data = `<!doctype html><html lang="en"><head><meta charset="UTF-8" /><link rel="icon" type="image/svg+xml" href="/vite.svg" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Vite + React + TS</title></head><body><div id="root">${content}</div></body></html>`;

      callback(null, data);
    },
  });

  return transform;
}

이 과정을 통해 서버 사이드 렌더링(SSR)을 구현할 수 있습니다. 이제 서버에서 렌더링된 HTML을 클라이언트에 전송하고, 클라이언트는 이를 재사용하여 빠르게 초기 로딩을 완료할 수 있습니다.

3. index.html 수정

src/main.tsx 파일을 삭제하고 index.html 파일을 수정하여 클라이언트 사이드 엔트리 포인트를 변경합니다.

// index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/entry-client.tsx"></script>
  </body>
</html>

4. 빌드 설정

Firebase에 배포를 위한 설정을 진행하겠습니다. 루트 경로의 vite.config.ts package.json firebase.json 파일을 다음과 같이 수정합니다.

// vite.config.ts

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

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        entryFileNames: "[name].js",
      },
    },
  },
});
// firebase.json

{
  "functions": [
    {
      "runtime": "nodejs20",
      "source": "./dist/server",
      "codebase": "default"
    }
  ],
  "hosting": {
    "public": "dist/client",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "**",
        "function": "render"
      }
    ]
  }
}
// package.json

{
// ...생략
  "main": "dist/server/index.server.js",
  "scripts": {
    "dev": "vite",
    "build:client": "tsc && vite build --outDir dist/client",
    "build:server": "tsc && vite build --outDir dist/server --ssr index.server.ts && cp ./package.json ./dist/server",
    "fb:serve": "yarn build:client && yarn build:server && rm -rf dist/client/index.html && firebase serve",
    "fb:deploy": "yarn build:client && yarn build:server && rm -rf dist/client/index.html && firebase deploy"
  }
}

package.json 파일 중 scripts 구문에 빌드 후 index.html 파일을 삭제하는 부분이 있습니다. index.html 파일을 삭제하는 이유는 Firebase 호스팅의 경우 루트 경로에 HTML 파일이 있으면 firebase.json 파일에서 설정한 rewrites 구문이 무시되고 무조건 HTML 파일을 읽게 됩니다. 우리는 SSR이 적용된 프로젝트를 Firebase에 배포하는 것이 목적이므로, 호스팅 루트 경로에 있는 HTML 파일은 삭제해주어야 합니다.

5. 배포

이제 모든 과정이 끝났습니다. 콘솔에 yarn fb:serve를 입력하고 개발 서버를 실행하면 SSR이 적용된 페이지를 볼 수 있습니다.

until-1174

로컬 환경에서 SSR이 적용된 것을 확인했으면 터미널에서 yarn fb:deploy를 입력합니다. 배포된 사이트에 접속하면 SSR이 적용된 페이지를 확인 할 수 있습니다.

배포된 결과물은 react-ts-with-ssr.web.app 에서 확인 할 수 있습니다.

until-1175

마무리

앞으로 Emotion JS를 사용해 SSR 환경에서 스타일을 주입하거나, react-helmet-async 라이브러리를 사용해 SEO 성능을 높이는 등의 활용이 가능합니다. 다음 글에서는 react-router-dom 라이브러리와 react-helmet-async 라이브러리를 활용해서 SEO 최적화하는 방법을 알아보겠습니다.

[ 다음글 ] React + Typescript 프로젝트에 SSR을 적용해 파이어베이스에 배포하기(2)


- 컬렉션 아티클






하하