"로컬에선 되는데 왜 배포가 안 돼!"
새로운 기능을 위해 profiles 테이블에 nickname 컬럼을 추가했습니다.
로컬에서 supabase db diff로 마이그레이션 파일을 만들고, 잘 돌아가는 걸 확인했습니다.
그리고 자신 있게 배포 명령어를 쳤습니다.
supabase db push
그런데 터미널이 빨간색 에러를 토해냈습니다.
Error: migration 20251212000000_add_nickname.sql mismatch
Remote history is different from local history.
"아니, 로컬 히스토리랑 리모트랑 다르다니? 내가 방금 짰는데?" 알고 보니 다른 팀원(혹은 어제의 나)이 서버 DB를 건드린 적이 있었던 겁니다.
처음엔 뭐가 이해가 안 갔나? (Git 충돌이랑 비슷해 보임)
저는 마이그레이션 충돌이 Git 코드 충돌(Merge Conflict)과 같은 건 줄 알았습니다. 그냥 코드 합치듯이 SQL 파일 합치면 되는 거 아닌가?
하지만 데이터베이스는 상태(State)를 가집니다.
Git은 코드가 꼬이면 다시 풀면 되지만,
DB는 ALTER TABLE을 순서대로 실행해야지, 순서가 꼬이면 데이터가 날아가거나 테이블이 깨집니다.
그래서 Supabase(Postgres)는 supabase_migrations라는 테이블에 "지금까지 실행한 마이그레이션 파일 목록"을 엄격하게 기록해둡니다.
이 기록(History)과 내 로컬 파일 목록이 단 하나라도 다르면, 안전을 위해 배포를 거부하는 것입니다.
어떤 포인트에서 이해가 됐나? (블록체인 비유)
이걸 "블록체인 장부"에 비유하니 이해가 됐습니다.
- Migration History: 한 번 기록되면 위변조가 불가능한 장부입니다.
- Local Migration Files: 내가 새로 쓰려고 하는 거래 내역입니다.
서버 장부에는 [A -> B -> C]라고 적혀 있는데,
제 로컬 파일에는 [A -> B -> D]라고 되어 있으면,
"어? 너 장부 조작했어? 너랑은 거래 안 해!" 하고 거부하는 겁니다.
제가 C를 건너뛰고 D를 들이밀었거나, C 내용을 몰래 바꿔치기했기 때문입니다.
해결 과정 - Repair와 Manual Sync
상황: 서버엔 2025..._team_change.sql이 적용되어 있는데, 내 로컬엔 그 파일이 없습니다.
방법 1 - 리모트 변경 사항 가져오기 (Pull)
가장 정석적인 방법은 서버의 변경 사항을 로컬로 가져와서 싱크를 맞추는 겁니다.
supabase db pull
이러면 서버에만 있던 스키마 변경 사항이 내 로컬 DB에 반영됩니다.
그리고 나서 다시 diff를 뜨면, 내 변경 사항만 깔끔하게 새 파일로 나옵니다.
방법 2 - 마이그레이션 히스토리 수동 조작 (Repair)
만약 "서버에 적용된 건 실수니까 무시하고, 내 걸로 덮어씌워야 해!" 라는 상황이라면? (예: 개발 서버라서 데이터를 날려도 될 때)
migration repair 명령어를 씁니다. 이건 서버의 장부를 강제로 고치는 위험한 명령어입니다.
# 서버에서 특정 버전을 '실행됨(applied)' 처리 (실제 SQL 실행 X)
supabase migration repair --status applied 20251212000000
반대로, 실제로는 실행 안 됐는데 실행된 것처럼 되어 있다면:
# 서버 기록에서 특정 버전을 삭제 (reverted)
supabase migration repair --status reverted 20251212000000
이 명령어를 통해 로컬과 리모트의 "버전 기록"을 강제로 일치시킨 후 push 하는 것입니다.
깊이 파고들기 - 안전한 팀 워크플로우
혼자 개발할 땐 db push 막 써도 됩니다. 하지만 팀이라면 규칙이 필요합니다.
- Branching Database: Supabase는 Branch 기능을 제공합니다. Git 브랜치마다 별도의 DB 인스턴스를 줍니다.
- Dashboard 수정 금지: 이게 제일 중요합니다.
- 절대 Supabase 웹 대시보드에서 테이블을 수정하지 마세요.
- 오직
migrations파일로만 수정하세요. - 대시보드에서 수정하면 "기록되지 않은 변경(Drift)"이 발생해서 나중에 피눈물 흘립니다.
추천 워크플로우:
- 로컬에서
supabase start. - 로컬 대시보드(
localhost:54323)에서 테이블 수정. supabase db diff -f add_users-> 마이그레이션 파일 생성.- Git Commit & Push.
- CI/CD(GitHub Actions)가
supabase db push실행.
다운 마이그레이션 (Down Migration) 전략 더 알아보기
마이그레이션을 배포했는데 심각한 버그가 있어서 revert해야 한다면 어떻게 할까요?
Git은 revert 커밋을 하면 코드가 되돌아가지만, DB는 그렇지 않습니다.
20251212_add_table.sql을 취소하려면, 그 반대인 DROP TABLE을 수행하는 새로운 마이그레이션 파일을 만들어야 합니다.
Supabase CLI는 기본적으로 Up migration만 생성해줍니다.
그래서 중요한 스키마 변경 시에는 수동으로 Down 스크립트를 미리 준비해두는 것이 좋습니다. (혹은 db diff로 지우는 변경사항을 생성하거나요.)
가장 좋은 건 "롤백할 일이 없게 만드는 것"입니다. 이때 Expand and Contract 패턴을 씁니다.
7. Case Study: 컬럼 이름 바꾸기 대소동 (Expand and Contract)
username 컬럼을 email로 이름을 바꾸고 싶었습니다.
순진하게 ALTER TABLE users RENAME COLUMN username TO email을 날렸습니다.
결과: 서비스 중단 (Downtime).
배포되는 순간, 아직 업데이트되지 않은 구버전 API 서버들은 여전히 username을 찾고 있었고, 쿼리 에러가 500 Internal Server Error로 이어졌습니다.
해결: 4단계 배포 (Expand and Contract)
- Expand:
email컬럼을 새로 추가합니다. (기존username도 유지). 둘 다 쓰게 합니다. - Migrate:
username의 데이터를email로 복사합니다. - Shift: API 서버 코드를
email만 읽고 쓰도록 배포합니다. - Contract: 이제 아무도 안 쓰는
username컬럼을 삭제합니다.
이렇게 하면 서비스 중단 없이 스키마를 변경할 수 있습니다. 마이그레이션은 "코드"보다 "타이밍"이 더 중요합니다.
환경 분리 (Staging vs Production) 파헤치기
"개발 서버에선 잘 되는데 왜 운영 서버에선 안 되지?" 로컬(Local), 스테이징(Staging), 프로덕션(Production) 환경의 DB 스키마가 미세하게 다르기 때문입니다.
Supabase는 프로젝트별로 별도의 Git 리모트가 아닙니다.
config.toml에 project_id를 명시해서 관리해야 합니다.
가장 추천하는 방식은 GitHub Actions에서 환경변수로 분기하는 것입니다.
- run: supabase db push --project-ref $PROJECT_ID
env:
PROJECT_ID: ${{ github.ref == 'refs/heads/main' && secrets.PROD_ID || secrets.STAGING_ID }}
이렇게 하면 develop 브랜치에 푸시하면 Staging DB가 업데이트되고, main에 머지하면 Production DB가 업데이트됩니다.
절대 내 로컬 컴퓨터에서 supabase link --project-ref 1234 명령어로 프로덕션에 직접 연결하지 마세요. 실수로 db reset이라도 치면 끝장입니다.
10. Case Study: 시드 데이터(Seed Data) 관리
마이그레이션은 "스키마(뼈대)"만 관리합니다. 하지만 "공통 코드(예: 은행 목록, 카테고리 목록)" 같은 기초 데이터는요?
supabase/seed.sql 파일을 활용하세요.
하지만 주의할 점은, seed.sql은 db reset 할 때만 실행됩니다.
이미 운영 중인 서버에 데이터를 밀어넣으려면 별도의 Data Migration 파일을 만들어야 합니다.
-- 20251214_add_banks.sql
INSERT INTO banks (name, code) VALUES ('KakaoBank', '090')
ON CONFLICT (code) DO NOTHING;
ON CONFLICT 처리는 필수입니다. 이미 들어있을 수도 있으니까요.
11. Troubleshooting: 데드락과 타임아웃 (Lock Timeout)
마이그레이션이 영원히 멈춰있다가 실패하는 경우가 있습니다. 주로 Lock 때문입니다.
ALTER TABLE 같은 명령어는 테이블 전체에 락(Access Exclusive Lock)을 겁니다.
만약 누군가 해당 테이블을 읽거나 쓰고 있다면(트랜잭션 중), 마이그레이션은 그 작업이 끝날 때까지 무한정 기다립니다.
해결책:
- Lock Timeout 설정:
SET lock_timeout = '2s'; -- 2초 안에 락 못 얻으면 에러 발생시키고 종료 ALTER TABLE users ADD COLUMN age INT; - 트래픽이 적은 시간에 배포: 새벽 시간에 배포하세요.
- 장기 트랜잭션 킬(Kill):
pg_stat_activity를 조회해서 멈춘 쿼리를 강제 종료하세요.
12. Application: CI/CD 설정
GitHub Actions로 배포 자동화를 해두면 충돌을 미리 막을 수 있습니다.
name: Deploy Migrations
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: supabase/setup-cli@v1
- run: supabase db push
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
이제 main 브랜치에 합쳐지기만 하면 DB는 알아서 최신 상태가 됩니다.
한 줄 요약
마이그레이션 충돌은 '장부 불일치'다. 절대 웹 대시보드에서 깨작깨작 수정하지 말고, 로컬에서 db diff로 파일을 만들어서 Git으로 관리해라. 꼬였을 땐 db pull이나 migration repair로 장부를 맞춰라.