LabHub

블로그

TypeScript ORM·쿼리 빌더 2026 — Drizzle·Kysely·Prisma·postgres.js 심층 비교 (SQL에 얼마나 가까워야 하는가)

한국어English日本語

프롤로그 — "Prisma 또는 raw SQL"의 시대는 끝났다

2026년에도 신규 TypeScript 프로젝트의 첫 번째 회의에서 빠지지 않는 질문이 있다.

"DB 접근, 뭐 쓸 거예요?"

2022년만 해도 답은 보통 두 개였다. Prisma(가장 유명, 풀 ORM) 아니면 raw pg/mysql2 (직접 SQL, 타입은 알아서). 사이에는 TypeORM·Sequelize 같은 클래스 기반 ORM이 끼어 있었지만, TypeScript 친화도와 마이그레이션 경험 때문에 신규 프로젝트에서는 점점 안 쓰였다.

2026년은 다르다. 두 개의 새로운 강자가 자리를 굳혔다.

여기에 Prisma가 새로 등장한 두 트럼프 카드.

그리고 모두를 흔드는 한 갈래.

이 글은 2026년 5월 기준으로 이 도구들을 직접 비교한다. 추상화 정도, 마이그레이션, 엣지, 번들, escape hatch, 멀티 DB의 6축으로. 그리고 같은 쿼리를 네 가지 도구로 나란히 짜본다.


1장 · 풍경 — 도구 지도

먼저 도구를 분류한다. 모든 게 같은 자리에 있지 않다.

분류도구한 줄 요약
풀 ORM (Active Record·Data Mapper)Prisma·MikroORM·TypeORM·Sequelize모델·관계·페치·캐시까지 챙긴다
헤드리스 ORMDrizzle스키마는 코드, 쿼리는 SQL 1:1
쿼리 빌더Kysely·Knex스키마 없이 타입 안전 SQL 합성
Raw 클라이언트 + 타입 헬퍼postgres.js·pg·mysql2 + Zodtagged template + 런타임 검증
관계형 DSLEdgeQL(EdgeDB)·SurrealQLDB가 자체 쿼리 언어

이 글의 초점은 굵게 표시한 4그룹의 대표 — Prisma·Drizzle·Kysely·postgres.js — 다. MikroORM·TypeORM은 마지막 장에서 짧게 다룬다.

왜 이 4개인가


2장 · 추상화 스펙트럼 — 어디까지 감출 것인가

데이터 접근 라이브러리는 "DB와 코드 사이에 얼마나 두꺼운 층을 둘지"의 선택이다. 왼쪽이 SQL에 가깝고, 오른쪽이 객체에 가깝다.

SQL                                                              객체
 |                                                                |
postgres.js   --   Kysely   --   Drizzle   --   Prisma   --   MikroORM/TypeORM
(raw)             (빌더)         (헤드리스 ORM)   (풀 ORM)        (Active Record)

추상화가 두꺼울수록 좋은가? 단언컨대 아니다. 트레이드오프다.

2026년의 분위기는 "SQL에 가깝게, 단 타입은 안전하게" 쪽으로 기울었다. Drizzle·Kysely·postgres.js의 성장이 그 증거다. 다만 Prisma 6의 엔진 재작성으로 두꺼운 쪽도 가벼워져서, "ORM이라서 무거워요" 논리는 옛말이 되어 가고 있다.


3장 · Drizzle — "TypeScript로 쓰는 SQL"

Drizzle은 2026년 신규 프로젝트에서 가장 많이 선택되는 데이터베이스 도구 중 하나다. 핵심 철학 3가지.

  1. 스키마는 TS 코드schema.ts에서 테이블·컬럼·관계를 선언한다. DB가 진실의 원천이 아니라, TS 파일이 진실의 원천이다.
  2. 쿼리는 SQL 1:1db.select().from(users).where(eq(users.email, '...')) 같은 식. SQL을 아는 사람이 보면 무엇으로 컴파일될지 즉시 알 수 있다.
  3. 헤드리스 + 작음 — 런타임 의존성이 거의 없다. Cloudflare Workers·Vercel Edge·Deno Deploy에 그대로 올라간다. 번들 크기는 압축 기준 7~15KB 수준.

Drizzle 스키마

// db/schema.ts
import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  authorId: integer('author_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  body: text('body').notNull(),
})

Drizzle Kit으로 마이그레이션

# drizzle.config.ts 작성 후
npx drizzle-kit generate   # 스키마 변경에서 SQL 마이그레이션 생성
npx drizzle-kit migrate    # 실제 DB에 적용
npx drizzle-kit studio     # 로컬 GUI

Drizzle의 강점

Drizzle의 약점


4장 · Kysely — "스키마 없는 순수 쿼리 빌더"

Kysely는 다른 길이다. 스키마를 강제하지 않는다. 너의 DB 스키마는 너의 일이고(SQL 파일·Atlas·Sqitch·Liquibase 무엇이든), Kysely는 그 위에 타입 안전 쿼리 빌더만 제공한다.

// db/types.ts — 너의 DB 스키마와 매칭되는 TS 타입
import type { ColumnType, Generated } from 'kysely'

export interface Database {
  user: UserTable
  post: PostTable
}

export interface UserTable {
  id: Generated<number>
  email: string
  name: string
  created_at: ColumnType<Date, string | undefined, never>
}

export interface PostTable {
  id: Generated<number>
  author_id: number
  title: string
  body: string
}

Kysely의 핵심 트릭은 위 Database 인터페이스를 제네릭으로 받아서, 모든 쿼리에서 컬럼 이름·타입을 자동 추론하는 것이다.

kysely-codegen으로 타입 자동 생성

스키마 인터페이스를 손으로 쓰지 않으려면 kysely-codegen을 쓴다. 라이브 DB를 인트로스펙션해서 위 같은 TS 타입을 생성한다.

npx kysely-codegen --url postgres://user:pw@localhost/db --out-file db/types.ts

Kysely의 강점

Kysely의 약점


5장 · Prisma — 새 엔진과 Prisma Postgres

Prisma는 2024~2025 사이 두 번의 큰 변화가 있었다.

5.1 엔진 재작성 — Rust에서 TS 네이티브 + Go로

Prisma 5 시대의 query engine은 Rust로 작성된 별도 프로세스(또는 WASM)였다. 콜드 스타트가 느리고, 엣지 런타임 호환성이 어색했다. Prisma 6 이후 단계적 재작성으로 다음이 바뀌었다.

5.2 TypedSQL — Prisma 안에서 raw SQL을 타입 안전하게

Prisma의 약점은 항상 "복잡한 SQL을 못 짠다"였다. TypedSQL은 그 약점을 직접 친다.

-- prisma/sql/findActiveUsers.sql
SELECT id, email, name
FROM "User"
WHERE last_seen_at > $1
ORDER BY last_seen_at DESC
LIMIT $2;
import { PrismaClient } from '@prisma/client'
import { findActiveUsers } from '@prisma/client/sql'

const prisma = new PrismaClient()
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
const users = await prisma.$queryRawTyped(findActiveUsers(since, 50))
//    ^ users: Array of typed rows, inferred from the SQL parameters

.sql 파일이 빌드 타임에 검사되고, 파라미터·결과 타입이 자동 생성된다. SQL을 SQL로 쓰되, 타입 안전성은 유지.

5.3 Prisma Postgres — 매니지드 서비스

Prisma 사가 직접 운영하는 Postgres. 핵심 차별화는 3개.

매니지드 Postgres 시장은 이미 Neon·Supabase·PlanetScale Postgres·Vercel Postgres가 있어서 경쟁이 치열하지만, Prisma 사용자에게는 가장 친화적인 선택지가 됐다.

Prisma의 강점 (2026)

Prisma의 약점 (2026)


6장 · postgres.js·pg·mysql2 — ORM 없이 가기

가장 얇은 길. tagged template과 TypeScript의 추론·satisfies만으로 충분하다는 진영.

// db/index.ts
import postgres from 'postgres'

export const sql = postgres(process.env.DATABASE_URL!, {
  max: 10,
  idle_timeout: 20,
})

// 쿼리 — tagged template
type User = { id: number; email: string; name: string }
const users = await sql<User[]>`
  SELECT id, email, name
  FROM users
  WHERE email = ${'foo@example.com'}
  LIMIT 10
`
//   ^ users: User[]

postgres.js는 자체적으로 SQL injection 방지(파라미터 바인딩)와 prepared statement 캐싱을 한다. 타입은 손으로 선언하거나, kysely-codegen 같은 도구로 생성하거나, Zod로 런타임 검증한다.

Zod로 런타임 검증

import { z } from 'zod'

const UserRow = z.object({
  id: z.number(),
  email: z.string().email(),
  name: z.string(),
})

const rows = await sql<unknown[]>`SELECT id, email, name FROM users LIMIT 10`
const users = rows.map((r) => UserRow.parse(r))
//   ^ users: z.infer<typeof UserRow>[]

이 패턴의 장점은 DB가 실제로 무엇을 돌려주는지를 한 번 검증한다는 것. ORM·쿼리 빌더의 타입은 컴파일 타임 약속일 뿐, 실제 DB가 약속을 어겼는지(컬럼 NULL·잘못된 타입) 잡지는 못한다.

Raw 클라이언트의 강점

Raw 클라이언트의 약점


7장 · 같은 쿼리, 네 가지 도구 — "email로 사용자 조회"

가장 단순한 쿼리부터. email로 user 한 명 찾고, 그 user의 최신 post 5개를 같이 가져오기.

7.1 Drizzle

import { db } from './db'
import { users, posts } from './db/schema'
import { eq, desc } from 'drizzle-orm'

async function findUserWithPosts(email: string) {
  const user = await db.query.users.findFirst({
    where: eq(users.email, email),
    with: {
      posts: {
        orderBy: [desc(posts.id)],
        limit: 5,
      },
    },
  })
  return user
  // user: { id, email, name, createdAt, posts: Array of post rows } or undefined
}

db.query.users.findFirst는 Drizzle Relations API. 내부적으로 한 번의 SQL로 json_agg나 LATERAL join을 만들어 보낸다.

7.2 Kysely

import { db } from './db'
import { jsonArrayFrom } from 'kysely/helpers/postgres'

async function findUserWithPosts(email: string) {
  const user = await db
    .selectFrom('user')
    .where('email', '=', email)
    .select((eb) => [
      'id',
      'email',
      'name',
      jsonArrayFrom(
        eb
          .selectFrom('post')
          .whereRef('post.author_id', '=', 'user.id')
          .orderBy('post.id', 'desc')
          .limit(5)
          .select(['post.id', 'post.title', 'post.body'])
      ).as('posts'),
    ])
    .executeTakeFirst()
  return user
}

Kysely는 sub-select 헬퍼로 nested JSON을 만든다. jsonArrayFrom은 Postgres json_agg 한 줄 컴파일. SQL이 무엇이 될지 머릿속에 그대로 그려진다.

7.3 Prisma

import { prisma } from './db'

async function findUserWithPosts(email: string) {
  const user = await prisma.user.findUnique({
    where: { email },
    include: {
      posts: {
        orderBy: { id: 'desc' },
        take: 5,
      },
    },
  })
  return user
}

읽기 가장 쉽다. Prisma가 내부적으로 알아서 효율적인 쿼리를 만든다(예전엔 N+1 우려가 있었지만 새 엔진에서는 join strategy를 명시적으로 선택할 수 있다).

7.4 postgres.js (raw)

import { sql } from './db'

type UserRow = {
  id: number
  email: string
  name: string
  posts: Array<{ id: number; title: string; body: string }>
}

async function findUserWithPosts(email: string) {
  const rows = await sql<UserRow[]>`
    SELECT
      u.id, u.email, u.name,
      COALESCE(
        json_agg(json_build_object('id', p.id, 'title', p.title, 'body', p.body))
          FILTER (WHERE p.id IS NOT NULL),
        '[]'::json
      ) AS posts
    FROM users u
    LEFT JOIN LATERAL (
      SELECT id, title, body
      FROM posts
      WHERE author_id = u.id
      ORDER BY id DESC
      LIMIT 5
    ) p ON TRUE
    WHERE u.email = ${email}
    GROUP BY u.id
  `
  return rows[0]
}

길지만 정확히 무엇이 도는지 안다. COALESCE(json_agg(...) FILTER (WHERE ...)) 같은 패턴은 raw에서는 매번 손으로 짜야 한다(헬퍼 만들면 줄어듦).

비교 — 같은 쿼리, 다른 추상화

도구줄 수읽기 난이도SQL 가시성타입 추론
Drizzle짧음쉬움자동
Kysely높음자동
Prisma가장 짧음가장 쉬움낮음자동
postgres.js가장 김SQL 의존가장 높음수동/Zod

8장 · 6축 비교 — 골라잡기 좋게

8.1 추상화 정도

도구추상화 정도
postgres.js거의 없음
KyselySQL 빌더
Drizzle스키마 + SQL 빌더
Prisma풀 ORM
MikroORM·TypeORMActive Record·Data Mapper

8.2 마이그레이션 도구

도구빌트인 마이그레이션자동 생성비고
Prismaprisma migrateO셰도우 DB 필요
Drizzledrizzle-kitO보수적 자동 생성
KyselyKysely 마이그레이션 + 외부 도구XAtlas·dbmate 권장
postgres.js없음XAtlas·Sqitch·Flyway 등

8.3 엣지 런타임 호환성 (Cloudflare Workers·Vercel Edge·Bun)

도구엣지 호환비고
Drizzle매우 좋음모든 어댑터가 엣지 우선
Kysely좋음드라이버 의존, 보통 잘 됨
postgres.js좋음Neon HTTP·Hyperdrive 결합 시 매우 좋음
Prisma좋음새 엔진 이후 안정
MikroORM·TypeORM보통~제한데코레이터·reflect-metadata 의존

8.4 번들 크기 (압축, 클라이언트 코어만)

대략적인 감(릴리즈에 따라 다름).

도구압축 후 크기(대략)
postgres.js매우 작음
Kysely작음
Drizzle작음
Prisma중간 (새 엔진 이후 큰 폭 축소)
MikroORM·TypeORM

8.5 SQL escape hatch

도구escape hatch매끄러움
postgres.js본인이 SQL최고
Kyselysql 템플릿매우 좋음
Drizzlesql 템플릿매우 좋음
PrismaTypedSQL·$queryRaw좋음(TypedSQL 이후 정상)
MikroORM·TypeORMraw query보통

8.6 멀티 DB 지원

도구PostgresMySQLSQLite기타
DrizzleOOOD1·LibSQL·Bun·Neon·Planetscale 외
KyselyOOO어댑터 다수
PrismaOOOSQL Server·MongoDB(제한)
postgres.jsOXXPostgres 전용

9장 · 실제 마이그레이션 후기 — Prisma → Drizzle / Kysely

9.1 Prisma → Drizzle (가장 흔한 경로)

이 경로를 택하는 팀이 늘었다. 이유는 두 가지.

  1. 엣지/번들 — 콜드 스타트와 응답 시간에서 차이가 보인다. 새 Prisma 엔진으로 격차는 줄었지만 0은 아니다.
  2. SQL escape hatch — sql 템플릿이 Prisma의 TypedSQL보다 자연스럽다. TypedSQL은 별도 .sql 파일이 필요하고, 동적 SQL에는 제약이 있다.

전형적인 마이그레이션 단계.

잘 안 되는 부분

9.2 Prisma → Kysely (SQL 팀의 경로)

DB 스키마 운영을 이미 Atlas·Sqitch 같은 도구로 하고 있는 팀, 또는 SQL을 그대로 짜고 싶은데 타입은 받고 싶은 팀이 자주 선택.

잘 안 되는 부분


10장 · MikroORM·TypeORM — 여전히 살아 있지만

MikroORM

엔티티 클래스 기반 Data Mapper ORM. identity map·unit of work·풍성한 관계 API가 강점. DDD 스타일 프로젝트나 도메인 객체 중심으로 짜는 팀이 좋아한다. SQL과 거리는 가장 멀다.

TypeORM

가장 오래된 TS ORM 중 하나. 데코레이터 기반 Active Record + Data Mapper. 유지보수가 일정치 않고 알려진 버그가 오래 남는 경향이 있어 신규 프로젝트의 첫 선택지는 아니다. 그러나 이미 TypeORM으로 짠 코드가 많으면 빨리 떼지 못한다.

신규 프로젝트에서 TypeORM을 자발적으로 고르는 경우는 2026년에 드물다. 이미 있는 코드베이스는 마이그레이션 비용·이득을 따로 따져야 한다.


11장 · 무엇을 언제 골라야 하나 — 솔직한 결론

자, 정답은? 항상 그렇듯 "상황 따라 다르다". 다만 패턴이 있다.

Drizzle을 고를 때

Kysely를 고를 때

Prisma를 고를 때

postgres.js·pg(raw)를 고를 때

안티 체크리스트 (피해야 할 패턴)


에필로그 — 추상화는 도구이지 종교가 아니다

ORM·쿼리 빌더 선택은 추상화 정도와 escape hatch 매끄러움의 트레이드오프다. 한쪽이 우월하지 않다. 다만 2026년 5월의 풍경은 분명하다.

결정 체크리스트

안티 패턴

  1. 측정 없이 ORM 갈아타기 — 결정은 측정 후에.
  2. 한 코드베이스에 ORM 둘 공존 — 종착지 정하고 마이그레이션 일정 잡기.
  3. ORM 쓰면서 $queryRaw 90% — ORM이 아니라 무거운 raw.
  4. 스키마 진실의 원천 두 개 — DB와 코드의 진실은 하나여야.
  5. 마이그레이션 자동화 없이 codegen — 타입이 거짓말한다.
  6. 엣지 우선인데 무거운 데코레이터 ORM — 빌드·런타임 둘 다 비용.
  7. 관계가 복잡한데 raw — 직접 짜야 할 SQL이 산더미.

다음 글 예고

다음 글 후보: Drizzle Relations v2 깊게 — json_agg·LATERAL join이 어떻게 만들어지는가, Prisma TypedSQL 한 달 써본 후기 — 어디에 쓰고 어디에 안 쓰나, Cloudflare Workers + Hyperdrive + postgres.js 풀스택 사례.

"SQL을 알면 모든 도구가 친절해진다. 모르면 어떤 도구도 마법이 아니다."

— TypeScript ORM·쿼리 빌더 2026, 끝.


참고 / References

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다