추천 가젯

리액트 + 비트(Vite)로 모바일 청첩장 만들기 — 2편

모바일 청첩장 시리즈 2편 · R2 갤러리 & Firebase 방명록

클라우드플레어 R2로 갤러리 저장하고, Firebase로 방명록 달기

안녕하세요, 병민입니다 🙌 1편에서 전체 흐름을 잡았고, 이번엔 사진 업로드/보관방명록을 연결합니다. 서버는 따로 없고 Cloudflare Pages를 쓰고 있으니, Pages Functions(= 워커)로 R2에 사전서명 URL을 만들어주고, 프론트에서 그 URL로 바로 업로드하는 구조예요. 방명록은 Firebase DB로 간단·안전하게!

전체 그림
프론트(React) → /api/r2/upload로 업로드용 URL 요청 → R2에 파일 PUT
프론트(React) → /api/r2/list로 목록 요청 → 갤러리 렌더
프론트(React) → Firebase SDK로 방명록 작성/조회

1) R2 버킷 & Pages Functions 준비

  1. Cloudflare 대시보드 > R2 > Create bucket (예: wedding-gallery)
  2. 버킷 > Settings > CORS에서 사이트 도메인 허용(예: https://*.pages.dev, 커스텀 도메인)
  3. Pages 프로젝트 > Settings > Functions에서 R2 바인딩 추가:
    • Binding name: GALLERY_BUCKET
    • Bucket: wedding-gallery

* “자격증명 키”는 워커/함수에만 바인딩(백엔드). 프론트에 노출하지 않습니다.

2) Pages Functions(워커) — 업로드 URL 발급 & 목록 조회

리포지토리 루트에 /functions 폴더를 만들면 Pages가 자동으로 빌드해줍니다.

/functions/api/r2/upload.ts
export const onRequestPost: PagesFunction<{ GALLERY_BUCKET: R2Bucket }> = async (ctx) => {
  const url = new URL(ctx.request.url);
  const filename = url.searchParams.get("filename");
  const type = url.searchParams.get("type") || "image/jpeg";

  if (!filename) {
    return new Response(JSON.stringify({ error: "filename is required" }), { status: 400 });
  }

  // 업로드 키(폴더/파일명 규칙)
  const objectKey = `uploads/${Date.now()}-${filename}`;

  // 1) 업로드용 사전서명 URL 생성(일정 시간만 유효)
  const putUrl = await ctx.env.GALLERY_BUCKET.createPresignedUrl({
    method: "PUT",
    key: objectKey,
    expiration: 10 * 60, // 10분
    // R2는 조건별 옵션이 달라질 수 있음
    // 일부 플랜/지역에선 워커로 직접 put/get 수행 권장
  });

  // 2) 공개 접근 URL(리스트용) — 퍼블릭 에셋 경로(도메인+key) 형태 사용
  // * R2 퍼블릭 접근을 쓰지 않는다면, 별도 게이트웨이/워커 경유로 서빙
  const publicUrl = `https://<YOUR_PUBLIC_R2_DOMAIN_OR_ROUTE>/${objectKey}`;

  return new Response(JSON.stringify({ putUrl, publicUrl, objectKey, type }), {
    headers: { "content-type": "application/json" }
  });
};
/functions/api/r2/list.ts
export const onRequestGet: PagesFunction<{ GALLERY_BUCKET: R2Bucket }> = async (ctx) => {
  const objects = await ctx.env.GALLERY_BUCKET.list({ prefix: "uploads/" });
  // 퍼블릭 경로 규칙에 맞춰 URL 생성
  const items = objects.objects.map(o => ({
    key: o.key,
    size: o.size,
    uploaded: o.uploaded,
    url: `https://<YOUR_PUBLIC_R2_DOMAIN_OR_ROUTE>/${o.key}`
  }));
  return new Response(JSON.stringify({ items }), {
    headers: { "content-type": "application/json" }
  });
};
주의
- R2의 퍼블릭 서빙 방식(커스텀 도메인/라우트) 구성은 프로젝트마다 다릅니다. 위 <YOUR_PUBLIC_R2_DOMAIN_OR_ROUTE>는 본인 환경에 맞춰 교체하세요.
- 보안상 업로드 권한항상 서버(Functions)에서 사전서명 URL을 생성해 클라이언트에 전달하는 구조로 잡습니다.

4) Firebase — 방명록 DB 연결

방명록은 Firebase를 붙여 깔끔하게 처리했어요. Firestore든 Realtime DB든 가능하지만, 저는 문서형으로 다루기 쉬운 Firestore를 기준으로 설명할게요.

  1. Firebase 프로젝트 생성 > 웹 앱 추가 > SDK 설정값(키) 복사
  2. Firestore 사용 설정 & 보안 규칙(읽기/쓰기 조건) 점검
  3. 프론트에서 SDK로 초기화 → Guestbook 컴포넌트에서 읽고/쓰기
src/firebase.ts
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "{{YOUR_KEY}}",
  authDomain: "{{YOUR_APP}}.firebaseapp.com",
  projectId: "{{YOUR_PROJECT_ID}}",
  storageBucket: "{{YOUR_APP}}.appspot.com",
  messagingSenderId: "{{SENDER_ID}}",
  appId: "{{APP_ID}}"
};

export const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
Guestbook 사용 예 (간단 형태)
import { collection, addDoc, serverTimestamp, query, orderBy, onSnapshot } from "firebase/firestore";
import { db } from "./firebase";

export function initGuestbook(setItems) {
  const qRef = query(collection(db, "guestbook"), orderBy("createdAt", "desc"));
  return onSnapshot(qRef, (snap) => {
    const arr = [];
    snap.forEach((doc) => arr.push({ id: doc.id, ...doc.data() }));
    setItems(arr);
  });
}

export async function writeGuestbook(name, message) {
  await addDoc(collection(db, "guestbook"), {
    name, message, createdAt: serverTimestamp()
  });
}
보안 규칙 예시(Firestore)
- 공개 방명록이면 읽기는 허용, 쓰기는 간단한 rate-limit & 필드 검증 필요
- 실제 규칙은 서비스 성격에 맞게 조정하세요(스팸 방지 위해 reCAPTCHA/토큰도 고려)

5) 우리 코드에선 이렇게 붙였다

  • 갤러리: <GuestGallery /> 내부에서
    • 목록: GET /api/r2/list → 썸네일 그리드 렌더
    • 업로드: 파일 선택 → POST /api/r2/upload → 반환된 putUrl로 바로 업로드
    • 모달/더보기: 이미 1편 코드처럼 상태 관리(showAllGallery, selectedImageIndex)
  • 방명록: <Guestbook /> 내부에서
    • 초기화 시 Firestore 구독(onSnapshot)
    • 작성은 addDoc 한 줄 + 타임스탬프
실무 팁
- 파일명은 uploads/<timestamp>-<random>-원본명처럼 충돌 없이 저장
- 이미지 용량이 큰 경우, 프론트에서 리사이즈(캔버스) 후 업로드하면 데이터 절약 ⛳
- 리스트는 캐싱해도 좋지만, 새 업로드 직후엔 즉시 갱신하도록 분기 처리

6) 트러블슈팅 메모

  • CORS 에러: R2 버킷 CORS에 정확한 오리진을 넣고, PUT 메서드/헤더 허용 확인
  • 퍼블릭 URL 403: 퍼블릭 라우트 설정(도메인 매핑) 점검. 퍼블릭 모드 아니라면 워커 경유로 GET 서빙
  • Firebase 권한: 규칙 때문에 쓰기 막히면 콘솔에서 규칙/인덱스 확인
© R2/Functions/Firebase 셋업은 서비스 정책/요금에 따라 달라질 수 있어요.

댓글

가장 많이 본 글