sooyoungarchive
소개
전체보기
프로젝트
블로그 만들기그림톡캠핑 인스타
개발

© 2026 sooyoung archive. All rights reserved.

개인정보처리방침연락처
← 목록
Sooyoung Archive

캠핑인스타 만들기 4편

2026년 3월 3일 · 29회 조회

🏕️ Next.js + Supabase 캠핑 소셜 앱 만들기 4편: OG 이미지 삽질기와 Supabase 함정 카드

지난 3편에서는 앱의 전반적인 UI를 인스타그램 스타일로 리팩토링하고, Hydration Error나 Silent Failure 같은 버그를 잡았다. 이번 4편에서는 공유 기능의 핵심인 Open Graph(OG) 이미지를 구현하면서 겪은, 솔직히 말하면 꽤 삽질한 이야기를 정리한다.


1. 목표: 링크 공유 시 예쁜 미리보기 이미지 만들기

카카오톡이나 슬랙에 링크를 붙여넣으면 자동으로 뜨는 미리보기 이미지, 그게 OG 이미지다. 기존에는 단색 초록 배경에 텍스트만 덩그러니 놓여있었는데, 게시물의 첫 번째 캠핑 사진을 배경으로 꽉 채우고 싶었다.

구현 방식

  • Next.js의 next/og (내부적으로 Satori + Resvg 사용)를 통해 서버에서 동적으로 PNG 이미지를 생성
  • 라우트 구조: /og/feed/[id] (게시물), /og/camp/[id] (캠핑장)
  • DB에서 게시물 데이터를 조회 → 첫 번째 이미지를 fetch → Base64로 변환 → <img> 태그로 배경에 삽입

2. 오늘의 트러블 슈팅 🔥

이번 편의 하이라이트다. OG 이미지 하나 제대로 띄우려고 원인을 3겹으로 까야 했다. 하나를 고치면 다음 문제가 나타나는, 마치 러시안 인형 같은 디버깅이었다.

🐛 이슈 1: "데이터가 안 나와요" — Supabase RLS의 조용한 거절

증상: OG 이미지에 항상 기본값("캠핑 후기", @camper, ❤️ 0)만 표시되고, 실제 게시물 데이터가 전혀 반영되지 않았다.

삽질 과정: 처음에는 이미지 렌더링 문제인 줄 알고 CSS를 뒤집어엎고, Base64 인코딩 방식을 바꾸고, 타임아웃을 늘리고… 온갖 곳을 건드렸다. 하지만 브라우저에서 직접 스크린샷을 찍어보니, 텍스트 자체가 기본값이었다. 이미지 문제가 아니라 데이터 조회 자체가 실패하고 있었던 것이다.

진짜 원인: Supabase의 RLS(Row Level Security) 정책이 anon 키로 saved_campsites 테이블 읽기를 차단하고 있었다. 코드의 catch 블록이 에러를 조용히 삼켜버려서(// fallback only), 실패해도 아무 로그 없이 기본값만 보여줬다.

// ❌ 이전: anon 키 → RLS에 의해 모든 데이터가 빈 배열로 반환
const supabase = createPublicSupabaseClient(); // anon key 사용

// ✅ 수정: service_role 키 → RLS를 우회하여 공개 게시물 조회 가능
const supabase = createClient(supabaseUrl, serviceRoleKey, {
  auth: { persistSession: false, autoRefreshToken: false },
});

교훈: catch 블록에서 에러를 삼키지 말자. 최소한 개발 환경에서는 console.error로 찍어두자. 그리고 데이터가 안 나오면 렌더링보다 DB 조회부터 의심하자.


🐛 이슈 2: "이미지 URL은 맞는데 왜 400이야?" — Supabase Image Transformation은 유료

증상: 데이터 조회는 성공했지만(텍스트는 잘 나옴), 배경 이미지만 여전히 초록색이었다.

원인: OG 이미지 용량을 줄이기 위해 Supabase의 Image Transformation API(/storage/v1/render/image/...)를 사용했는데, 이 기능은 Pro 플랜 이상에서만 동작한다. 무료 플랜에서는 HTTP 400을 반환한다.

# 400 Bad Request! 💀
curl -s -o /dev/null -w "%{http_code}" \
  "https://xxx.supabase.co/storage/v1/render/image/public/camping-photos/photo.jpg?width=1200"
# 200 OK ✅
curl -s -o /dev/null -w "%{http_code}" \
  "https://xxx.supabase.co/storage/v1/object/public/camping-photos/photo.jpg"

해결: Image Transformation API를 포기하고, 원본 URL(/storage/v1/object/public/...)을 직접 fetch하도록 변경했다. 어차피 업로드 시 클라이언트에서 이미 WebP 압축 + 1600px 리사이징을 하고 있었기 때문에, 원본도 충분히 작았다.

교훈: Supabase 무료 플랜의 제약을 미리 파악하자. 공식 문서에 /render/image/가 Pro 전용이라는 게 작게 적혀있었다. 😅


🐛 이슈 3: "왜 이미지가 꽉 안 차지?" — Satori 엔진의 CSS 제약

증상: 배경 이미지가 드디어 나타났지만, 전체 영역(1200×630)을 꽉 채우지 못하고 초록색 여백이 보였다.

원인: Satori(Next.js OG 이미지를 그리는 SVG 렌더 엔진)는 일반 브라우저가 아니다. width: "100%" 같은 상대 단위를 제대로 계산하지 못하는 경우가 있다.

// ❌ Satori에서 100%가 제대로 동작하지 않음
style: { width: "100%", height: "100%" }

// ✅ 명시적 픽셀 값으로 지정
style: { width: "1200px", height: "630px" }
// + <img> 태그에 width={1200} height={630} 속성도 추가

교훈: Satori는 브라우저가 아니다. 지원하는 CSS 속성이 제한적이므로, 가능하면 절대 단위(px)를 사용하고, backgroundImage보다 <img> 태그가 더 안정적이다.


3. 최종 아키텍처 정리

사용자가 링크 공유
    ↓
/og/feed/[id] 라우트 호출
    ↓
Supabase (service_role) → saved_campsites 조회
    ↓
첫 번째 이미지 URL 추출
    ↓
서버에서 이미지 fetch → ArrayBuffer → Base64 Data URI 변환
    ↓
React.createElement로 레이아웃 구성
  ├─ <img> 배경 (1200x630, objectFit: cover)
  ├─ <div> 어두운 그라데이션 오버레이
  └─ <div> 텍스트 (캠핑장명, 내용, 닉네임, 좋아요/댓글)
    ↓
next/og ImageResponse → PNG 반환

4. 기타 작업 내역

🔗 공유 기능 404 에러 수정

공유 버튼 클릭 시 404가 뜨는 문제가 있었는데, 라우팅 경로 불일치가 원인이었다. 미들웨어(proxy) 설정을 점검하여 해결했다.

🖼️ 캠핑장 상세 페이지 UI 개선

  • 히어로 이미지 영역에 뒤로가기 버튼을 오버레이로 배치
  • 헤더 겹침(overlap) 이슈를 해결하여 스크롤 시에도 자연스러운 레이아웃 유지

🔐 카카오 로그인 리다이렉트 수정

배포 환경에서 카카오 로그인 후 로컬(localhost)로 리다이렉트되는 문제를 발견하고, Supabase Auth 콜백 URL 설정을 프로덕션 도메인으로 맞춰 해결했다.


5. 배운 것들 (TL;DR)

| 문제 | 원인 | 해결 | |------|------|------| | OG 이미지에 기본값만 표시 | Supabase RLS가 anon 키 차단 | service_role 키 사용 | | 이미지 fetch 시 400 에러 | Image Transformation API는 Pro 전용 | 원본 URL 직접 사용 | | 이미지가 꽉 안 참 | Satori의 100% CSS 미지원 | 명시적 픽셀값 1200px 사용 | | catch 블록의 Silent Failure | 에러를 삼키는 빈 catch문 | console.error로 로깅 |


🚀 다음 편 예고

  • 스토리 기능 실제 데이터 연동
  • 검색 기능 고도화 (지역 + 키워드 복합 필터)
  • 알림 센터 MVP 구현
  • 배포 후 실제 카카오톡/슬랙 공유 테스트

"에러 메시지가 없다고 에러가 없는 게 아니다." 오늘의 가장 큰 교훈이다. catch 블록을 비워두면 개발 시간이 3배로 늘어난다. 🥲

댓글