테마 전환

Supabase Storage 실전 가이드: 파일 업로드, CDN, 액세스 제어

Easton editorial illustration: cost-quality-speed triangle

지난주 한 독자가 제게 물었습니다. “사용자가 아바타를 업로드하는 기능에는 S3와 Cloudflare R2 중 무엇이 좋을까요?”

잠시 생각이 멈췄습니다. 두 프로젝트에서 이 문제로 모두 시행착오를 겪었기 때문입니다. S3 권한 정책을 설정할 때는 머리가 복잡해졌고, R2는 저렴하지만 인증 시스템을 따로 구축해야 했습니다. 결국 프로젝트를 Supabase Storage로 옮겼습니다. Supabase Storage가 모든 문제를 해결하는 ‘만능 도구’여서가 아니라, 이미 Supabase의 Auth와 데이터베이스를 사용한다면 Storage까지 묶어서 쓰는 경험이 정말 매끄럽기 때문입니다.

이 글에서는 Supabase Storage의 핵심 구조, 세 가지 액세스 제어 방식, 대용량 파일 업로드 시 주의할 점, CDN 최적화 방법, R2/S3와의 비용 비교를 하나씩 살펴보겠습니다. 코드는 모두 바로 실행할 수 있는 예제입니다.


1. Supabase Storage 핵심 아키텍처

먼저 한 가지를 분명히 하겠습니다. Supabase Storage는 내부적으로 AWS S3를 사용합니다.

하지만 그 위에 매우 얇은 추상화 계층을 제공합니다. JavaScript SDK로 바로 조작할 수 있을 만큼 얇아서 AWS의 복잡한 자격 증명 체계와 IAM 정책을 직접 다룰 필요가 없습니다.

Auth와 자동 연동

제가 가장 좋아하는 부분입니다. Supabase에서 bucket을 생성한 뒤 supabase.auth.getUser()로 얻은 JWT token을 사용해 업로드와 다운로드 권한을 제어할 수 있습니다. 별도의 권한 시스템을 구축할 필요가 없습니다.

// 업로드할 때 사용자 인증 정보가 자동으로 포함됩니다
const { data, error } = await supabase.storage
  .from('avatars')
  .upload('user-123/profile.jpg', file)

내부적으로는 RLS(Row Level Security) 정책을 확인합니다. 설정 방법은 뒤에서 자세히 다룹니다.

글로벌 CDN 자동 적용

업로드한 파일은 Cloudflare CDN을 통해 자동으로 배포됩니다. CloudFront나 Cloudflare Workers를 직접 설정할 필요가 없습니다.

브라우저 개발자 도구를 열고 응답 헤더의 cf-cache-status를 확인해 보세요.

cf-cache-status: HIT

HIT는 캐시 적중, MISS는 캐시 미적중을 뜻합니다. Private bucket은 캐시 정책이 더 엄격하므로 일반적으로 Public bucket의 적중률이 훨씬 높습니다.

Smart CDN: 캐시 ‘자동 무효화’ 메커니즘

Supabase의 돋보이는 기능 중 하나입니다. 기존 CDN에서는 파일을 갱신한 뒤 캐시를 직접 purge하거나 TTL이 만료될 때까지 기다려야 합니다. Supabase의 Smart CDN은 파일 metadata를 엣지 노드에 자동으로 동기화합니다. 파일이 갱신되면 최대 60초 안에 전 세계에 반영됩니다.

그렇다고 너무 빨리 안심해서는 안 됩니다. 실시간성이 중요한 일부 상황에서는 60초도 상당히 깁니다. 즉시 반영해야 한다면 뒤에서 소개할 cacheNonce 매개변수를 사용해야 합니다.


2. 세 가지 액세스 제어 방식 비교

많은 사람이 혼동하는 부분입니다. Public bucket, Private bucket, Signed URL은 각각 적합한 사용 사례가 다릅니다. 잘못 선택하면 캐시 적중률이 지나치게 낮아지거나 보안 문제가 생길 수 있습니다.

3가지
액세스 제어 방식
Public, Private, Signed URL은 각각 적합한 사용 사례가 있습니다

2.1 Public Bucket: 공개 리소스에 가장 적합

웹사이트 logo, 블로그 이미지, 공개 문서처럼 애초에 누구나 보도록 만든 파일이라면 Public bucket을 사용하면 됩니다.

장점:

  • URL이 가장 간결합니다: https://xxx.supabase.co/storage/v1/object/public/bucket-name/file.jpg
  • 캐시 적중률이 가장 높고, Auth 검증 없이 CDN이 바로 응답합니다
  • 코드가 가장 간단해 한 줄이면 충분합니다
// Public URL 가져오기
const { data } = supabase.storage
  .from('public-images')
  .getPublicUrl('hero-banner.jpg')

console.log(data.publicUrl)
// https://xxx.supabase.co/storage/v1/object/public/public-images/hero-banner.jpg

적합한 사용 사례:

  • 공개 표시되는 사용자 아바타
  • 블로그 이미지
  • 웹사이트 정적 리소스
  • 공개 문서

2.2 Private Bucket + Signed URL: 비공개 파일의 표준 방식

사용자가 업로드한 계약서, 회원 전용 콘텐츠, 민감한 문서처럼 공개해서는 안 되는 파일도 있습니다. 이때는 Private bucket을 사용하고 유효 기간이 있는 Signed URL을 생성합니다.

// 1시간 동안 유효한 액세스 링크 생성
const { data, error } = await supabase.storage
  .from('private-docs')
  .createSignedUrl('contracts/user-123.pdf', 3600) // 3600초 = 1시간

console.log(data.signedUrl)
// https://xxx.supabase.co/storage/v1/object/sign/private-docs/contracts/user-123.pdf?token=xxx

주의: Signed URL은 생성할 때마다 달라지므로 캐시 적중률에 영향을 줍니다. Signed URL을 자주 새로 만들면 CDN에서 계속 MISS가 발생합니다.

최적화 팁: 같은 사용자가 짧은 시간 안에 같은 파일에 여러 번 액세스한다면 Signed URL을 프론트엔드나 Redis에 캐시해 두고 매번 새로 생성하지 마세요.

2.3 RLS 정책: 세밀한 권한 제어

가장 강력하지만 가장 쉽게 간과되는 기능입니다. storage.objects 테이블에 RLS 정책을 정의해 어떤 사용자가 어떤 파일을 조작할 수 있는지 정확하게 제어할 수 있습니다.

사례 1: 사용자는 자신의 폴더에만 업로드할 수 있음

-- storage.objects 테이블에 정책 생성
CREATE POLICY "Users can upload to own folder"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'avatars' 
  AND auth.uid()::text = (storage.foldername(name))[1]
);

-- 설명: name은 'user-123/avatar.jpg' 같은 파일의 전체 경로입니다
-- storage.foldername(name)[1]은 첫 번째 폴더 이름인 'user-123'을 가져옵니다
-- auth.uid()는 현재 로그인한 사용자의 ID입니다
-- 두 값이 일치해야 업로드할 수 있습니다

사례 2: 관리자는 모든 파일에 액세스할 수 있음

CREATE POLICY "Admins can access all"
ON storage.objects FOR ALL
USING (
  auth.jwt() ->> 'role' = 'admin'
);

사례 3: 회원만 특정 콘텐츠를 다운로드할 수 있음

CREATE POLICY "Members can download premium content"
ON storage.objects FOR SELECT
USING (
  bucket_id = 'premium-content'
  AND EXISTS (
    SELECT 1 FROM user_subscriptions
    WHERE user_id = auth.uid()
    AND status = 'active'
  )
);

이 세 정책을 조합하면 대부분의 비즈니스 시나리오를 처리할 수 있습니다.


3. 파일 업로드 실전

이제 직접 구현해 보겠습니다.

3.1 표준 업로드: 작은 파일을 빠르게 처리

5MB보다 작은 파일은 upload 메서드로 바로 업로드하면 됩니다.

// React 업로드 컴포넌트
import { useState } from 'react'
import { supabase } from './supabase-client'

export function AvatarUpload() {
  const [uploading, setUploading] = useState(false)
  const [avatarUrl, setAvatarUrl] = useState<string | null>(null)

  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return

    setUploading(true)
    
    const fileExt = file.name.split('.').pop()
    const fileName = `${Date.now()}.${fileExt}`
    const filePath = `avatars/${fileName}`

    const { error } = await supabase.storage
      .from('public-images')
      .upload(filePath, file, {
        cacheControl: '3600', // 브라우저 캐시 1시간
        upsert: false // 기존 파일을 덮어쓰지 않음
      })

    if (error) {
      alert('업로드 실패: ' + error.message)
    } else {
      const { data } = supabase.storage
        .from('public-images')
        .getPublicUrl(filePath)
      setAvatarUrl(data.publicUrl)
    }

    setUploading(false)
  }

  return (
    <div>
      <input 
        type="file" 
        accept="image/*" 
        onChange={handleUpload}
        disabled={uploading}
      />
      {avatarUrl && <img src={avatarUrl} alt="avatar" />}
      {uploading && <p>업로드 중...</p>}
    </div>
  )
}

몇 가지 세부 사항을 살펴보겠습니다.

  • cacheControl은 브라우저 캐시 시간을 설정하며 CDN 캐시와는 별개입니다
  • upsert: false는 의도하지 않은 덮어쓰기를 방지합니다. 덮어쓰려면 true로 변경하세요
  • 파일명에는 timestamp를 사용해 중복을 피합니다. UUID를 사용해도 됩니다

3.2 TUS 청크 업로드: 대용량 파일을 안정적으로 처리

5MB가 넘는 파일을 업로드하거나 네트워크가 불안정한 환경이라면 TUS 청크 업로드를 사용합니다.

핵심 제한: chunkSize는 반드시 6MB여야 하며 변경할 수 없습니다. Supabase에 하드코딩된 제한입니다.

유효 기간: 업로드 URL은 24시간 동안 유효하며 시간이 지나면 다시 생성해야 합니다.

먼저 의존성을 설치합니다.

npm install tus-js-client uppy @uppy/core @uppy/dashboard @uppy/tus

전체 코드는 다음과 같습니다.

import Uppy from '@uppy/core'
import { Dashboard } from '@uppy/react'
import Tus from '@uppy/tus'
import { supabase } from './supabase-client'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'

export function LargeFileUploader() {
  const uppy = new Uppy({
    restrictions: {
      maxFileSize: 100 * 1024 * 1024, // 100MB
      allowedFileTypes: ['video/*', 'image/*']
    }
  })

  // Supabase session token 가져오기
  const getSession = async () => {
    const { data: { session } } = await supabase.auth.getSession()
    return session?.access_token || ''
  }

  uppy.use(Tus, {
    endpoint: 'https://xxx.supabase.co/storage/v1/upload/resumable',
    chunkSize: 6 * 1024 * 1024, // 반드시 6MB
    async onBeforeRequest(req) {
      const token = await getSession()
      req.setHeader('Authorization', `Bearer ${token}`)
    },
    onAfterResponse(req, res) {
      // 업로드 완료 후 파일 경로 가져오기
      const location = res.getHeader('Location')
      console.log('File uploaded to:', location)
    }
  })

  return (
    <div style={{ maxWidth: '600px', margin: '0 auto' }}>
      <Dashboard uppy={uppy} />
    </div>
  )
}

문제 해결: 청크 업로드가 6MB에서 멈춘다면 다음 항목을 확인하세요.

  1. chunkSize가 6MB인지 확인합니다(정확히 일치해야 함)
  2. Token이 만료되지 않았는지 확인합니다(유효 기간 24시간)
  3. RLS 정책에서 INSERT를 허용하는지 확인합니다(GitHub Issue #563 참고)

3.3 Presigned Upload URL: 서드파티 업로드 권한 부여

사용자가 직접 업로드하도록 하면서 service_role key는 노출하고 싶지 않을 때가 있습니다. createSignedUploadUrl로 presigned upload URL을 생성하세요.

// 서버에서 업로드 URL 생성
const { data, error } = await supabase.storage
  .from('user-uploads')
  .createSignedUploadUrl('documents/report.pdf')

// data.signedUrl을 프론트엔드에 전달해 바로 업로드할 수 있습니다
// 프론트엔드는 service_role key를 알 필요가 없습니다

4. CDN 및 이미지 최적화

4.1 Smart CDN 메커니즘 자세히 알아보기

앞서 설명했듯 Smart CDN은 파일이 갱신되면 캐시를 자동으로 무효화합니다. 하지만 60초의 전파 지연이 답답하게 느껴지는 경우도 있습니다.

권장 사항:

  1. 자주 갱신하는 파일은 새 경로에 업로드합니다
// 이렇게 하지 마세요: 매번 같은 파일을 갱신
await storage.from('images').upload('logo.png', file, { upsert: true })

// 이렇게 하세요: 매번 새 파일명 생성
const version = Date.now()
await storage.from('images').upload(`logo-${version}.png`, file)
  1. cacheNonce로 캐시를 강제 우회합니다
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('logo.png', {
    cacheNonce: Date.now().toString() // 요청할 때마다 값이 달라집니다
  })
  1. Signed URL을 재사용합니다

같은 사용자가 같은 파일에 액세스한다면 Signed URL을 저장해 두고 매번 새로 생성하지 마세요.

4.2 이미지 변환 및 자동 최적화

Supabase는 실시간 이미지 변환을 지원합니다. 너비, 높이, 품질, 형식을 모두 조정할 수 있습니다.

제한 사항:

  • 너비와 높이: 1~2500px
  • 파일 크기: ≤25MB
  • 해상도: ≤50MP
// 썸네일 가져오기
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('hero.jpg', {
    transform: {
      width: 300,
      height: 200,
      resize: 'cover', // 또는 'contain', 'fill'
      quality: 80,
      format: 'webp' // WebP로 자동 변환
    }
  })

Next.js 이미지 Loader 연동:

// next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './lib/supabase-image-loader.js'
  }
}
// lib/supabase-image-loader.js
export default function supabaseLoader({ src, width, quality }) {
  const url = new URL(src)
  url.searchParams.set('width', width.toString())
  url.searchParams.set('quality', (quality || 75).toString())
  url.searchParams.set('format', 'webp')
  return url.toString()
}

요금: 이미지 변환 요금은 origin images 1,000장당 $5입니다. 반응형 이미지의 여러 크기처럼 하나의 이미지를 많이 변환한다면 이 비용을 정확히 계산해야 합니다.


5. 비용 비교 및 선택 가이드

많은 사람이 궁금해하는 부분입니다. 주요 서비스를 표로 비교해 보겠습니다.

5.1 가격 비교표

서비스스토리지 요금송신 요금무료 할당량특징
Supabase StorageS3 요금 기준CDN 별도 과금Pro 플랜에 포함Auth 연동, RLS
Cloudflare R2$0.015/GB무료10GB + 1M ops송신 요금 없음
AWS S3$0.023/GB$0.09/GB5GB/12개월가장 강력한 생태계
DigitalOcean Spaces$5/250GB포함없음고정 비용
$0
Cloudflare R2 송신 요금

5.2 서비스 선택

다운로드가 많은 경우 → R2

이미지 공유 사이트나 동영상 호스팅처럼 파일이 자주 다운로드된다면 R2의 무료 송신 정책으로 큰 비용을 절약할 수 있습니다. S3의 송신 요금은 GB당 약 10센트이므로 트래픽이 늘면 부담이 커집니다.

Auth 연동이 필요한 경우 → Supabase Storage

이미 Supabase의 Auth와 데이터베이스를 사용하고 있다면 Storage 연동은 매우 매끄럽습니다. 사용자 권한 제어와 RLS 정책을 그대로 재사용할 수 있습니다.

AWS 생태계를 깊이 사용하는 경우 → S3

Lambda, CloudFront, S3 Select, S3 Glacier처럼 아키텍처가 이미 AWS에 깊이 묶여 있다면 서비스를 바꾸는 비용이 절약할 수 있는 금액보다 더 클 수 있습니다.

예산이 고정되어 있고 트래픽을 예측할 수 있는 경우 → DigitalOcean Spaces

월별 고정 요금이므로 사용량 기반 과금을 신경 쓰고 싶지 않은 소규모 프로젝트에 적합합니다.

5.3 비용 최적화 팁

  1. 수명 주기 정책: 오래된 파일을 Glacier로 자동 보관합니다
  2. 이미지 압축: 업로드 전에 압축하거나 Supabase의 이미지 변환을 사용합니다
  3. Public bucket으로 캐시 적중률 향상: 공개할 수 있는 파일은 가능한 한 공개합니다
  4. Signed URL 재사용: 반복 생성으로 인한 캐시 MISS를 줄입니다

6. 자주 발생하는 문제 해결

6.1 파일을 갱신했는데 이전 버전이 표시됨

원인: Smart CDN의 60초 전파 지연입니다.

해결 방법:

  • 60초 동안 기다립니다
  • 새 경로에 업로드합니다
  • cacheNonce로 캐시를 우회합니다

6.2 청크 업로드가 6MB에서 멈춤

원인: chunkSize 설정이 잘못되었습니다.

해결 방법: chunkSize: 6 * 1024 * 1024로 바이트 단위까지 정확히 설정하세요.

// 잘못된 설정: 5MB로 지정
chunkSize: 5 * 1024 * 1024 // 업로드가 멈춥니다

// 올바른 설정: 반드시 6MB
chunkSize: 6 * 1024 * 1024

6.3 업로드 시 403 Forbidden 반환

원인: RLS 정책이 올바르게 설정되지 않았습니다.

확인 절차:

  1. bucket이 Public인지 Private인지 확인합니다
  2. storage.objects 테이블의 RLS 정책을 확인합니다
  3. 정책이 INSERT 작업을 허용하는지 확인합니다
-- 기존 정책 확인
SELECT * FROM pg_policies WHERE tablename = 'objects';

-- 업로드를 허용하는 정책 추가
CREATE POLICY "Allow upload"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'your-bucket');

6.4 Signed URL에 액세스할 수 없음

원인: URL이 만료되었거나 token이 유효하지 않습니다.

해결 방법:

  • 만료 시간이 적절한지 확인합니다
  • token이 잘리지 않았는지 확인합니다
  • 테스트할 때는 24시간처럼 유효 기간이 긴 URL을 먼저 생성합니다

정리

핵심 내용을 다시 정리해 보겠습니다.

  • Public bucket은 공개 리소스에 적합하며 캐시 적중률이 가장 높습니다
  • Private bucket + Signed URL은 비공개 파일에 적합하며 캐시 정책에 주의해야 합니다
  • RLS 정책은 세밀한 권한 제어를 제공합니다. 이 기능을 간과하지 마세요
  • TUS 청크 업로드는 대용량 파일을 처리하며 chunkSize는 반드시 6MB여야 합니다
  • Smart CDN은 캐시를 자동 무효화하지만 60초의 지연이 있습니다
  • 서비스 선택: 다운로드가 많다면 R2, Auth 연동이 필요하면 Supabase Storage, AWS 생태계를 사용한다면 S3가 적합합니다

이미 Supabase 데이터베이스와 인증을 사용하고 있다면 Storage는 자연스러운 선택입니다. 하지만 객체 스토리지만 필요하고 트래픽이 많으며 예산에 민감하다면 R2의 무료 송신 정책은 분명 매력적입니다.

Supabase Storage에 관해 궁금한 점이 있거나 직접 겪은 문제가 있나요? 댓글로 경험을 공유해 주세요.


참고 자료

Supabase Storage 파일 업로드 전체 과정

bucket 생성부터 RLS 정책 설정까지 안전하고 제어 가능한 파일 업로드를 구현합니다

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Storage Bucket 생성

    Supabase Dashboard에서 bucket을 생성합니다:

    • Storage 페이지로 이동한 뒤 "Create a new bucket"을 클릭합니다
    • bucket 이름을 입력합니다(예: avatars, documents)
    • Public 또는 Private 모드를 선택합니다
    • Public bucket의 파일은 바로 액세스할 수 있고, Private bucket은 Signed URL이 필요합니다
  2. 2

    Step 2: RLS 정책 설정

    storage.objects 테이블에 액세스 제어를 정의합니다:

    ```sql
    -- 사용자는 자신의 파일만 조작할 수 있습니다
    CREATE POLICY "Users manage own files"
    ON storage.objects FOR ALL
    USING (auth.uid()::text = (storage.foldername(name))[1]);
    ```

    • bucket_id가 대상 bucket과 일치해야 합니다
    • auth.uid()로 현재 사용자 ID를 가져옵니다
    • storage.foldername()으로 파일 경로를 분석합니다
  3. 3

    Step 3: 표준 업로드 구현

    upload 메서드로 작은 파일(<5MB)을 업로드합니다:

    ```typescript
    const { error } = await supabase.storage
    .from('bucket-name')
    .upload('path/file.jpg', file, {
    cacheControl: '3600',
    upsert: false
    });
    ```

    • cacheControl로 브라우저 캐시 시간을 설정합니다
    • upsert: false로 기존 파일 덮어쓰기를 방지합니다
  4. 4

    Step 4: TUS 청크 업로드 설정

    큰 파일(>5MB)은 TUS 프로토콜로 처리합니다:

    • 의존성 설치: npm install @uppy/tus tus-js-client
    • chunkSize: 6 * 1024 * 1024로 설정합니다(반드시 6MB)
    • Authorization header에 JWT token을 전달합니다
    • 업로드 URL의 유효 기간은 24시간입니다
  5. 5

    Step 5: CDN 캐시 최적화

    캐시 적중률을 높이는 핵심 방법입니다:

    • Public bucket의 캐시 적중률이 가장 높습니다
    • 자주 갱신하는 파일은 덮어쓰지 말고 새 경로를 사용합니다
    • cacheNonce로 캐시를 강제로 새로고침합니다
    • Signed URL을 재사용해 반복 생성을 피합니다

FAQ

Supabase Storage의 chunkSize는 왜 반드시 6MB여야 하나요?
Supabase 서버에 하드코딩된 제한이기 때문입니다. 다른 값(예: 5MB)으로 설정하면 업로드가 멈출 수 있습니다. 코드에서 chunkSize: 6 * 1024 * 1024로 정확히 설정하세요.
Public bucket과 Private bucket 중 무엇을 선택해야 하나요?
파일 액세스 권한에 따라 선택합니다:

• Public bucket: 웹사이트 정적 리소스, 공개 이미지, 블로그 이미지처럼 누구나 액세스할 수 있는 파일
• Private bucket: 사용자 개인 파일, 회원 전용 콘텐츠, 민감한 문서처럼 Signed URL 또는 RLS 정책으로 액세스를 제어해야 하는 파일
파일을 갱신했는데 왜 이전 버전이 계속 표시되나요?
Smart CDN의 캐시 무효화가 전 세계 노드에 전파되는 데 최대 60초가 걸립니다. 해결 방법은 세 가지입니다. 60초 동안 기다리거나, 새 경로에 업로드하거나(권장), cacheNonce 매개변수로 캐시를 강제로 우회하세요.
Supabase Storage와 Cloudflare R2 중 어느 쪽이 더 저렴한가요?
사용 시나리오에 따라 다릅니다:

• 다운로드가 많을 때: R2는 송신 요금이 없어 더 저렴합니다(S3 송신 요금은 $0.09/GB)
• Auth 연동이 필요할 때: Supabase Storage가 더 편리합니다(RLS 정책을 그대로 재사용할 수 있음)
• 이미 Supabase를 사용 중일 때: Storage가 자연스러운 선택입니다
• 순수 객체 스토리지만 필요할 때: R2의 비용이 더 낮습니다
RLS 정책으로 사용자가 자신의 파일에만 액세스하도록 제한하려면 어떻게 하나요?
storage.foldername()으로 파일 경로를 분석하고 auth.uid()로 사용자 ID를 대조합니다:

```sql
CREATE POLICY "Users own files"
ON storage.objects FOR ALL
USING (
bucket_id = 'avatars'
AND auth.uid()::text = (storage.foldername(name))[1]
);
```

이렇게 하면 사용자는 경로의 첫 번째 부분이 자신의 ID인 파일만 조작할 수 있습니다.
Signed URL의 캐시 적중률이 낮을 때는 어떻게 해야 하나요?
Signed URL은 생성할 때마다 달라져 CDN 캐시 MISS가 발생합니다. Signed URL을 프론트엔드나 Redis에 저장해 같은 사용자가 짧은 시간 안에 재사용하도록 하세요. 또는 RLS 정책으로 액세스를 제어하고 파일은 Public bucket을 통해 제공할 수 있습니다.

4분 읽기 · 게시일: 2026년 4월 14일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog