Cloudflare R2를 사용하여 이미지 업로드 기능을 구현하는 방법을 알아봅니다.
클라우드 플레어 R2는 S3 호환 개체 스토리지와 뛰어난 성능을 제공하는 오브젝트 스토리지 서비스입니다. 이 글에서는 클라우드 플레어 R2에 이미지를 업로드하는 기능을 구현하는 방법을 단계별로 설명합니다.
먼저 클라우드 플레어 로그인 후 R2 서비스를 활성화해야 합니다.
`클라우드 플레어 대시보드`로그인 후 대시보드 좌측 메뉴에서 스토리지 및 데이터베이스 > R2 Object Storage를 선택합니다.

클라우드 플레어 신용카드 정보 입력 화면
프로젝트에서 Wrangler를 사용하여 클라우드 플레어 워커를 배포할 수 있습니다. 다음 명령어로 Wrangler를 설치합니다.
bun add -D wrangler@latest # 또는 pnpm, npm 사용 가능설치가 완료되면 다음 명령어로 클라우드 플레어에 로그인합니다.
wrangler login위 명령어를 입력하면 브라우저가 열리고 클라우드 플레어 계정으로 로그인할 수 있습니다.
R2 버킷을 생성하려면 클라우드 플레어 대시보드에서 R2 Object Storage로 이동한 후 Create Bucket 버튼을 클릭합니다. 버킷 이름을 입력하고 생성합니다.
wrangler 로그인 후 다음 명령어를 입력하면 생성된 버킷을 확인할 수 있습니다.
bunx wrangler r2 bucket list # 또는 npx wrangler r2 bucket list
클라우드 플레어 R2 버킷 생성 화면
이미지 업로드 및 처리를 위해 aws-sdk의 S3 클라이언트와 sharp 패키지를 설치합니다.
bun add @aws-sdk/client-s3 sharp@aws-sdk/client-s3: AWS SDK의 S3 클라이언트로, 클라우드 플레어 R2와 호환됩니다.sharp: 이미지 리사이징 및 포맷 변환을 위한 고성능 이미지 처리 라이브러리입니다.클라우드 플레어 R2에 접근하기 위해 필요한 환경 변수를 설정합니다. 클라우드 플레어 대시보드에서 R2 Object Storage > API Keys로 이동하여 Access Key ID와 Secret Access Key를 생성합니다.
보이지 않는다면 우측 하단의 Account Details의 Manage 버튼을 클릭하여 확인할 수 있습니다.
Access Api 토큰 생성과 User Api 토큰 생성 두가지가 있는데
개발 환경에서는 User Api 토큰 생성을 선택하는 것을 권장합니다.
필요한 권한 및 옵션을 선택하고 토큰을 생성한 후, 다음과 같은 환경 변수를 설정합니다.
.env 파일을 생성하고 다음과 같이 환경 변수를 추가합니다.
R2_ACCESS_KEY_ID=your_access_key_id
R2_SECRET_ACCESS_KEY=your_secret_access_key
R2_BUCKET_NAME=your_bucket_name
R2_ACCOUNT_ID=your_account_id필요시에 envConfig에 다음과 같이 추가합니다.
저는 zod를 사용하여 환경 변수를 검증하고 있습니다. 프로젝트에 맞게 수정하세요.
import { z } from "zod";
const envSchema = z.object({
R2_ACCESS_KEY_ID: z.string().min(1),
R2_SECRET_ACCESS_KEY: z.string().min(1),
R2_BUCKET_NAME: z.string().min(1),
R2_ACCOUNT_ID: z.string().min(1),
// 기타 환경 변수들...
});
const parsed = envConfigSchema.safeParse(process.env);
if (!parsed.success) {
console.error("❌ Invalid environment variables:", parsed.error.issues);
process.exit(1); // 잘못된 경우 서버 실행 중단
}
export const envConfig = parsed.data;이제 S3 클라이언트를 설정합니다. src/lib/r2Client.ts 파일을 생성하고 다음 코드를 추가합니다.
import { S3Client } from "@aws-sdk/client-s3";
import { envConfig } from "@/config/env";
export const r2Client = new S3Client({
region: "auto", // R2는 region 개념 없음
endpoint: `https://${envConfig.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: envConfig.R2_ACCESS_KEY_ID,
secretAccessKey: envConfig.R2_SECRET_ACCESS_KEY,
},
});이미지를 R2 버킷에 업로드하는 함수를 구현합니다. src/lib/uploadImage.ts 파일을 생성하고 다음 코드를 추가합니다.
저는 Hono.js 프레임워크를 사용하고 있지만, 다른 프레임워크에서도 비슷한 방식으로 구현할 수 있습니다.
설명을 돕기위해
uploadController에 직접 구현하였습니다. 필요에따라 분리하세요.
import { Context } from "hono";
import sharp from "sharp";
import { r2Client } from "@/libs/r2Client.ts";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { envConfig } from "@/config/env.ts";
export const uploadController = async (c: Context) => {
const formData = await c.req.formData();
const file = formData.get("file") as File | null;
const originalName = file?.name || "unknown";
const folder = "uploads";
const mimeType = file?.type;
if (!file) {
return c.json({ message: "파일이 없습니다." }, 400);
}
if (!mimeType?.startsWith("image/")) {
return c.json({ message: "이미지 파일만 업로드할 수 있습니다." }, 400);
}
// 파일 이름과 확장자 설정
const ext = originalName.split(".").pop();
const filename = `${Date.now()}.${ext}`;
const key = `${folder}/${filename}`;
// file을 Buffer로 변환
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// sharp를 사용하여 이미지 리사이징
// 이 부분은 필요에 따라 조정하세요
// 원본이 필요하다면 이 부분을 제거하세요
const resized = await sharp(buffer)
.resize({ width: 1200, withoutEnlargement: true }) // 최대 너비 1200px, 확대 방지
.toBuffer(); // format 지정 안 하므로 원본 포맷 유지
// R2에 업로드할 키 생성
await r2Client.send(
new PutObjectCommand({
Bucket: envConfig.R2_BUCKET_NAME,
Key: key,
Body: resized,
ContentType: mimeType, // 클라이언트에서 받은 타입 그대로
}),
);
const url = `https://${envConfig.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${envConfig.R2_BUCKET_NAME}/${key}`;
// 이미지 업로드 후 URL 반환
console.log("Uploaded image URL:", url);
// db에 메타정보 저장하는 로직 추가 가능
// const [image] = await db
// .insert(imagesTable)
// .values({
// folder,
// filename,
// url,
// refId,
// })
// .returning();
return c.json({ url });
};이제 클라우드 플레어 R2에 이미지를 업로드할 수 있는 기능이 구현되었습니다.
생성된 이미지는 기본적으로 비공개(private) 입니다. 생성된 url로 브라우저에서 접근하면 Authorization 오류로 접근이 불가능합니다.
공개(public)로 설정하려면 클라우드 플레어 대시보드에서 R2 버킷의 설정에서 사용자 설정 도메인을 통해 공개 설정을 해야합니다.
사용자 설정 도메인을 설정하기 위해 도메인을 소유하고 있어야 하며, 도메인에 대한 DNS 설정도 필요합니다.
우선 설정 방법은 생성한 버킷의 Settings 탭에서 사용자 설정 도메인을 추가하면 됩니다.

클라우드 플레어 R2 사용자 설정 도메인 추가 화면

클라우드 플레어 R2 사용자 설정 도메인 추가 화면2
사용자 도메인 설정이 완료되면 업로드된 이미지에 접근할 수 있습니다.
사용자 도메인 설정을 완료 했다는 가정하에 업로드된 이미지 URL은 다음과 같은 형식이 됩니다.
https://<your-custom-domain.com>/uploads/your-image-file-name.jpg서버에서 이미지를 공개 url로 내려주기 위해 위 코드를 다음과 같이 수정할 수 있습니다.
// ... 생략
await r2Client.send(
new PutObjectCommand({
Bucket: envConfig.R2_BUCKET_NAME,
Key: key,
Body: resized,
ContentType: mimeType, // 클라이언트에서 받은 타입 그대로
}),
);
const url = `https://<your-custom-domain.com>/${key}`; // 도메인은 차후 생성후 env 변수로 관리하세요.
// ...이제 업로드된 이미지에 접근할 수 있습니다.
마지막으로 한글 파일명 및 특정 파일명 업로드 시 문제를 방지하기 위한 함수를 추가합니다.
import { randomUUID } from "crypto";
const createSafeFileName = (originalName: string) => {
const ext = originalName.split(".").pop()?.toLowerCase() || "png";
// 파일명만 추출
const base = originalName.replace(/\.[^/.]+$/, "");
// 영문/숫자/ - _ 만 허용 (한글, 공백, 특수문자 제거)
const safeBase = base.replace(/[^a-zA-Z0-9-_]/g, "");
// URL safe
const encoded = encodeURIComponent(safeBase || "file");
return `${Date.now()}-${randomUUID()}-${encoded}.${ext}`;
};파일명 변환을 포함한 최종 코드입니다.
// ... 생략
import { createSafeFileName } from "@/utils/createSafeFileName.ts";
export const uploadController = async (c: Context) => {
const formData = await c.req.formData();
const file = formData.get("file") as File | null;
if (!file) {
return c.json({ message: "파일이 없습니다." }, 400);
}
const mimeType = file.type;
if (!mimeType.startsWith("image/")) {
return c.json({ message: "이미지 파일만 업로드할 수 있습니다." }, 400);
}
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const folder = "uploads";
const safeFilename = createSafeFileName(file.name);
const key = `${folder}/${safeFilename}`;
const resized = await sharp(buffer)
.resize({ width: 1200, withoutEnlargement: true })
.toBuffer();
await r2Client.send(
new PutObjectCommand({
Bucket: envConfig.R2_BUCKET_NAME,
Key: key,
Body: resized,
ContentType: mimeType,
}),
);
const url = `${envConfig.CDN_URL}/${key}`;
// 필요에따라 DB 저장 로직 추가 가능
// const [image] = await db
// .insert(imagesTable)
// .values({
// folder,
// filename: safeFilename,
// url,
// refId,
// })
// .returning();
return c.json(
{
message: "File uploaded successfully",
name: safeFilename,
url,
},
200,
);
};이제 클라우드 플레어 R2에 이미지를 안전하게 업로드할 수 있습니다. 필요에 따라 이미지 리사이징 옵션이나 업로드 폴더 경로를 parameter로 조정할 수 있습니다.
이 글에서는 클라우드 플레어 R2에 이미지를 업로드하는 방법을 단계별로 설명했습니다. 클라우드 플레어 R2는 뛰어난 성능을 제공하고 보다 저렴한 스토리지 가격으로, 이미지 업로드 기능이 필요한 애플리케이션에 적합한 솔루션입니다.
이상으로 클라우드 플레어 R2에 이미지 업로드 기능을 구현하는 방법에 대한 설명을 마칩니다.