Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Node.js/TypeScript용 ORM Prisma 살펴보기

데이터베이스를 다루는 프로젝트를 하려면 ORM(혹은 SQL 매퍼)같은 라이브러리가 필수다. SQL을 직접 다뤄도 되기는 하지만 쓰기도 어렵고 유지보수도 어렵기 때문에 ORM 같은 라이브러리를 쓰는 게 일반적이다. JavaScript로 프로젝트를 하면서 SequelizeBookshelf.js도 써봤고 MongoDB를 쓸 때는 mongoose또 사용해봤다.

최근 몇 년간에는 JavaScript로 데이터베이스를 크게 다룰 일이 많지 않아서 사실 ORM을 써본 지가 오래되었고 간단한 토이 프로젝트에서는 쉽게 사용할 수 있는 nedbpouchdb같은 JavaScript 데이터베이스를 더 많이 사용했다. 간단히 저장했다 꺼내 쓸 수 있으면서 성능을 걱정할 만큼 많은 데이터가 필요한 건 아니라서 그냥 쓰고 있었는데 현재 일하는 프로젝트에서 데이터베이스를 다루기 위해서 동료가 Prisma를 도입해서 실제로 사용해 보게 되었고 나쁘진 않은 것 같아서 (공부 겸) 내 사이드 프로젝트에도 도입하고 있다. (TypeORM 등 다양한 대안과 내가 세밀하게 비교해 본 것은 아니다.)

Prisma ORM

Prisma 홈페이지

사이트에 나온 대로 Prisma는 Node.js와 TypeScript용 ORM으로 현재 PostgreSQL, MySQL, MiariaDB, SQL Server, SQLite, CockroachDB, MongoDB 등 다양한 데이터베이스를 지원하고 있다. Prisma에서 데이터 플랫폼을 유료로 서비스하고 있지만 Prisma ORM 자체는 오픈소스라서 무료로 이용하고 있다. 데이터 플랫폼은 안써봤지만 설명을 보면 협업을 도와주는 기능과 서비스 프락시 등을 제공하는 걸로 보인다.

Prisma 프로젝트 설정

Node.js와 TypeScript 모두에서 사용할 수 있지만 여기서는 TypeScript를 기준으로 사용해 보자. 일단 package.json이 있는 TypeScript 프로젝트에 TypeScript 의존성을 추가하자.

$ npm install typescript ts-node @types/node --save-dev

TypeScript 컴파일을 위한 tsconfig.json 파일을 다음이 내용을 추가한다.

{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "esModuleInterop": true
  }
}

이제 prisma 의존성을 설치한다.

$ npm install prisma --save-dev

설치된 바이너리는 ./node_modules/.bin/prisma에 있으므로 버전 등을 확인해 볼 수 있다.

$ ./node_modules/.bin/prisma -v
prisma                  : 4.2.1
@prisma/client          : Not found
Current platform        : darwin-arm64
Query Engine (Node-API) : libquery-engine 2920a97877e12e055c1333079b8d19cee7f33826 (at node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node)
Migration Engine        : migration-engine-cli 2920a97877e12e055c1333079b8d19cee7f33826 (at node_modules/@prisma/engines/migration-engine-darwin-arm64)
Introspection Engine    : introspection-core 2920a97877e12e055c1333079b8d19cee7f33826 (at node_modules/@prisma/engines/introspection-engine-darwin-arm64)
Format Binary           : prisma-fmt 2920a97877e12e055c1333079b8d19cee7f33826 (at node_modules/@prisma/engines/prisma-fmt-darwin-arm64)
Default Engines Hash    : 2920a97877e12e055c1333079b8d19cee7f33826
Studio                  : 0.469.0

이제 Prisma를 초기화해보자. prisma init 명령어로 초기화를 하는 데 테스트로는 PostgreSQL을 사용했다.

$ ./node_modules/.bin/prisma init --datasource-provider postgresql

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

위 안내메시지처럼 prisma/schema.prisma라는 스키마 파일이 생성되었고 기본으로 DATABASE_URL 환경변수를 사용하기 때문에 .env 파일도 생성되었다.

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

.env는 사용하지 않고 있고 환경변수만 제공하면 되는 것이므로 .env 파일은 삭제하고 따로 환경변수를 제공했다.

Prisma schema

앞의 과정에서 생성된 prisma/schema.prisma 파일은 다음의 내용이 포함되어 있다. 이 파일은 Prisma Schema Language(PSL)로 작성된다고 한다. 하지만 PSL에 대한 문서는 못 찾았고 스키마 문서에서 문법의 사용 방법을 볼 수 있다.

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator는 스키마에 기반해서 Prisma 클라이언트를 만들어 준다. 여기서는 postgresql을 데이터베이스 프로바이더로 지정했으므로 당연히 여기에 맞는 클라이언트를 만들어 줄 것이고 실제 데이터를 다룰 때는 이 클라이언트를 사용한다. 문서에 따르면 데이터베이스 스키마를 이용하는 다른 제너레이터도 이용할 수 있다고 한다.

datasource는 Prisma가 데이터베이스에 어떻게 연결하는지를 다룬다. 데이터베이스 URL은 시크릿 정보가 많으므로 env()라는 함수를 제공해 주어 환경변수(여기서는 DATABASE_URL)에서 읽어올 수 있다. 스키마는 단 하나의 데이터소스만 가질 수 있다.

스키마에 모델 정의하기

방금 살펴본 prisma/schema.prisma에 데이터베이스 모델을 정의할 수 있다.

generator client {} // 생략

datasource db {} // 생략

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}

사용자 테이블을 추가한다고 하면 위처럼 추가할 수 있다. 데이터베이스를 사용해 봤다면 문법은 모르더라도 대충 이해는 할 수 있다. 각 데이터베이스에서 사용할 수 있는 타입과 타입 속성은 데이터베이스 커넥터에서 볼 수 있고 각 데이터베이스의 특성이 있기 때문에 해당 문서를 잘 살펴봐야 한다.

모델을 정의했으니 이를 데이터베이스에 적용해야 하는데 먼저 데이터베이스를 준비하자.

$ docker run --name postgres -p 5432:5432 \
  -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres \
  postgres

로컬에 데이터베이스를 설치하는 건 번거롭기 때문에 Docker로 PostgreSQL을 띄웠다. 로컬 PostgreSQL의 설정대로 DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" 환경변수를 지정했다.

정의한 모델을 데이터베이스에 적용할 때는 prisma migrate dev 명령어를 사용한다. --name 플래그는 이번 마이그레이션의 이름으로 기록을 봤을 때 이해할 수 있는 이름을 지정하면 된다. prisma migrate dev 명령어는 개발하면서 개발환경에서 데이터베이스를 마이그레이션 하는 용도이고 프로덕션은 다른 과정이 필요한 것 같은데 이건 따로 더 살펴볼 예정이다.

$ ./node_modules/.bin/prisma migrate dev --name init
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"

Applying migration `20220815094441_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20220815094441_init/
    └─ migration.sql

Your database is now in sync with your schema.

Running generate... (Use --skip-generate to skip the generators)

added 2 packages, and audited 24 packages in 2s

found 0 vulnerabilities

✔ Generated Prisma Client (4.2.1 | library) to ./node_modules/@prisma/client in 46ms

위 결과에 나온 대로 prisma/migrations 폴더 아래 마이그레이션 상황을 기록한 파일이 생기고 폴더 이름에 마이그레이션 한 시간과 앞에서 지정한 init이라는 이름이 나와 있는걸 볼 수 있다.(Generated Prisma Client 부분도 있는데 이건 뒤에서 다시 얘기하겠다.)

prisma
├── migrations
│   ├── 20220815094441_init
│   │   └── migration.sql
│   └── migration_lock.toml
└── schema.prisma

migration.sql을 열어보면 마이그레이션을 위해 사용한 SQL 문을 볼 수 있다.

-- CreateTable
CREATE TABLE "User" (
    "id" SERIAL NOT NULL,
    "email" TEXT NOT NULL,
    "name" TEXT,

    CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

PostgreSQL에 접속해서 확인해 보면 당연히 User 테이블이 생성되었고 _prisma_migrations라는 마이그레이션 상황을 기록하는 테이블도 추가된 것을 볼 수 있다.

postgres=# \d
                 List of relations
 Schema |        Name        |   Type   |  Owner
--------+--------------------+----------+----------
 public | User               | table    | postgres
 public | User_id_seq        | sequence | postgres
 public | _prisma_migrations | table    | postgres
(3 rows)

postgres=# SELECT * FROM _prisma_migrations;
                  id                  |                             checksum                             |          finished_at|   migration_name    | logs | rolled_back_at |          started_at           | applied_steps_count
--------------------------------------+------------------------------------------------------------------+-------------------------------+---------------------+------+----------------+-------------------------------+---------------------
 d12910d5-1d0e-46af-9b82-cc61c9823cac | 74321ef4af21dd277012df04272490dac49483b673162e05f491e8db020c4494 | 2022-08-15 09:44:41.071292+00 | 20220815094441_init |      |                | 2022-08-15 09:44:41.058691+00 |                   1
(1 row)

마이그레이션 상태는 prisma migrate status 명령어로도 확인해 볼 수 있다.

./node_modules/.bin/prisma migrate status
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"

1 migration found in prisma/migrations


Database schema is up to date!


데이터베이스 사용

이제 테이블도 준비되었으니 데이터를 추가하기 위해 다음과 같은 add-user.ts 파일을 만들어 보자.

// add-user.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function add() {
  const user = await prisma.user.create({
    data: {
      name: 'Alice',
      email: 'alice@prisma.io',
    },
  })
  console.log(user)
}

add()
  .then(() => {
    console.log('added')
  })
  .catch((err) => {
    console.error(err)
    process.exit(1)
  })

@prisma/client에서 PrismaClient 클라이언트를 임포트했다. 이 부분에서 "응?"하는 분도 있을 수 있는데 앞에서 prismanpm install로 설치했지만 @prisma/client를 따로 설치한 적은 없는데 여기서 임포트를 했고 npm ls로도 @prisma/client가 설치된 것으로 나온다.

$ npm ls
prisma-demo@1.0.0 /Users/outsider/prisma-demo
├── @prisma/client@4.2.1
├── @types/node@18.7.3
├── prisma@4.2.1
├── ts-node@10.9.1
└── typescript@4.7.4

앞에서 prisma migrate dev를 실행했을 때 Generated Prisma Client (4.2.1 | library) to ./node_modules/@prisma/client in 46ms라는 부분이 출력되었다. 이 클라이언트는 npm install로 추가되는 게 아니라 실제로 내가 정의한 데이터베이스 스키마에 따라(여기서는 User 모델을 가진) 생성되는 클라이언트이기 때문에 마이그레이션 할 때 자동으로 생성되고 마치 설치한 모듈처럼 node_modules 밑에 추가된다. 그래서 위에서 설치는 안 했지만 임포트해서 사용할 수 있는 것이다. 이는 스키마를 변경하면 클라이언트도 새로 생성해야 한다는 의미가 되고 보통 node_modules는 Git에 추가하지 않기 때문에 배포나 패키징을 할 때 클라이언트도 생성되도록 관리가 필요하다는 의미가 된다.

클라이언트만 생성하고 싶다면 prisma generate 명령어를 사용하면 된다.

이 클라이언트가 스키마를 이해하고 있기 때문에 prisma.user.create()처럼 사용할 수 있다. 추가적인 쿼리는 모델 쿼리 문서에서 찾을 수 있다.

이 TypeScript 코드를 실행하면 사용자가 잘 추가되는 것을 볼 수 있다.

$ ./node_modules/.bin/ts-node add-user.ts
{ id: 1, email: 'alice@prisma.io', name: 'Alice' }
added
2022/08/15 19:35 2022/08/15 19:35