파트너 이미지 처리 및 비용 통제 검토

파트너 이미지 처리 및 비용 통제 검토

ADR-340 파트너 이미지 처리 및 비용 통제 검토

1. Metadata

  • ADR ID: ADR-340
  • Status: draft
  • Date: 2026-03-19
  • Owner: YSY
  • Related ADRs: ADR-150, ADR-230, ADR-240, ADR-250

2. Context

  • 현재 파트너 매장 이미지는 Supabase Storage의 public bucket(partner-photos)에 저장한다.
  • 현재 구조는 최대 4장이며, 첫 번째 이미지는 thumb(320px) + detail(1080px)를 함께 만들고, 나머지는 detail 이미지만 만든다.
  • 리스트/관심매장 화면은 primary_thumb_url을 사용하고, 상세 화면은 photo_urls 전체를 가로 스트립으로 로드한다.
  • 현재 비용 구조에서 먼저 커질 가능성이 높은 항목은 Supabase MAU보다 이미지 egress다.
  • 검토 대상은 4장 유지, 2장 축소, 외부 CDN/이미지 플랫폼, 외부 사이트 이미지/WebView 대체다.

2.1 Current Baseline

이미 완료된 항목:

  1. 이미지 variant 분리는 이미 적용되어 있다.
  2. 대표 이미지는 thumb(320px) + detail(1080px)를 분리 저장하고, DB에는 primary_thumb_url, primary_detail_url을 별도 보관한다.
  3. 이미지 변경 시 partners.image_version을 증가시키는 계약이 이미 들어가 있다.
  4. 앱은 상세 화면과 파트너 편집 화면에서 ?v={image_version}를 붙여 URL 버저닝을 적용한다.
  5. 따라서 내용 변경 시 URL 변경이라는 캐시 무효화의 기본 축은 이미 확보되어 있다.

아직 남아 있는 항목:

  1. 업로드 시 cacheControl을 명시하지 않아 긴 캐시 헤더 전략은 코드 기준으로 아직 완성되지 않았다.
  2. 즉, 현재 구조는 thumb/detail 분리 + URL 버저닝까지는 적용됐지만, 강한 CDN 캐시 정책까지 확정된 상태는 아니다.
  3. Cloudflare 계층을 붙이더라도 이미지 URL이 실제로 Cloudflare를 거쳐야 캐시 절감 효과를 온전히 얻을 수 있다.

2.2 Versioning and Cache Direction

  1. 이미지 캐시 전략의 기본 원칙은 같은 URL은 오래 캐시하고, 내용이 바뀌면 URL을 바꾼다이다.
  2. URL 변경은 파일명 변경보다 image_version 기반 쿼리 파라미터 방식(?v={image_version})을 기본으로 사용한다.
  3. 따라서 캐시 무효화의 SoT는 스토리지 객체 자체가 아니라 partners.image_version이다.
  4. 이 구조에서는 긴 캐시 헤더를 붙여도 이미지 변경 시 stale URL을 강제로 재사용하지 않는다.
  5. 반대로 cacheControl 없이 URL 버저닝만 두면 정합성은 확보되지만 CDN 절감 폭은 제한될 수 있다.

3. Option Review

3.1 옵션 A. 현재 구조 유지 (Supabase + 최대 4장)

가정:

  1. 대표 thumb 120KB
  2. detail 이미지 650KB
  3. 파트너 1개 전체 세트는 2.72MB
  4. 상세 화면에서 4장을 모두 보면 약 2.60MB

장점:

  1. 이미 구현되어 있고 즉시 추가 작업이 거의 없다.
  2. 매장 분위기, 좌석, 메뉴, 외관 등 다양한 컷을 보여주기 쉽다.

단점:

  1. 리스트 화면 비용은 크게 변하지 않지만, 상세 화면 진입당 detail payload가 크다.
  2. 같은 구조에서 2장 대비 저장 용량과 detail egress가 약 1.9배 크다.
  3. Supabase Free 1GB 기준 저장 한계는 약 376개 파트너다.
  4. Supabase Pro 250GB 기준 detail full-load 한계는 약 98,461회/월 수준이다.

판단:

  1. 비용 통제 우선순위 기준에서는 MVP 기본안으로 유지하지 않는다.

3.2 옵션 B. 축소 구조 (Supabase + 최대 2장)

가정:

  1. 첫 번째 이미지는 thumb + detail
  2. 두 번째 이미지는 detail
  3. 파트너 1개 전체 세트는 약 1.42MB
  4. 상세 화면에서 2장을 모두 보면 약 1.30MB

장점:

  1. 현재 구조를 거의 유지한 채 저장 용량과 detail egress를 약 47.8% 줄일 수 있다.
  2. Supabase Free 1GB 기준 저장 한계가 약 721개 파트너로 늘어난다.
  3. Supabase Pro 250GB 기준 detail full-load 한계가 약 196,923회/월로 늘어난다.
  4. 파트너 입력 피로도도 줄어 초기 온보딩에 유리하다.

단점:

  1. 매장 소개 표현력이 줄어든다.
  2. 외관/실내/대표 메뉴/분위기 등 4컷 구성이 필요한 업종에는 부족할 수 있다.

판단:

  1. 비용과 운영 단순성을 함께 만족하는 MVP 기본안으로 채택한다.

3.3 옵션 C. Cloudflare Images 사용

전제:

  1. Cloudflare Images는 저장(Images Stored), 전달(Images Delivered), 변환(Unique Transformations) 기준으로 과금한다.
  2. 현재 2장 정책 기준으로 파트너 1개는 보통 대표 thumb, 대표 detail, 보조 detail3개 variant를 가진다고 본다.

장점:

  1. 이미지 전달 비용을 요청 기반 또는 별도 미디어 계정으로 분리할 수 있다.
  2. 포맷 변환, 리사이징, 캐시 제어, 커스텀 도메인 운용이 쉬워질 수 있다.
  3. Supabase DB/Auth와 미디어 비용을 분리 관찰하기 좋다.

단점:

  1. 업로드, 삭제, 권한, URL 생성, 캐시 무효화, 장애 대응이 모두 추가된다.
  2. 현재 DB와 앱은 파일 식별자보다 public URL 자체에 더 강하게 결합돼 있어, URL 체계가 바뀌면 저장/조회/캐시 정책을 함께 바꿔야 한다.
  3. cache purge 실패, 원본-variant 불일치, DB URL-실제 자산 불일치 같은 정합성 문제가 새로 생긴다.
  4. signed URL, hotlink 방지, 공개 범위, custom domain 같은 전달 정책 결정을 별도로 가져가야 한다.
  5. 비용 관측도 DB/Auth 비용미디어 비용이 분리되어 운영 대시보드와 알람 체계가 복잡해진다.
  6. 현재 단계에서는 2장 축소만으로도 상당한 비용 절감 효과를 얻을 수 있어 즉시 이전 명분이 약하다.

비용 감각:

  1. 공식 가격 기준 Unique Transformations 5,000건/월 무료, 초과 시 1,000건당 $0.50이다.
  2. 저장은 100,000장당 월 $5, 전달은 100,000건당 $1 수준이다.
  3. 2장 정책에서 detail gallery full-load 1회는 image deliveries 2건으로 계산한다.
  4. 2장 정책 기준 1,666개 파트너까지는 월 3 variant x 파트너 수 가정에서도 transformation 무료 구간 안에 머문다.

판단:

  1. 장기 대안으로 유지하되, 지금 즉시 기본안으로 채택하지 않는다.
  2. 순수 전달비만 보면 Supabase egress overage보다 유리해질 수 있지만, 저성장 구간에서는 Supabase 포함량을 버리고 별도 비용을 추가하는 셈이 된다.

3.4 옵션 D. Cloudflare R2 + 현재 방식 유지

전제:

  1. 현재 앱은 클라이언트에서 thumb/detail을 미리 생성하므로, R2에 완성된 variant 파일만 저장하는 구조가 가능하다.
  2. 이 경우 Cloudflare Images의 변환 과금 없이 R2 Standard storage + Class B reads만 주로 본다.

장점:

  1. 현재 미리 생성한 썸네일/상세 파일 업로드 구조와 가장 가깝다.
  2. 공식 가격 기준 egress 무료, 10GB storage 무료, 10M Class B reads 무료라서 초기부터 중기까지 매우 싸다.
  3. asset id 관리만 추가하면 이미지 비용을 DB/Auth 비용과 깔끔하게 분리할 수 있다.

단점:

  1. 업로드 인증, 파일 경로 설계, 삭제/정리, 캐시 제어를 직접 관리해야 한다.
  2. Cloudflare Images처럼 자동 포맷 변환과 variant 관리가 기본 제공되지는 않는다.
  3. 현재 Supabase public URL을 그대로 쓰는 코드에서 URL 체계를 추상화해야 한다.
  4. 이미지 삭제/교체 시 orphan file 정리, stale cache 회피, DB 메타데이터 동기화 책임이 모두 애플리케이션 쪽으로 이동한다.
  5. 비용은 싸더라도 운영 난이도는 지금보다 낮아지지 않는다.

비용 감각:

  1. 공식 가격 기준 storage $0.015/GB-month, Class B reads $0.36/million, egress 무료다.
  2. 2장 정책 기준 파트너 1개 저장량 1.42MB로 보면 약 7,211개 파트너까지 storage 무료 구간 안에 머문다.
  3. detail gallery full-load 기준 5,000,000회/월까지는 10M reads 무료 구간 안에 머문다.

판단:

  1. 외부 미디어 계층을 도입해야 한다면 비용 최적화 관점의 1순위 후보는 Cloudflare Images보다 R2 + 현행 variant 업로드다.
  2. 다만 현재 앱은 Supabase Storage에 이미 결합돼 있으므로, 이전 타이밍은 비용뿐 아니라 마이그레이션 공수와 함께 판단한다.

3.5 옵션 E. Supabase Storage Image Transformations

장점:

  1. 저장소를 바꾸지 않고 리사이징/포맷 최적화를 붙일 수 있다.
  2. WebP 자동 최적화 등으로 egress를 일부 줄일 수 있다.

단점:

  1. egress 문제를 근본적으로 없애지 못한다.
  2. Pro 기준 origin image 100개만 무료이고, 초과 시 1,000 origin images당 $5가 붙는다.
  3. 현재는 이미 클라이언트에서 공용 variant를 만들고 있어, on-demand transform을 붙여도 구조 이점이 제한적일 수 있다.

판단:

  1. Supabase 안에서 해결하려는 보조 대안으로는 가능하지만, 현재 구조의 1차 대안으로 채택하지 않는다.

3.6 옵션 F. 외부 사이트 이미지 또는 WebView 대체

예시:

  1. 파트너 홈페이지 이미지를 직접 참조
  2. 인스타그램/블로그/플레이스 페이지를 앱 안 WebView로 노출

장점:

  1. 우리 저장 비용을 줄이는 것처럼 보일 수 있다.

단점:

  1. 이미지 소유권, hotlink 허용 여부, robots, 추적 스크립트, CORS, 접속 차단 정책을 통제할 수 없다.
  2. 원본 해상도, 비율, 로딩 속도, 만료 정책이 제각각이라 앱 일관성이 깨진다.
  3. 앱 상세 화면의 핵심은 가볍고 안정적인 매장 미디어인데, WebView는 페이지 전체 비용과 불안정성을 함께 들여온다.
  4. 외부 페이지 변경이나 삭제가 곧바로 앱 품질 저하로 이어진다.
  5. 캐시/오프라인/오류 복구/심사 대응 측면에서도 불리하다.

판단:

  1. 파트너 대표 미디어의 기본안으로 채택하지 않는다.
  2. 외부 사이트는 보조 링크(홈페이지 보기)로만 허용 검토한다.

3.7 시나리오별 비용 비교 (2장 정책 기준)

전제:

  1. 이 비교는 이미지 때문에 추가로 붙는 증분 비용만 비교한다.
  2. Supabase Pro 기본요금과 compute는 Auth/DB 때문에 계속 필요하다고 보고, 이미지 overage만 계산한다.
  3. Cloudflare Images는 저장 + 전달 + transformation overage를 계산한다.
  4. Cloudflare R2는 현재와 같은 사전 생성 variant 업로드를 가정하고 storage + Class B reads만 계산한다.
  5. detail gallery full-load 1회는 약 1.30MB, image deliveries 2건, R2 reads 2건으로 본다.
시나리오파트너 수월 이미지 트래픽Supabase directCloudflare ImagesCloudflare R2
B. 1개 생활권 라이브10075GB$0약 $1.19약 $0
C. 생활권 PMF 확인300300GB약 $4.50약 $4.76약 $0
D. 도시 확장 직전1,000900GB약 $58.50약 $14.28약 $0
E. MAU 100k 돌파2,0001.8TB약 $139.50약 $29.06약 $0

해석:

  1. Supabase direct250GB 포함량 안에서는 가장 단순하고 싸다.
  2. Cloudflare Images는 저성장 구간에서는 오히려 별도 비용을 추가하지만, Supabase egress overage가 보이기 시작하는 구간부터 유리해진다.
  3. Cloudflare R2는 현재처럼 thumb/detail을 미리 만들어 올리는 구조라면 비용만 놓고는 가장 강하다.
  4. 다만 R2Cloudflare Images 모두 현재 Supabase URL/권한 구조를 바꾸는 공수만 있는 것이 아니라, 이후 운영 모델 자체를 더 복잡하게 만든다.
  5. 따라서 현재 권장 순서는 2장 축소 -> 실제 egress 계측 -> 필요 시 R2 우선 검토 -> 이미지 변환 요구가 커지면 Cloudflare Images 검토다.

4. Domain Decision

  1. 파트너 대표 미디어의 SoT는 플랫폼이 직접 관리하는 이미지 자산으로 고정한다.
  2. 파트너 대표 미디어는 외부 웹페이지나 제3자 웹 콘텐츠에 의존하지 않는다.
  3. MVP 기본 이미지 정책은 대표 1장 + 보조 1장, 총 최대 2장으로 고정한다.
  4. 첫 번째 이미지는 리스트/관심매장용 thumb와 상세용 detail을 함께 가지는 대표 자산으로 정의한다.
  5. 두 번째 이미지는 상세 화면에서만 사용하는 보조 자산으로 정의한다.

5. Product Decision

  1. 기본 플랜 기준 파트너 이미지 허용 수는 최대 2장으로 운영한다.
  2. 제품 가치는 가볍고 신뢰 가능한 매장 신호를 우선하고, 초기부터 풍부한 갤러리를 기본 제공하지 않는다.
  3. 파트너가 더 많은 사진을 보여주고 싶다면 앱 기본 갤러리 확장 대신 외부 홈페이지/소셜 링크를 보조 경로로 제공하는 방식을 우선 검토한다.
  4. 기존에 3장 이상 저장된 파트너는 정책 전환 후에도 즉시 깨지지 않도록 읽기 경로에서 우선 앞 2장만 사용한다.
  5. 3번째/4번째 이미지는 향후 별도 상위 플랜, 수동 승인, 또는 특수 업종 정책이 필요할 때만 재검토한다.

6. UX Decision

  1. 파트너 편집 화면의 이미지 슬롯은 2개만 노출한다.
  2. 첫 번째 슬롯은 대표 이미지, 두 번째 슬롯은 보조 이미지로 명확히 구분한다.
  3. 상세 화면은 최대 2장만 보여주며, WebView로 외부 페이지를 대체 렌더링하지 않는다.
  4. 사진 더 보기 수요는 앱 내부 갤러리 확장보다 외부 링크 열기 같은 보조 액션으로 대응한다.
  5. 기존에 3장 이상이 있는 파트너라도 사용자에게는 우선 앞 2장만 노출해 레이아웃을 단순하게 유지한다.

7. Tech Decision

  1. partners.photo_urls의 최대 cardinality는 4에서 2로 축소한다.
  2. PHOTO_SLOTS4에서 2로 축소한다.
  3. 첫 번째 이미지에만 primary_thumb_urlprimary_detail_url를 유지하고, 두 번째 이미지는 detail URL만 유지한다.
  4. 리스트/관심매장/최근 본 화면은 계속 primary_thumb_url만 사용한다.
  5. 상세 화면은 photo_urlsprimary_detail_url을 조합하되 최대 2장까지만 구성한다.
  6. 이미지 캐시 무효화는 계속 image_version 기반 URL 버저닝을 표준으로 사용한다.
  7. 업로드 경로는 variant 이미지에 Cache-Control: public, max-age=31536000, immutable 또는 동등한 긴 캐시 헤더를 명시하는 것을 기본 정책으로 한다.
  8. 이 긴 캐시 헤더 정책은 image_version이 URL에 항상 반영된다는 전제에서만 유효하다.
  9. 외부 CDN 이전은 현재 즉시 진행하지 않고, 플랫폼 관리 업로드 -> 플랫폼 관리 전달 구조를 유지한다.
  10. 외부 CDN 검토는 다음 조건 중 하나가 충족될 때 시작한다.
  11. 월 image egress 150GB 이상이 2개월 연속 발생한다.
  12. 파트너 1,000개 이상 단계에서 detail 이미지 트래픽이 반복적으로 Pro 포함량 근처까지 올라간다.
  13. 리사이징, 서명 URL, 이미지 정책 분리 등 미디어 전용 계층의 운영 이점이 비용보다 커진다.
  14. 외부 웹페이지/WebView는 대표 미디어 전달 계층으로 사용하지 않는다.
  15. 외부 미디어 계층으로 이전할 때 비용 최적화 1순위 후보는 Cloudflare R2 + 사전 생성 variant 유지다.
  16. 외부 미디어 계층으로 이전할 때 자동 최적화와 variant 관리 가치가 커지면 Cloudflare Images를 다음 후보로 검토한다.

8. Ops Decision

  1. 운영 지표에 월 image egress, thumb 평균 바이트, detail 평균 바이트, 파트너당 이미지 수, 상세 화면 이미지 로드 수를 포함한다.
  2. 2장 정책 전환 후 기존 3번째/4번째 이미지는 즉시 삭제하지 않고, 읽기 중단과 저장 중단을 먼저 적용한다.
  3. 실제 스토리지 정리는 정책 전환 안정화 이후 별도 배치로 수행한다.
  4. 외부 CDN 이전 여부는 월간 비용 리뷰에서 Supabase egress, 파트너 수, detail screen load를 함께 보고 판단한다.
  5. 외부 사이트/WebView를 대표 미디어 예외로 허용하지 않는다.
  6. 외부 CDN으로 이전하더라도 비용 외 운영 리스크 항목(cache purge, orphan file, broken URL, signed/public 정책)을 함께 점검한다.

9. Implementation Contract (Optional)

9.1 Data Contract

  • partners.photo_urls는 최대 2개까지만 저장한다.
  • partners.primary_thumb_url은 첫 번째 이미지의 썸네일 URL을 저장한다.
  • partners.primary_detail_url은 첫 번째 이미지의 상세 URL을 저장한다.
  • image_version은 기존과 동일하게 이미지 변경 시 증가시킨다.
  • 이미지 내용이 바뀌면 클라이언트 렌더링 URL도 ?v={image_version} 기준으로 새 URL을 사용해야 한다.
  • 쿼리 파라미터 버저닝 형식은 v={image_version}으로 통일한다.

9.2 UI Contract

  • 파트너 편집 화면의 이미지 안내 문구는 최대 2장 기준으로 고정한다.
  • 첫 번째 슬롯은 대표 (thumb/detail), 두 번째 슬롯은 보조 이미지로 표시한다.
  • 사용자는 세 번째 이미지 슬롯을 보지 않아야 한다.

9.3 Runtime Contract

  • 리스트 계층은 primary_thumb_url만으로 카드를 렌더링해야 한다.
  • 상세 계층의 gallery 구성 함수는 최대 2장까지만 반환해야 한다.
  • 기존 데이터에 3장 이상이 있더라도 렌더링 계층은 앞 2장만 사용해야 한다.
  • 상세/편집 미리보기 계층은 image_version을 URL에 반영해야 한다.
  • URL이 이미 쿼리스트링을 갖고 있더라도 버저닝은 &v= 형식으로 추가할 수 있어야 한다.
  • 업로드 계층은 variant 이미지 저장 시 긴 캐시 헤더를 일관되게 명시해야 한다.

9.4 Delivery Contract

  • 공개 이미지 variant는 Cache-Control: public, max-age=31536000, immutable 또는 동등한 장기 캐시 정책을 사용한다.
  • 긴 캐시 정책은 image_version 증가 없는 overwrite를 전제로 사용하지 않는다.
  • CDN을 붙이더라도 URL 버저닝 규칙은 그대로 유지한다.

9.5 Test/Acceptance Contract

  • 파트너 저장 요청은 photo_urls 3개 이상을 허용하지 않아야 한다.
  • 파트너 편집 UI는 슬롯 2개만 노출해야 한다.
  • 상세 화면은 최대 2장의 이미지 카드만 렌더링해야 한다.
  • 기존 4장 데이터가 있어도 앱은 오류 없이 앞 2장만 노출해야 한다.
  • 이미지 정책 전환 후에도 리스트 썸네일 노출은 깨지지 않아야 한다.
  • 이미지 변경 후 image_version이 증가하고 새 렌더링 URL이 달라져야 한다.
  • 업로드된 variant 응답은 긴 캐시 헤더를 가져야 한다.

10. Consequences / Impact

긍정 영향:

  1. 현재 구조 기준 저장 용량과 detail egress를 거의 절반 수준으로 줄일 수 있다.
  2. 파트너 온보딩 UI와 검수 기준이 단순해진다.
  3. Supabase direct public URL 구조를 그대로 유지하면서도 비용 민감도를 낮출 수 있다.
  4. 이미 적용된 URL 버저닝을 유지함으로써 긴 캐시 헤더나 CDN 캐시를 붙일 준비가 되어 있다.
  5. 긴 캐시 헤더 + 버저닝 조합으로 썸네일 재방문 비용을 크게 줄일 수 있다.

부정 영향:

  1. 일부 업종은 매장 표현력이 부족해질 수 있다.
  2. 기존 4장 설계를 전제로 한 문구, 제약, 테스트를 함께 수정해야 한다.
  3. 외부 CDN을 미루는 동안 미디어 계층 분리 이점은 당장 얻지 못한다.
  4. 반대로 외부 CDN을 조기 도입하면 비용은 줄어도 정합성, purge, 권한, 관측성 복잡도가 증가한다.
  5. cacheControl이 비어 있으면 URL 버저닝이 있어도 CDN 비용 절감 폭이 기대보다 작을 수 있다.
  6. image_version 증가 없이 같은 URL을 overwrite하면 장기 캐시 정책과 충돌할 수 있다.

11. Rollout / Migration

  1. 1차로 읽기/쓰기 정책을 최대 2장으로 전환한다.
  2. 기존 3장 이상 데이터는 렌더링에서 앞 2장만 사용한다.
  3. 편집 UI, DB 제약, 저장 함수, 상세 gallery 구성을 같은 배포 묶음으로 맞춘다.
  4. 안정화 후 미사용 3번째/4번째 이미지 정리 배치를 별도로 검토한다.

12. Validation

  • Domain/Product/UX/Tech/Ops 결정이 서로 충돌하지 않는다.
  • App/db/03_store_domain.sql의 이미지 제약과 문서 결정이 일치한다.
  • 파트너 편집 UI와 상세 화면이 최대 2장 정책으로 정렬된다.
  • image_version 기반 URL 버저닝 계약이 렌더링 계층과 문서에서 일치한다.
  • 업로드 계층의 긴 캐시 헤더 정책이 문서와 구현에서 일치한다.
  • TECH_COST_SCENARIOS.md의 이미지 비용 판단과 모순되지 않는다.
  • 외부 CDN 재검토 트리거가 운영 지표로 측정 가능하다.