Next.js와 Supabase 프로덕션 앱을 위한 데이터베이스 마이그레이션 전략

발행: (2026년 6월 11일 PM 09:57 GMT+9)
9 분 소요
원문: Dev.to

출처: Dev.to

Next.js와 Supabase 프로덕션 앱을 위한 데이터베이스 마이그레이션 전략

Supabase와 함께 Next.js 앱을 만들었습니다. 개발 환경에서는 완벽히 동작합니다. 이제 프로덕션에 배포하려고 하는데, 스키마를 안전하게 변경하려면 어떻게 해야 할까? 라는 고민이 생깁니다.
데이터베이스 마이그레이션은 스키마를 버전 관리하고 변경 사항을 안전하게 배포할 수 있게 해줍니다. 이 가이드는 기본 마이그레이션부터 무중단 프로덕션 배포까지 모든 내용을 다룹니다.

필요 사전 조건

  • Supabase 프로젝트 (로컬 및 프로덕션)
  • Supabase CLI 설치
  • Next.js 애플리케이션
  • Git (버전 관리)

마이그레이션이란?

SQL 파일 하나로 데이터베이스 스키마를 변경하는 것을 말합니다.

-- supabase/migrations/20260314120000_add_posts_table.sql
CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  title TEXT NOT NULL,
  content TEXT,
  user_id UUID REFERENCES auth.users(id),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view own posts"
  ON posts FOR SELECT
  USING (auth.uid() = user_id);

마이그레이션의 특징

  • 버전 관리됨: 타임스탬프가 포함된 파일명으로 순서를 보장
  • 추적 가능: Supabase가 어떤 마이그레이션이 실행됐는지 파악
  • 재현 가능: 같은 마이그레이션을 여러 번 실행해도 동일한 결과
  • 역전 가능: 롤백 로직을 직접 작성 가능

로컬 Supabase 초기화

npx supabase init

생성되는 구조

supabase/
  config.toml
  seed.sql
  migrations/

원격 프로젝트와 연결

npx supabase link --project-ref your-project-ref

새 마이그레이션 만들기

npx supabase migration new create_posts_table

생성되는 파일

supabase/migrations/20260314120000_create_posts_table.sql

스키마 변경 내용 작성

-- Create posts table
CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  published BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- RLS policies
CREATE POLICY "Anyone can view published posts"
  ON posts FOR SELECT
  USING (published = TRUE);

CREATE POLICY "Users can view own posts"
  ON posts FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "Users can create posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update own posts"
  ON posts FOR UPDATE
  USING (auth.uid() = user_id);

-- Indexes
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_slug_idx ON posts(slug);
CREATE INDEX posts_published_created_at_idx ON posts(published, created_at DESC);

-- Updated at trigger
CREATE TRIGGER set_updated_at
  BEFORE UPDATE ON posts
  FOR EACH ROW
  EXECUTE FUNCTION update_updated_at_column();

로컬에 적용

npx supabase db reset

로컬 데이터베이스를 삭제하고 모든 마이그레이션을 처음부터 적용합니다.

Supabase Studio(로컬)에서 스키마 변경 후 마이그레이션 생성

npx supabase db diff -f migration_name

변경 사항을 반영한 마이그레이션 파일이 생성됩니다.
생성된 SQL을 검토하고 로컬 DB에 적용합니다.

npx supabase db reset

애플리케이션 테스트 → Git 커밋 → 스테이징 테스트

npx supabase db push --db-url postgresql://staging-url

새 스키마와 함께 애플리케이션이 정상 동작하는지 확인합니다.

프로덕션에 배포

npx supabase db push
  • 애플리케이션 코드를 배포하고 오류를 모니터링합니다.

추가 마이그레이션 예시

-- supabase/migrations/20260314130000_add_view_count.sql
ALTER TABLE posts
ADD COLUMN view_count INTEGER DEFAULT 0 NOT NULL;

CREATE INDEX posts_view_count_idx ON posts(view_count DESC);
-- supabase/migrations/20260314140000_make_content_optional.sql
ALTER TABLE posts
ALTER COLUMN content DROP NOT NULL;
-- supabase/migrations/20260314150000_rename_content_to_body.sql
ALTER TABLE posts
RENAME COLUMN content TO body;
-- supabase/migrations/20260314160000_add_category_fk.sql
ALTER TABLE posts
ADD COLUMN category_id UUID REFERENCES categories(id);

CREATE INDEX posts_category_id_idx ON posts(category_id);
-- supabase/migrations/20260314170000_add_search_index.sql
CREATE INDEX posts_title_search_idx ON posts
USING GIN (to_tsvector('english', title));

다운타임 없이 스키마 변경하기

1️⃣ 새 컬럼/테이블 추가 → 기존 컬럼 유지

Phase 1 – 새 컬럼 추가

-- Migration 1
ALTER TABLE posts
ADD COLUMN published_at TIMESTAMPTZ;

Phase 2 – 애플리케이션이 양쪽 컬럼에 모두 쓰도록 배포

await supabase.from('posts').insert({
  published: true,
  published_at: new Date().toISOString() // 새 컬럼에 기록
})

Phase 3 – 기존 데이터 백필

-- Migration 2
UPDATE posts
SET published_at = created_at
WHERE published = TRUE AND published_at IS NULL;

Phase 4 – 새 컬럼을 NOT NULL 로 전환

-- Migration 3
ALTER TABLE posts
ALTER COLUMN published_at SET NOT NULL;

Phase 5 – 오래된 컬럼 삭제

-- Migration 4
ALTER TABLE posts
DROP COLUMN published;

2️⃣ 컬럼 이름 변경 시 안전하게 진행하기

  1. 새 컬럼 추가
  2. 데이터 복사
  3. 애플리케이션을 새 컬럼 사용하도록 업데이트
  4. 오래된 컬럼 삭제

3️⃣ 뷰를 활용한 호환성 유지

-- 테이블 이름 변경
ALTER TABLE posts RENAME TO articles;

-- 기존 이름으로 뷰 생성
CREATE VIEW posts AS SELECT * FROM articles;

애플리케이션을 점진적으로 마이그레이션한 뒤, 뷰를 제거합니다.

4️⃣ 스키마와 데이터 동시 마이그레이션 (태그 정규화 예시)

-- supabase/migrations/20260314180000_normalize_tags.sql

-- 새 tags 테이블 생성
CREATE TABLE tags (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name TEXT UNIQUE NOT NULL
);

-- 연결 테이블 생성
CREATE TABLE post_tags (
  post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
  tag_id UUID REFERENCES tags(id) ON DELETE CASCADE,
  PRIMARY KEY (post_id, tag_id)
);

-- 기존 tags 컬럼(text 배열)에서 데이터 이동
INSERT INTO tags (name)
SELECT DISTINCT unnest(tags) AS name
FROM posts
WHERE tags IS NOT NULL;

-- 연결 테이블 채우기
INSERT INTO post_tags (post_id, tag_id)
SELECT p.id, t.id
FROM posts p
CROSS JOIN LATERAL unnest(p.tags) AS tag_name
JOIN tags t ON t.name = tag_name;

-- 오래된 컬럼 삭제
ALTER TABLE posts DROP COLUMN tags;

롤백 계획 세우기

Supabase는 자동 롤백을 제공하지 않으므로, 각 마이그레이션에 역방향 스크립트를 직접 작성해야 합니다.

-- supabase/migrations/20260314190000_add_featured_flag.sql
ALTER TABLE posts
ADD COLUMN featured BOOLEAN DEFAULT FALSE;
-- supabase/migrations/20260314190001_rollback_featured_flag.sql
ALTER TABLE posts
DROP COLUMN featured;

트랜잭션으로 마이그레이션 감싸기

BEGIN;

-- 변경 내용
ALTER TABLE posts ADD COLUMN featured BOOLEAN;

-- 변경 검증
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_name = 'posts' AND column_name = 'featured'
  ) THEN
    RAISE EXCEPTION 'Migration failed';
  END IF;
END $$;

COMMIT;

코드와 스키마 변경을 분리하기

// 1) 스키마 변경 먼저 배포
// 2) 기능 플래그로 새 스키마 사용 여부 제어
const useNewSchema = await getFeatureFlag('use_new_posts_schema')

if (useNewSchema) {
  // 새 스키마 사용
} else {
  // 기존 스키마 사용
}

CI/CD 흐름 예시

# 데이터베이스 초기화 및 전체 마이그레이션 적용
npx supabase db reset

# 애플리케이션 테스트 실행
npm test

# 스테이징에 푸시
npx supabase db push --db-url postgresql://staging-url

# 스테이징에서 통합 테스트 실행
npm run test:integration

# 프로덕션에 마이그레이션 적용
npx supabase db push

# 롤백이 필요할 경우
npx supabase db push   # (롤백 마이그레이션 파일을 포함한 상태)

테스트 없이 바로 배포

0 조회
Back to Blog

관련 글

더 보기 »