Supabase 무료 플랜 정지 복구 기록

By Yoon
·
thumbnail
Supabase가 멈췄다 — 새 프로젝트 마이그레이션 기록

글을 시작하며

이 글은 Claude로 작성한 초안에 글쓴이의 수정과 보완을 더한 글입니다.
Next.js + MongoDB + Supabase Storage 로 운영중인 YoonLog의 Supabase가 정지됐을 때 해결 과정입니다.

왜 Supabase가 정지됐을까?

Supabase를 무료로 사용하면서, 일정 기간 활동이 없어 정지되는 문제가 발생했습니다. 정지된 경우 쉽게 복구가 가능한 경우도 있다고 하지만, 저 같은 경우는 복구가 안되는 상황이였습니다.

따라서, 복구 대신 새 프로젝트로 마이그레이션하기로 했습니다.


Step 1. Supabase CLI 설치 및 로그인

brew install supabase/tap/supabase

로그인은 Access Token 방식을 사용했습니다. supabase.com/dashboard/account/tokens 에서 토큰을 발급받은 뒤:

export SUPABASE_ACCESS_TOKEN=
supabase projects list

기존 프로젝트와 새로 만든 프로젝트가 모두 보이면 성공입니다.


Step 2. 새 Supabase 프로젝트에 Storage 버킷 생성

새 프로젝트의 API 키를 가져옵니다:

curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  "https://api.supabase.com/v1/projects/[NEW_PROJECT_ID]/api-keys"

service_role 키로 버킷을 생성합니다:

curl -X POST \
  -H "Authorization: Bearer [SERVICE_ROLE_KEY]" \
  -H "Content-Type: application/json" \
  -d '{"id": "images", "name": "images", "public": true}' \
  "https://[NEW_PROJECT_ID].supabase.co/storage/v1/bucket"

public: true를 꼭 설정해야 이미지가 인증 없이 공개적으로 접근 가능합니다.


Step 3. 기존 이미지 새 버킷으로 업로드

기존 Supabase 프로젝트에서 이미지를 미리 다운로드해 로컬에 보관하고 있었습니다. 아래 스크립트로 일괄 업로드했습니다:

#!/bin/bash
SERVICE_KEY="[SERVICE_ROLE_KEY]"
BUCKET_DIR="/path/to/downloaded/images"
SUPABASE_URL="https://[NEW_PROJECT_ID].supabase.co"

for file in "$BUCKET_DIR"/*; do
  filename=$(basename "$file")
  ext="${filename##*.}"

  case "$ext" in
    png) mime="image/png" ;;
    jpg|jpeg) mime="image/jpeg" ;;
    mov) mime="video/quicktime" ;;
    *) mime="application/octet-stream" ;;
  esac

  res=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
    -H "Authorization: Bearer $SERVICE_KEY" \
    -H "Content-Type: $mime" \
    --data-binary "@$file" \
    "$SUPABASE_URL/storage/v1/object/images/$filename")

  echo "$res: $filename"
done

주의: Supabase Storage는 중복 파일 업로드 시 HTTP 400을 반환하지만, 응답 body의 statusCode"409"입니다. 스크립트에서 400을 실패로 처리하면 실제로는 성공한 파일도 실패로 보일 수 있습니다. 업로드 후 버킷 파일 수로 검증하세요.

버킷 파일 수 확인:

curl -X POST \
  -H "Authorization: Bearer [SERVICE_ROLE_KEY]" \
  -H "Content-Type: application/json" \
  -d '{"prefix": "", "limit": 200}' \
  "https://[NEW_PROJECT_ID].supabase.co/storage/v1/object/list/images"

Step 4. Vercel 환경변수 업데이트

npm install -g vercel
vercel login
vercel link --scope [TEAM] --project yoon-log

# 기존 값 삭제
vercel env rm SUPABASE_URL production --yes
vercel env rm SUPABASE_KEY production --yes
vercel env rm SUPABASE_BUCKET production --yes

# 새 값 등록
echo "https://[NEW_PROJECT_ID].supabase.co" | vercel env add SUPABASE_URL production
echo "[NEW_ANON_KEY]" | vercel env add SUPABASE_KEY production
echo "images" | vercel env add SUPABASE_BUCKET production

Step 5. MongoDB 이미지 URL 일괄 교체

가장 중요한 단계입니다. DB에 저장된 포스트들의 thumbnailUrlcontent 안에 구 Supabase URL이 하드코딩되어 있었습니다.

# 기존
https://[OLD_PROJECT_ID].supabase.co/storage/v1/object/public/[OLD_BUCKET]/[FILE]

# 변경
https://[NEW_PROJECT_ID].supabase.co/storage/v1/object/public/images/[FILE]

Python으로 일괄 교체했습니다:

from pymongo import MongoClient

client = MongoClient("[MONGODB_URI]")
db = client["test"]
posts = db["posts"]

OLD_BASE = "https://[OLD_PROJECT_ID].supabase.co/storage/v1/object/public/[OLD_BUCKET]"
NEW_BASE = "https://[NEW_PROJECT_ID].supabase.co/storage/v1/object/public/images"

for post in posts.find({"$or": [
    {"thumbnailUrl": {"$regex": "[OLD_PROJECT_ID]"}},
    {"content": {"$regex": "[OLD_PROJECT_ID]"}}
]}):
    new_thumbnail = post.get("thumbnailUrl", "").replace(OLD_BASE, NEW_BASE)
    new_content = post.get("content", "").replace(OLD_BASE, NEW_BASE)

    posts.update_one(
        {"_id": post["_id"]},
        {"$set": {"thumbnailUrl": new_thumbnail, "content": new_content}}
    )
    print(f"업데이트: {post.get('title')}")

Step 6. next.config.js 이미지 도메인 수정

Next.js의 <Image /> 컴포넌트는 next.config.js에 허용된 도메인만 로드합니다. 구 Supabase 도메인이 하드코딩되어 있어서 이미지가 보이지 않았습니다.

// next.config.js
images: {
  remotePatterns: [
    {
      protocol: "https",
      hostname: "[NEW_PROJECT_ID].supabase.co", // 여기를 새 프로젝트 ID로 변경
    },
  ],
},

변경 후 커밋 & 재배포하면 완료입니다.


트러블슈팅 요약

문제원인해결
이미지 업로드 스크립트가 전부 실패로 표시Supabase가 중복 시 HTTP 400 반환 (body는 409)버킷 파일 수로 별도 검증
환경변수 업데이트 후에도 이미지 안 뜸MongoDB에 구 URL 하드코딩Python 스크립트로 일괄 교체
코드 수정 후에도 이미지 안 뜸next.config.js 도메인 미변경새 Supabase 호스트명으로 수정 후 재배포
Vercel CLI 배포 실패Node.js 18.x 지원 종료22.x로 업그레이드
이미지 URL에 %0A 포함echo로 환경변수 등록 시 줄바꿈 자동 삽입printf로 재등록
업로드 후 이미지 깨짐anon key는 Storage 업로드 권한 없음service_role key로 교체
기본 썸네일 이미지 깨짐컴포넌트에 구 Supabase URL 하드코딩새 URL로 코드 수정 후 재배포

번외: 또 정지되지 않으려면? GitHub Actions로 자동 ping

마이그레이션을 마쳤더라도 Supabase 무료 플랜은 일정 기간 사용하지 않으면 다시 정지될 수 있습니다. 이를 막기 위해 GitHub Actions 크론잡으로 5일마다 자동으로 ping을 보내도록 설정했습니다.

# .github/workflows/keep-supabase-alive.yml
name: Keep Supabase Alive

on:
  schedule:
    - cron: '0 0 */5 * *' # 5일마다 자정에 실행

jobs:
  ping:
    runs-on: ubuntu-latest
    steps:
      - name: Ping Supabase
        run: |
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
            https://[PROJECT_ID].supabase.co/storage/v1/bucket \
            -H "Authorization: Bearer ${{ secrets.SUPABASE_KEY }}")
          echo "Response: $STATUS"
          if [ "$STATUS" != "200" ]; then
            echo "Supabase ping failed with status $STATUS"
            exit 1
          fi

SUPABASE_KEY는 GitHub 리포 → Settings → Secrets and variables → Actions 에서 등록하면 됩니다.

  • 정상 응답이면 조용히 넘어가고
  • 실패 시 GitHub에서 이메일 알림이 옵니다

마치며

Supabase 무료 플랜을 사이드 프로젝트에서 쓸 때는 정지 가능성을 항상 염두에 두고, 이미지 URL을 환경변수 기반으로 관리하거나 도메인에 의존하지 않는 구조로 설계하면 다음 마이그레이션이 훨씬 수월합니다.

// 개선 예시: URL을 환경변수로 조합
const imageUrl = `${process.env.SUPABASE_URL}/storage/v1/object/public/${process.env.SUPABASE_BUCKET}/${filename}`;

이렇게 하면 Supabase 프로젝트가 바뀌어도 환경변수만 교체하면 됩니다.