08. 타입 추론 (Type Inference)

2026년 4월 30일 수정됨

📋 개요

TypeScript의 강력한 타입 추론을 활용하여 테이블 타입, JSON 컬럼 타입, 관계 쿼리 결과 타입까지 추론합니다.

📋 목차

TypeScript의 강력함을 100% 활용하는 방법입니다. 단순히 테이블 타입뿐만 아니라, JSON 컬럼 타입관계가 포함된 쿼리 결과 타입 까지 추론합니다.


🧬 1. 기본 모델 타입 (InferSelectModel, InferInsertModel)

가장 기초적인 테이블 단위 타입입니다.

import { type InferSelectModel, type InferInsertModel } from 'drizzle-orm';
import { users } from './schema';
 
// 1. 조회용 (DB에 저장된 형태) - 모든 필수 컬럼 포함
export type User = InferSelectModel<typeof users>;
 
// 2. 입력용 (Insert시 형태) - id, default값 등은 Optional
export type NewUser = InferInsertModel<typeof users>;

🪄 2. JSON 컬럼 타입 지정 ($type<T>)

DB에는 그냥 JSON으로 저장되지만, 코드에서는 구체적인 인터페이스로 쓰고 싶을 때 사용합니다.

// 타입 정의
interface UserSettings {
  theme: 'dark' | 'light';
  notifications: boolean;
}
 
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  // ⭐️ $type<T>()을 붙이면 TS가 이 컬럼을 T로 인식함
  settings: jsonb('settings').$type<UserSettings>().default({
    theme: 'light',
    notifications: true
  }),
});
 
// 결과:
// user.settings.theme  -> 'dark' | 'light' (자동완성 됨!)

🌳 3. 관계가 포함된 쿼리 결과 타입

findMany로 조인된 데이터를 가져오면 타입이 복잡해집니다. 이를 수동으로 User & { posts: Post[] } 이렇게 적으면 나중에 유지보수 부담이 열립니다. Drizzle에게 물어보세요.

방법 A: typeof 사용 (가장 추천)

// 1. 쿼리를 작성하는 함수를 만듭니다 (실행 안 함)
const usersQuery = db.query.users.findMany({
  with: {
    posts: {
      with: { comments: true }
    },
    profile: true,
  }
});
 
// 2. Awaited<...> 유틸리티 타입으로 결과 타입을 추출합니다.
// Promise를 벗겨내고 실제 데이터 타입만 가져옵니다.
export type UsersWithPosts = Awaited<typeof usersQuery>;

방법 B: Drizzle Helper 사용 (BuildQueryResult)

조금 더 복잡한 제네릭 방식이지만, 라이브러리 제작자라면 필요할 수 있습니다. (A번 방법을 권장합니다)


✂️ 4. 타입 쪼개기 (Pick / Omit)

추론된 타입에서 UI에 필요한 일부만 떼어낼 때, TypeScript 내장 유틸리티를 씁니다.

// 패스워드 뺀 유저 정보
type PublicUser = Omit<User, 'password'>;
 
// 유저 목록 카드에 보여줄 정보만
type UserCardProps = Pick<User, 'id' | 'name' | 'image'>;

📝 마무리 퀴즈

Q1. InferSelectModelInferInsertModel을 구분해서 쓰는 이유는 무엇인가요?

정답: 조회된 row의 타입과 새로 insert할 입력 타입의 필수/선택 필드가 다르기 때문입니다.

💡 상세 해설: 조회 결과에는 DB가 채운 id, createdAt 같은 값이 존재합니다. 입력 타입에서는 기본값이나 자동 생성 컬럼이 optional일 수 있습니다.

Q2. JSON 컬럼에 $type<T>()를 붙일 때의 장점과 한계는 무엇인가요?

정답: 코드에서 자동완성과 타입 검사를 얻지만, 런타임 데이터 검증까지 대신해주지는 않습니다.

💡 상세 해설: $type<UserSettings>()는 TypeScript에게 JSON 구조를 알려주는 힌트입니다. 외부 입력으로 들어오는 JSON은 Zod 같은 런타임 검증을 거쳐야 합니다.

Q3. 영철이의 테스트 타임: User & { posts: Post[] } 타입을 손으로 만들어 쓰다가 relation이 바뀌었습니다. 무엇이 문제이고 어떻게 개선하나요?

정답: 수동 타입이 실제 쿼리 결과와 어긋날 수 있으므로 쿼리에서 Awaited<typeof usersQuery>처럼 결과 타입을 추출합니다.

💡 상세 해설: 관계 쿼리는 with 옵션이 바뀔 때마다 반환 모양도 바뀝니다. 쿼리 자체에서 타입을 추출하면 코드 변경과 타입 변경이 함께 움직입니다.

🐣 영철이의 퇴근 일기

오늘은 타입 추론이 편의 기능이 아니라 변경 비용을 줄이는 장치라는 걸 배웠다. 예전에는 type User = ...를 빨리 만들고 넘어갔는데, 이제는 그 타입이 DB 스키마와 쿼리 결과를 얼마나 정직하게 따라오는지가 더 중요해졌다.

특히 JSON 컬럼이 함정이었다. $type을 붙이면 IDE가 똑똑해지지만, 외부에서 들어온 설정값이 진짜 그 모양인지 검증하는 건 별개의 일이다.

💡 "좋은 타입은 손으로 많이 쓰는 타입이 아니라, 실제 데이터 흐름에서 자동으로 따라오는 타입이다."

내일은 수동으로 만든 WithPosts, PublicUser 타입을 보면 먼저 쿼리나 스키마에서 추출할 수 없는지 확인해야겠다.

🔗 레퍼런스

다음 장: 09. Supabase 연동 (Supabase Integration)