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

DB 컬럼을 추가했는데 프론트엔드에서는 여전히 에러가 납니다. `supabase gen types`의 작동 원리와 자동화된 타입 동기화 파이프라인 구축 방법을 정리해봤습니다.
서버를 끄지 않고 배포하는 법. 롤링, 카나리, 블루-그린의 차이점과 실제 구축 전략. DB 마이그레이션의 난제(팽창-수축 패턴)와 AWS CodeDeploy 활용법까지 심층 분석합니다.

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

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

내 서버가 해킹당하지 않는 이유. 포트와 IP를 검사하는 '패킷 필터링'부터 AWS Security Group까지, 방화벽의 진화 과정.

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"의 시작입니다.