기본 콘텐츠로 건너뛰기

추천 가젯

카카오내비 개편, 출근길 정체와 우회 경로를 출발 전에 확인하는 법

카카오내비에서 출근길이 평소보다 얼마나 밀리는지, 막히는 구간을 돌아갈 만한지 출발 전에 판단하기 쉬워집니다. 카카오모빌리티는 11월 11일 카카오내비 홈과 주행 화면을 개편한다고 발표했습니다. 평일 출퇴근길 지연을 홈에 요약하고, 경로를 고르는 단계부터 정체 원인과 CCTV를 보여주는 게 핵심입니다. 영동고속도로와 국도 우회 경로 비교 예시(2012년 자료). 이미지 제공 경기도 G뉴스플러스 . 홈에서 회사·집까지 얼마나 더 걸리는지 확인 홈 상단의 고정 목적지 카드가 평일 아침에는 회사까지, 저녁에는 집까지의 교통 상황을 보여줍니다. 평소보다 예상 소요 시간이 5분 이상 늘어나면 지연 시간과 정체 원인 등을 요약해 표시합니다. 집과 회사를 고정 목적지로 쓰는 운전자라면 출근 전에 카드를 보고 약속 시간에 맞춰 더 일찍 나갈지 판단할 수 있습니다. 빨간 구간의 CCTV를 보고 경로 비교 예전에는 정체 구간에 들어간 뒤에 볼 수 있었던 CCTV 영상, 지연 예상 시간, 정체 원인을 이제 경로 탐색 단계에서도 확인할 수 있습니다. 예를 들어 평소 타는 도로가 사고로 막혔다면, 차를 세워 둔 상태에서 영상과 예상 도착 시간을 보고 기존 경로와 우회 경로를 비교해 보세요. 정체 정보는 주행 중에도 제공되지만 화면을 직접 조작할 때는 안전한 곳에 정차하는 편이 좋습니다. 갈림길 앞에서 차로를 바꿀 시점도 안내 내가 빠질 분기점 앞부터 정체가 이어지면 진출 차로에 미리 들어가도록 권합니다. 바로 앞 분기점만 막히고 내가 빠질 곳은 원활하면, 차로 변경을 조금 뒤로 미루라고 안내할 수 있습니다. 갈림길이 500m 안에 연달아 나오는 구간에서는 도로 이름과 방향을 경로선에 표시하고 통과 순서를 숫자로 보여줍니다. 진출로가 연달아 있는 초행길에서 어느 쪽으로 먼저 빠질지 판단하기 쉬워지는 변화입니다. 내 앱에서 먼저 볼 것 출발 전에 앱 스토어에서 업데이트가 있는지 확인하고, 홈의 고정 목적지 카드와 경로 탐색 화면을 차례로 살펴보세요. 카카오모빌리티 발표...

리액트 + 비트(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 셋업은 서비스 정책/요금에 따라 달라질 수 있어요.

댓글

가장 많이 본 글

Icons by Flaticon