
DB랑 타입이 안 맞아요 (Supabase Type Generation의 함정)
DB 컬럼을 추가했는데 프론트엔드에서는 여전히 에러가 납니다. `supabase gen types`의 작동 원리와 자동화된 타입 동기화 파이프라인 구축 방법을 정리해봤습니다.

DB 컬럼을 추가했는데 프론트엔드에서는 여전히 에러가 납니다. `supabase gen types`의 작동 원리와 자동화된 타입 동기화 파이프라인 구축 방법을 정리해봤습니다.
AWS 콘솔 클릭질로 만든 서버가 왜 문제인지, Terraform으로 인프라를 코드로 선언하면 무엇이 달라지는지 실전 예제와 함께 정리했다.

새벽엔 낭비하고 점심엔 터지는 서버 문제 해결기. '택시 배차'와 '피자 배달' 비유로 알아보는 오토 스케일링과 서버리스의 차이, 그리고 Spot Instance를 활용한 비용 절감 꿀팁.

서버를 끄지 않고 배포하는 법. 롤링, 카나리, 블루-그린의 차이점과 실제 구축 전략. DB 마이그레이션의 난제(팽창-수축 패턴)와 AWS CodeDeploy 활용법까지 심층 분석합니다.

ChatGPT는 질문에 답하지만, AI Agent는 스스로 계획하고 도구를 사용해 작업을 완료한다. 이 차이가 왜 중요한지 정리했다.

bio 컬럼 추가했는데 왜 없대?"사용자 프로필에 bio(자기소개) 기능을 추가했습니다.
Supabase 대시보드에서 profiles 테이블에 bio 컬럼(Text, Nullable)을 추가했습니다.
그리고 프론트엔드 코드를 짰습니다.
const { data } = await supabase.from('profiles').select('bio');
console.log(data.bio); // Error: Property 'bio' does not exist on type ...
TypeScript 컴파일러가 빨간 줄을 그으며 화를 냅니다.
"야, profiles 테이블엔 bio 같은 거 없어!"
"아니, 방금 내가 추가했다니까? 내 눈엔 보이는데?" 저는 모니터를 삿대질하며 억울해했습니다.
저는 Supabase 클라이언트가 실시간으로 DB 스키마를 읽어오는 줄 알았습니다.
supabase-js가 마법처럼 DB를 스캔해서 타입을 알아낼 거라고 생각했죠.
하지만 TypeScript는 컴파일 타임(Compile Time)에 도는 정적 분석 도구입니다.
코드를 짜는 시점(VSCode)에 DB가 어떻게 생겼는지 알 방법이 없습니다.
누군가 "지금 DB는 이렇게 생겼어"라고 타입 정의 파일(database.types.ts)을 만들어주지 않으면, TS는 영원히 옛날 기억만 가지고 삽니다.
이걸 "졸업 앨범 촬영"에 비유하니 이해가 됐습니다.
database.types.ts): 졸업 앨범(스냅샷)입니다.제가 bio 컬럼을 추가한 건, 학생이 한 명 전학 온 겁니다.
하지만 제 손에 들린 건 작년에 찍은 졸업 앨범입니다.
앨범을 아무리 뒤져봐도 전학 온 학생 사진은 없습니다.
새로 사진을 찍어서(Type Generation) 앨범을 인쇄해야 비로소 그 학생이 보입니다.
"아, DB를 고쳤으면 앨범도 다시 찍어야 하는구나."
가장 기본적인 방법은 CLI 명령어를 치는 겁니다.
npx supabase gen types typescript --project-id "your-project-id" > src/types/database.types.ts
이 명령어가 바로 "사진 촬영" 버튼입니다. Supabase 서버에 접속해서 현재 스키마를 읽어오고, 그걸 예쁜 TypeScript 인터페이스로 변환해서 파일로 저장해줍니다. 이걸 실행하고 나면 거짓말처럼 빨간 줄이 사라집니다.
매번 저 긴 명령어를 칠 순 없습니다.
// package.json
"scripts": {
"update-types": "npx supabase gen types typescript --project-id \"abcdefg\" > src/types/database.types.ts",
"dev": "npm run update-types && next dev"
}
이제 npm run update-types만 치면 됩니다.
팀원이 DB를 고치고 코드를 푸시했는데, 타입을 안 업데이트했다면?
제 로컬에서 git pull 받고 실행하자마자 에러가 터집니다.
"아, 김대리님! 타입 업데이트 안 하셨죠?"
이런 대화를 줄이려면 CI/CD 단계에서 막아야 합니다.
# .github/workflows/type-check.yml
name: Type Check
on: [push]
jobs:
check-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm install
# DB랑 타입이 맞는지 확인 (생성 후 git diff가 있으면 실패 처리)
- run: npm run update-types
- run: git diff --exit-code
이렇게 하면 타입 파일이 최신이 아닐 때 커밋이 막히거나 CI가 실패해서, "앨범 업데이트"를 강제할 수 있습니다.
생성된 타입 파일은 꽤 복잡합니다.
매번 Database['public']['Tables']['profiles']['Row'] 이렇게 쓰면 손가락 아픕니다.
Supabase가 제공하는 Helper Type을 쓰세요.
import { Database } from '@/types/database.types';
// 이렇게 길게 쓰지 마세요 ❌
// type Profile = Database['public']['Tables']['profiles']['Row'];
// 이렇게 쓰세요 ✅
type Profile = Database['public']['Tables']['profiles']['Row'];
type UnsavedProfile = Database['public']['Tables']['profiles']['Insert']; // id, created_at 제외됨
type ProfileUpdate = Database['public']['Tables']['profiles']['Update']; // 모든 필드가 Optional
저는 이걸 더 줄이기 위해 types/helpers.ts를 따로 만들어 둡니다.
export type Tables<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Row'];
export type Enums<T extends keyof Database['public']['Enums']> = Database['public']['Enums'][T];
// 사용
type Profile = Tables<'profiles'>;
pg_catalog) 제대로 파보기supabase gen types가 어떻게 동작하는지 궁금하지 않으신가요?
이 녀석은 마법을 부리는 게 아니라, Postgres의 시스템 카탈로그(pg_catalog)를 조회합니다.
SELECT
column_name,
data_type,
is_nullable
FROM
information_schema.columns
WHERE
table_name = 'profiles';
이 쿼리 결과를 가져와서 TS 문법(interface Profile { ... })으로 변환(Transpile)하는 겁니다.
그래서 DB가 꺼져 있거나 접근 권한이 없으면 타입 생성도 실패합니다.
(그래서 CI/CD에서 돌릴 때 SUPABASE_ACCESS_TOKEN이 필요한 이유입니다.)
실제로 겪은 일입니다.
팀원이 DB에 is_premium 컬럼을 추가하고 로컬에서 잘 돌렸습니다.
그런데 배포 후 프로덕션 앱이 터졌습니다.
supabase/migrations)을 안 만듦.is_premium이 없음 -> 타입 생성 시 필드 누락 -> 빌드 에러? 아니요, 빌드 성공! (왜냐하면 CI는 로컬 DB 기준으로 타입을 생성했으니까)"DB 변경은 반드시 마이그레이션 코드로 관리해야 한다."
Dashboard UI로 깔짝거리는 건 혼자 할 때나 하는 겁니다. 팀 프로젝트에선 무조건 supabase migration new를 쓰세요.
Supabase가 타입을 잘 못 만들어주는 경우가 있습니다. 특히 복잡한 View나 SQL Function의 리턴 타입입니다.
이럴 땐 Override 타입을 만드세요.
// 원래 타입
// type UserStats = Database['public']['Views']['user_stats']['Row'];
// (이게 any로 나오거나 부정확할 때가 있음)
// 오버라이딩
export interface UserStatsOverride {
total_posts: number; // 원래 string으로 잘못 나올 때 강제 수정
last_active: string;
}
생성된 타입을 맹신하지 말고, 필요하면 확장(Extends)해서 쓰세요.
"어제는 됐는데 오늘은 타입 생성이 안 돼요." 범인은 Supabase CLI 버전 불일치일 확률이 높습니다.
로컬 CLI 버전(v1.100.0)과 Supabase 호스팅 버전(v1.110.0)이 차이가 많이 나면, gen types가 엉뚱한 결과를 뱉거나 에러를 뿜습니다.
해결책: 버전 고정
package.json에 supabase 패키지 버전을 명시하고, 팀원 모두가 같은 버전을 쓰도록 강제하세요. (engines 필드 활용)
brew install supabase로 설치하면 각자 버전이 달라지기 쉽습니다. npm install -D supabase로 프로젝트 로컬에 설치하는 것이 안전합니다.
Supabase의 jsonb 타입은 TS에서 기본적으로 Json (any랑 비슷함)으로 나옵니다.
이걸 구체적인 타입으로 바꾸고 싶다면?
interface UserMeta {
theme: 'dark' | 'light';
notifications: boolean;
}
const { data } = await supabase.from('users').select('metadata');
const meta = data.metadata as unknown as UserMeta; // 못생김
방법 2: Database 타입 오버라이드 (추천)
database.types.ts 생성 후, Json 타입을 찾아서 바꿔치기합니다.
하지만 매번 생성할 때마다 덮어씌워지므로, 별도의 Wrapper Type을 만드는 게 낫습니다.
type Row = Database['public']['Tables']['users']['Row'];
export interface UserRow extends Omit<Row, 'metadata'> {
metadata: UserMeta; // 여기서 타입을 좁혀줌
}
@ts-ignore의 유혹타입 에러가 안 잡히면 @ts-ignore나 any로 땜질하고 싶은 유혹이 듭니다.
"일단 배포하고 나중에 고치자."
하지만 이건 기술 부채(Technical Debt)가 아니라 기술 파산(Technical Bankruptcy)으로 가는 지름길입니다. DB 스키마가 변경되었을 때, 이 무시된 코드들은 런타임 에러의 지뢰밭이 됩니다.
차라리 Partial<Type>이나 Pick을 써서 타입을 느슨하게 만들더라도, any는 쓰지 마세요.
TS를 쓰는 이유를 스스로 부정하지 마십시오.
문제:
매번 supabase.from('table').select() 할 때마다 타입을 단언(as)하거나 제네릭을 길게 써야 합니다.
도전: Supabase Client를 래핑해서, 테이블 이름만 넣으면 자동으로 타입이 추론되게 만드세요.
// 목표:
const users = await db.get('users'); // users는 User[] 타입으로 자동 추론됨
힌트:
Database['public']['Tables']를 제네릭 T로 받아서 Row를 리턴하는 헬퍼 함수를 작성하면 됩니다.
이것이 진정한 "Type Safety"의 시작입니다.