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에 저장된 포스트들의 thumbnailUrl과 content 안에 구 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 프로젝트가 바뀌어도 환경변수만 교체하면 됩니다.