Skip to main content

Command Palette

Search for a command to run...

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

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

Updated
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로 캐시할 페이지의 캐시 규칙을 작성 후 생성하면 된다.

More from this blog

AI 에이전트에서 작업 맥락을 관리하기 위해 사용한 방법

AI 에이전트의 불편함 Codex CLI와 같은 AI 코딩 에이전트를 통해서 프로젝트 개발 속도를 높일 수 있다. 그리고 혼자 작업했을 때는 놓칠 수 있는 부분도 발생할 수 있는데 AI 에이전트를 통해 보완할 수 있었다. 컴포넌트에 대해서 접근성 태그 작성 API 호출하는 비동기 코드 작성할 때 에러 처리 그리고 AI 에이전트를 잘 활용하면 불가능하

Mar 8, 202610 min read

지시사항을 분할하여 Codex로 효율적인 작업하기

이슈 Codex와 같은 AI 기반 코드 에이전트는 출력 토큰의 수가 제한되어 있다. 그래서 다양한 작업을 하나의 프롬프트에 전부 몰아서 작성하면 원하는 결과가 나오지 않을 수 있다. Codex와 같은 LLM 기반 서비스는 출력 토큰 수를 초과하게 되면 일부 단계가 누락되거나 특정 요소를 과하게 요약할 수 있고 이는 전체적인 답변 퀄리티를 낮출 수 있기 때문이다. 그래서 다양한 AI 공식문서는 특정 작업을 더 작은 단위로 분할하는 방식을 강조한다...

Nov 3, 20255 min read

Key를 활용하여 컴포넌트 상태를 초기화하기

개요 컴포넌트의 상태가 업데이트되어도 컴포넌트 내부에 배치된 컴포넌트의 상태는 그대로 유지되는 경우가 있었다. 예전에는 Props 전달 및 내부에서 useEffect를 통해서 상태를 초기 상태로 업데이트하도록 했는데 useEffect를 남용하게 되면 상태 변화 추적이 어려워지는 문제가 있었다. 그런데 useEffect 외에도 Key를 통해서 컴포넌트 상태를 초기화할 수 있다는 사실을 알게 되었다. 리스트 렌더링 React 프론트엔드에서 목록 형...

Sep 21, 20253 min read

내가 AGENTS.md를 작성하는 방법

Codex CLI 요즘 프로젝트 개발에 Codex CLI를 활용하는 이유는 다음과 같다. 특정 프로젝트를 작업할 때 Claude Code는 종종 사용량을 초과한 것과 다르게 Codex CLI는 ChatGPT Plus 요금제만으로도 토큰 사용량 초과 걱정 없이 충분하게 활용할 수 있었다. 타 CLI 기반 AI 개발 도구에 비해서 요구사항을 더 정확하게 구현하고 꼭 필요한 작업만 진행하기 때문에 코드를 검토하는 시간을 줄일 수 있다. 반복적인 코드...

Sep 21, 20254 min read
N

Nowon Lee

22 posts