Remix 프로젝트를 Cloudflare Pages로 배포하기

Remix 프로젝트를 Cloudflare Pages로 배포할 수 있도록 리팩토링합니다.

·

3 min read

리팩토링 계기

지금 이력서와 포트폴리오를 마크다운 파일로 작성하여 Remix 프레임워크를 통해 개인 클라우드에서 배포하고 있다.

Remix를 채택한 이유는 다음과 같았다.

  • @mdx-js/rollup 패키지를 통해 마크다운 파일을 프로젝트 빌드 타임에 컴파일하여 사용자들에게 제공할 수 있다.

    • 마크다운 파일은 app/routes 디렉터리에 작성되어야 한다.
  • Remix에서 제공하는 라우팅 이름 컨벤션을 통해 레이아웃을 유연하게 구성할 수 있다.

    • mdx.tsx에 레이아웃을 작성하면 mdx로 시작하는 모든 파일이 공유할 수 있다. 프로젝트에 mdx/_index.tsx 파일을 생성하면 웹 브라우저에서는 /mdx로 접속할 수 있다.

    • 서브 경로가 아닌 인덱스 페이지로 접속하게 하려면 레이아웃 파일 이름 앞에 _을 붙여 _mdx/_index.tsx 등으로 작성해야 한다.

    • _mdx/resume.mdx에 이력서를 작성하여 /resume으로 접속할 수 있게 했다.

Cloudflare Pages를 사용하는 이유

  • 깃허브 저장소와 연결해서 브랜치가 업데이트될 때마다 자동으로 배포할 수 있다.

  • 개인 클라우드의 부담을 조금이나마 줄이고 싶었다.

기존 이력서가 @remix-run/node 기반으로 구성되어 있어서 클라우드플레어 Pages에서 배포될 수 있게 하려면 다음과 같은 작업이 필요했다.

공식문서에 프로젝트를 리팩토링하는 방법이 없었기 때문에 템플릿으로 생성된 프로젝트를 참고했다. 템플릿 명령어는 다음과 같다.

  • npx create-remix@latest --template remix-run/remix/templates/cloudflare

어댑터 설치 & 적용

서버의 Request, Response 객체를 Fetch API라 호환될 수 있도록 @remix-run/cloudflare 서버 어댑터와 필요한 추가 패키지를 설치한다.

wrangler는 로컬에서 개발 서버를 작동시키기 위해 설치한다.

기존에 설치된 @remix-run/node 패키지는 제거한다.

- @remix-run/cloudflare-pages
- @cloudflare/workers-types
- wrangler

wrangler.toml 파일을 프로젝트 폴더 최상위 위치에 작성한다. 빌드 후 배포 환경에서의 엔트리 파일의 경로를 명시해야 한다.

# Cloudflare pages requires a top level name attribute
name = "resume"

# Cloudflare Pages will ignore wrangler.toml without this line
pages_build_output_dir = "./build/client"

# Fixes "no such module 'node:events'"
compatibility_flags = [ "nodejs_compat" ]

# Fixes "compatibility_flags cannot be specified without a compatibility_date"
compatibility_date = "2024-04-18"

Cloudflare Pages의 환경에서는 node:fs와 같은 Node.js의 몇몇 모듈은 사용할 수 없다.

tsconfig.json에 특정 속성을 다음과 같이 변경한다.

  • ts-node를 사용하는 경우 타입스크립트 파일을 실행시키기 위한 전용 타입을 지정해줘야 한다.
{
  "compilerOptions": {
    "types": ["@remix-run/cloudflare", "vite/client"]
  },
  "ts-node": {
    "types": ["node"]
  }
}

타입 에러를 방지하고 Cloudflare Pages 환경에서 정상적으로 배포될 수 있도록 다음과 같은 파일을 작성해야 한다.

  • load-context.ts

  • functions/[[path]].ts

    • 사용자가 URL로 접속했을 때 요청을 처리하는 역할을 한다.
// load-context.ts
import { type PlatformProxy } from "wrangler";

interface Env {}

type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;

declare module "@remix-run/cloudflare" {
  interface AppLoadContext {
    cloudflare: Cloudflare;
  }
}
// function/[[path]].ts
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - the server build file is generated by `remix vite:build`
// eslint-disable-next-line import/no-unresolved
import * as build from "../build/server";

export const onRequest = createPagesFunctionHandler({ build });

개발 환경에서 클라우드플레어 Pages의 환경을 재현(시뮬레이션)할 수 있는 플러그인을 적용시킨다.

import {
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";

export default defineConfig(() => {
  // ...
  return {
    plugins: [
      remixCloudflareDevProxy(),
      remix({
        ignoredRouteFiles: ["**/*.css"],
      }),
      tsconfigPaths(),
    ],
    // ...
  };
});

캐시 설정

클라우드플레어에 배포 후 접속했을 때 웹 페이지가 캐시가 되지 않는 문제가 있었다. 네트워크 응답 헤더에서 Cf-Cache-Status 속성을 통해 캐시 여부를 확인할 수 있다.

  • DYNAMIC: 캐시되지 않음

  • MISS: 클라우드플레어의 캐시에 없어서 원본 데이터로 응답

  • HIT: 캐시된 데이터를 응답

배포 직후에 확인한 캐시 속성은 DYNAMIC으로 나타나 추가적인 캐시설정을 해줬다.

Remix 프로젝트에서 응답 헤더에 캐시 속성을 추가한다.

// entry-server.tsx

import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server";

export default async function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,

  loadContext: AppLoadContext
) {
  const body = await renderToReadableStream(
    <RemixServer context={remixContext} url={request.url} />,
    {
      signal: request.signal,
      onError(error: unknown) {
        responseStatusCode = 500;
      },
    }
  );

  responseHeaders.set("content-type", "text/html");

  // 캐시 컨트롤
  responseHeaders.set(
    "cache-control",
    "public, max-age=604800, s-max-age=604800, must-revalidate"
  );

  if (isbot(request.headers.get("user-agent") || "")) {
    await body.allReady;
  }

  responseHeaders.set("Content-Type", "text/html");
  return new Response(body, {
    headers: responseHeaders,
    status: responseStatusCode,
  });
}

그리고 클라우드플레어 대시보드에서 캐시 규칙을 추가하면 된다.

  1. 클라우드플레어 대시보드에서 배포한 웹 사이트 선택

  2. 좌측 사이드바에서 Caching 그리고 Cache Rules 항목 선택

  3. Create Rules로 캐시할 페이지의 캐시 규칙을 작성 후 생성하면 된다.