Remix 프로젝트를 Cloudflare Pages로 배포하기
Remix 프로젝트를 Cloudflare Pages로 배포할 수 있도록 리팩토링합니다.
Table of contents
리팩토링 계기
지금 이력서와 포트폴리오를 마크다운 파일로 작성하여 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,
});
}
그리고 클라우드플레어 대시보드에서 캐시 규칙을 추가하면 된다.
클라우드플레어 대시보드에서 배포한 웹 사이트 선택
좌측 사이드바에서
Caching
그리고Cache Rules
항목 선택Create Rules
로 캐시할 페이지의 캐시 규칙을 작성 후 생성하면 된다.