들어가기 앞서...
지난 포스트에선 Next.js에 대한 간단한 소개와 클라이언트에서 서버의 비즈니스 로직을 호출하는 방법에 대해 배웠습니다. 이번 포스트에선 Prisma ORM을 통해 DB에 접근해서 데이터를 관리하는 비즈니스 로직을 구현해보겠습니다.
실습에선 Postgresql DB를 사용합니다. DB는 docker를 통해 실행합니다. Docker Desktop을 준비해주세요.
Prisma ORM이란?
Prisma ORM은 TypeScript 생태계에서 DB 스키마를 정의하고 타입 안정성이 보장된 쿼리를 작성할 수 있도록 돕는 라이브러리 입니다. 주요 특징을 정리하면 아래와 같습니다.
- 직관적이고 간단한 스키마 정의
- 작성된 스키마를 바탕으로 TypeScript 타입 자동 생성
- 간편하고 안전한 마이그레이션
- 서버리스 환경 최적화
자세한 내용은 포스트를 진행하면서 설명하겠습니다.
설치 및 구성
grid-app 프로젝트 준비 및 Prisma 설치
먼저, 1편에서 만든 앱에 Prisma를 설치하겠습니다. 만약 이전 포스트를 스킵하신 경우, 아래의 저장소의 브랜치를 클론해서 시작해주세요.
- Github 저장소
- 클론한 다음 stage-1 브랜치로 이동하신 다음 작업해주세요!
준비되셨다면, 아래의 명령어를 프로젝트 루트 경로에서 실행해주세요.
1pnpm dlx prisma init
2실행하고 나면, prisma 폴더와 schema.prisma 파일이 생성된 것을 확인하실 수 있습니다.
Prisma 구성
아래의 명령어를 실행해서 필요한 패키지를 설치합니다.
1pnpm add prisma @types/node --save-dev
2pnpm add @prisma/client @prisma/adapter-pg dotenv
3prisma는 CLI 도구로, 초기화(init), 마이그레이션(migrate), 타입 생성(generate) 등의 명령어를 실행할 때 사용하는 패키지입니다.@prisma/client는 DB에 쿼리를 요청하는데 사용하는 패키지입니다. 비즈니스 로직은 해당 패키지를 통해 DB에 접근합니다.@prisma/adapter-pg는node-postgres드라이버 어댑터로, PrismaClient가 DB에 접속하는데 필요한 패키지입니다.dotenv는 환경변수를 로드하는데 필요한 패키지 입니다.
프로젝트 구성
package.json파일을 열고, 아래의 한 줄을 추가합니다.
1"type": "module",
2- Prisma는 ESM을 활성화 해야 동작하므로, 위와 같이 package.json을 설정해주어야 합니다.
개발용 DB 준비하기
프로젝트 루트 경로에 docker-compose.yml 파일을 생성하고, 아래 코드를 붙여넣습니다.
1services:
2 # Postgres 설정
3 # - https://hub.docker.com/_/postgres
4 grid-app-db:
5 image: postgres:14.5-alpine
6 ports:
7 - 5432:5432
8 environment:
9 POSTGRES_DB: grid-app
10 POSTGRES_USER: dev
11 POSTGRES_PASSWORD: dev
12그리고 도커 데스크톱 앱이 실행중인지 확인하고, docker compose up 명령어를 실행합니다.
그러면 Postgres 컨테이너가 실행되면서 DB를 이용할 수 있게 됩니다.
사용하지 않을땐 docker compose down 명령어를 실행하면 됩니다.
Prisma 설정
DB 접속 URL 설정
데이터베이스 접속을 위해, 필요한 설정을 등록해야 합니다.
먼저 prisma.config.ts를 열면, defineConfig 안에 아래와 같은 설정을 확인할 수 있습니다.
참고로 해당 파일은 아까 prisma init할 때 생성되었습니다.(아래에서 추가로 언급할 .env도요)
1datasource: {
2 url: process.env["DATABASE_URL"],
3},
4- 환경변수를 통해 DB주소를 불러오고 있습니다. 키 이름은 DATABASE_URL여야 하는걸 확인할 수 있네요.
- 이번엔 .env를 열어보겠습니다. DATABASE_URL가 있는 것을 확인할 수 있습니다.
- 이미 DB URL이 적혀있는데, 테스트용 DB의 URL로 바꿔보겠습니다.
1DATABASE_URL="postgresql://dev:dev@localhost:5432/grid-app"
2- 앞서 생성한 DB의 주소입니다. 이것만 입력하면 설정이 끝났습니다.
Prisma Studio로 접속 확인하기
- 이제 접속이 잘 되었는지 확인해보겠습니다. 아래의 명령어를 실행합니다.
1pnpm prisma studio
2- prisma는 데이터를 관리할 수 있는 studio라는 도구를 제공합니다.
- 실행하면 http://localhost:51212/ 주소로 웹페이지가 하나 열릴텐데, 에러 메시지가 표시되지 않는다면, 정상적으로 연결된 것을 의미합니다.
- 만약 실패했다면, DB 컨테이너가 켜져 있는지, 접속 URL에 문제가 없는지 확인해주세요.
엔티티 추가하기
이제 엔티티를 추가해보겠습니다. schema.prisma 파일을 열고 아래의 내용을 추가합니다.
1model Order {
2 id Int @id @default(autoincrement())
3 price Int
4 qty Int
5}
6- prisma는 자체 언어를 통해 엔티티를 정의합니다. 위의 경우, 자동 증가하는 id필드와 price, qty 필드를 정의했습니다.
- 이걸 DB와 동기화 시켜야 합니다. 아래의 명령어를 실행합니다.
1pnpm dlx prisma migrate dev --name init
2- 해당 명령어를 실행하면, init 이라는 이름의 마이그레이션이 생성되고, 현 상태의 스키마를 DB에 반영하기 위한 DDL문이 생성됩니다.
- 추후에 스키마가 변경되면, 마찬가지로 prisma migrate dev 명령을 통해 새로운 마이그레이션 파일을 생성하면 됩니다. 그럼 해당 변경을 DB와 동기화 하기 위한 DDL문이 자동생성됩니다.
- 아래의 코드는 init 마이그레이션을 통해 생성된 ddl문입니다. 보시다시피 Order 테이블을 생성하고 id, price, qty등의 컬럼을 생성하는것을 알 수 있습니다.
1-- CreateTable
2CREATE TABLE "Order" (
3 "id" SERIAL NOT NULL,
4 "price" INTEGER NOT NULL,
5 "qty" INTEGER NOT NULL,
6
7 CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
8);
9- 다시 studio로 돌아와보면, 두개의 테이블이 생성된 것을 알 수 있습니다.(
_prisma_migrations,Order) - Order는 우리가 앞서 만든 Order 모델에 대응하는 테이블입니다.
_prisma_migrations는 prisma에 의해 관리되는 테이블입니다. prisma는 해당 테이블을 통해 어떤 마이그레이션까지 이 DB에 적용되었는지를 나타냅니다.
Prisma Client 생성하기
스키마 파일을 기반으로 PrismaClient를 생성합니다. 앞서 Prisma Client는 DB에 쿼리를 전송할 때 사용하는 것이라고 했는데, generate 명령어를 통해 생성하게 됩니다.
1pnpm dlx prisma generate
2- 생성된 Prisma Client 코드는 schema.prisma 파일에서 지정한 경로에 생성됩니다.
- schema.prisma 파일을 확인해보겠습니다.
1generator client {
2 provider = "prisma-client"
3 output = "../app/generated/prisma"
4}
5../app/generated/prisma라고 되어 있으니, 프로젝트 루트 기준으로 app 폴더 밑에 generated라는 폴더가 생성됩니다.- 해당 폴더 안의 코드를 살펴보면, 다양한 코드가 생성되었는데, 그 중에서
../app/generated/prisma/models/Order.ts를 살펴보면 아래와 같은 코드를 찾을 수 있습니다.
1export type OrderCreateInput = {
2 price: number;
3 qty: number;
4};
5- 해당 타입은 PrismaClient를 통해 Order를 생성할 때 사용하는 인자의 타입인데, 앞서 schema 파일에서 정의한 것 처럼 필수 필드가 지정되어 있습니다.
- 그 외에도 where, select등의 다양한 쿼리에서 사용되는 타입을 제공하여, 강력한 타입 체킹으로 보호받으면서 DB 접근 코드를 작성할 수 있게 됩니다.
중간 정리
여기까지 해서 prisma를 설치하고 개발용으로 설정한 DB 컨테이너와 연결하는 방법, 그리고 엔티티를 추가하고 DB와 동기화 시키는 방법에 대해 알아보았습니다. 이어서 PrismaClient를 통해 CRUD를 하는 방법에 대해 알아보겠습니다.
PrismaClient를 통해 CRUD 구현하기
지난 실습에서 생성한 더미코드(server/service)를 PrismaClient로 구현해보겠습니다.
PrismaClient 인스턴스 관리
먼저, PrismaClient 인스턴스를 생성하고 관리하는 코드부터 작성해야 합니다. server/lib/prisma.ts파일을 생성하고 아래와 같이 입력합니다.
1import { PrismaClient } from '../../app/generated/prisma/client';
2import { PrismaPg } from '@prisma/adapter-pg';
3
4const globalForPrisma = global as unknown as {
5 prisma: PrismaClient;
6};
7
8// Postgres Adapter 객체 생성 및 PrismaClient에 적용
9const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
10const prisma = globalForPrisma.prisma || new PrismaClient({ adapter });
11
12// next 개발모드에선 모듈 핫 리로드(HMR)시 모듈이 재실행되므로, 아래와 같이 설정하여 이전에 생성한 클라이언트를 재사용한다.
13// 이렇게 안하면 모듈 재실행될 때 마다 새 커넥션풀을 생성하여, 커넥션이 고갈된다.
14if (process.env.NODE_ENV !== 'production') {
15 globalForPrisma.prisma = prisma;
16}
17
18export default prisma;
19- 모듈은 한번만 실행되는 점을 이용하여, PrismaClient 인스턴스를 생성하는 코드입니다.
- 특이한 점은
if (process.env.NODE_ENV !== "production") {...}이 부분인데, 이건 개발환경을 위한 설정입니다. - 개발환경에선 코드를 수정할때마다 모듈이 핫 리로드 됩니다. 만약 이 때마다 클라이언트를 재생성한다면, 커넥션이 금방 고갈됩니다. 이는 클라이언트 인스턴스가 생성될 때 마다 커넥션풀을 생성하기 때문입니다.
- 이러한 문제를 방지하기 위해 개발모드일때만 전역 객체에 캐싱해두고 다시 재사용하는 것 입니다.
Create 구현
server/service/create-order.ts를 아래와 같이 수정합니다.
1import prisma from '../lib/prisma';
2
3export interface CreateOrderInput {
4 price: number;
5 qty: number;
6}
7
8export async function createOrder({ price, qty }: CreateOrderInput) {
9 const newOrder = await prisma.order.create({
10 data: { price, qty },
11 });
12
13 return newOrder;
14}
15정말 간단하게 위와 같이 작성하면 끝입니다. 또한 타입이 지원되기 때문에, IDE에서 자동완성도 지원되고, 오타가 발생한 경우에도 컴파일 시점에 실수를 미리 인지하고 고칠 수 있기에 더 안전한 코딩이 가능합니다.
이어서 다른 비즈니스 로직도 구현해보겠습니다.
Read
server/service/get-orders.ts파일을 아래와 같이 수정합니다.
1import prisma from '../lib/prisma';
2
3export async function getOrders() {
4 return prisma.order.findMany();
5}
6지금은 모든 order를 조회하므로 findMany에 인자를 넘기지 않지만, 만약 필터링을 추가해야 한다면 아래와 같이 작성할 수 있습니다. 아래는 예시입니다. 눈으로만 봐주세요.
1prisma.order.findMany({
2 where: {
3 price: { gt: 1000 },
4 qty: { gt: 5 },
5 },
6 skip: 20,
7 take: 20,
8});
9- where 조건을 걸 수 있으며, and 조건을 걸어야 하는 경우, 위와 같이 where문을 설정하고, 각 필드별로 비교식을 넣을 수 있습니다.
물론 타입 지원이 완벽하게 됩니다. - skip을 통해 무시할 행 개수를 지정하고, take를 통해 가져올 최대 행 개수를 지정할 수 있습니다. 이는 SQL의 Offset과 Limit에 대응합니다. 페이지네이션을 구현할 때 유용합니다.
- 이 뿐만 아니라 aggregate, cursor도 지원하니, prisma 공식 사이트를 참고해주세요.
쿼리 최적화나 복잡한 한방 쿼리는 PrismaClient로 구현하기 어려운 경우가 있습니다. 이 경우엔, native query 기능을 통해 직접 작성한 SQL을 실행하는 방법도 있으니, 알아두시면 좋습니다.
Update & Delete
비슷한 패턴으로, 수정, 삭제 API를 구현해보겠습니다. 먼저 server/service/update-order.ts를 만들어주세요. 그리고 아래와 같이 작성합니다.
1import prisma from '../lib/prisma';
2
3export interface UpdateOrderInput {
4 id: number;
5 price?: number;
6 qty?: number;
7}
8
9export async function updateOrder({ id, price, qty }: UpdateOrderInput) {
10 return prisma.order.update({
11 where: { id },
12 data: { price, qty },
13 });
14}
15- 어떤 Order 한 건의 데이터를 수정하는 로직입니다.
- 어떤 Order를 수정할지 지정하기 위해 where절을 작성했습니다.
- 이 때, price, qty는 optional입니다. 특정 필드를 비운 상태로 updateOrder를 호출하는 경우, prisma는 해당 필드를 수정하지 않습니다.
이번에는 삭제를 구현해보겠습니다. server/service/delete-order.ts를 만들고 아래와 같이 작성해주세요.
1import prisma from '../lib/prisma';
2
3export async function deleteOrder(id: number) {
4 return prisma.order.delete({
5 where: { id },
6 });
7}
8- 역시 어떤 Order를 삭제할지를 지정하기 위해 where 절을 작성했습니다.
남은 서버액션 구현하기
- 지금 추가한 비즈니스 로직 중, update와 delete는 클라이언트 컴포넌트에서 호출할 수 있어야 합니다. 이를 위해 미리 서버액션을 만들어놓겠습니다.
- server/action 폴더 밑에 아래의 파일을 작성해주세요.
server/action/update-order-action.ts
1'use server';
2
3import { updateOrder, UpdateOrderInput } from '../service/update-order';
4
5export async function updateOrderAction(input: UpdateOrderInput) {
6 return updateOrder(input);
7}
8server/action/delete-order-action.ts
1'use server';
2
3import { deleteOrder } from '../service/delete-order';
4
5export async function deleteOrderAction(orderId: number) {
6 return deleteOrder(orderId);
7}
8확인해보기
- order-grid.tsx 파일을 열면, createOrderAction을 호출하는 코드에서 오류가 발생하는 것을 알 수 있습니다.
- 더 이상 해당 함수가 id를 인자로 받지 않기 때문입니다. 아래와 같이 수정해줍니다.
1const newOrder = await createOrderAction({
2 price: 5000,
3 qty: 3,
4});
5pnpm dev를 실행하여 next 앱을 켜면 아무것도 나오지 않습니다. 아직 데이터가 없어서 그렇습니다.- Create Order 버튼을 세번 누르고, 페이지를 새로고침해보면, Order 데이터가 세건 조회되는 것을 확인할 수 있습니다.
- prisma studio에서 확인해보면 실제 DB에 데이터가 추가된 것을 알 수 있습니다.
마치며...
여기까지 하여, PrismaORM을 활용한 CRUD를 전부 구현해보았습니다.
이번 포스트에선 Prisma ORM에 대한 간단한 소개와 설치 및 구성방법, 그리고 간단한 CRUD 코드 작성을 알아보았습니다.
개인적으로 타입 스크립트 진영의 DB 라이브러리 중에선 Prisma가 가장 문서화가 잘 되어 있고, 커뮤니티 규모도 크며 타입 안정성이나 성능, 사용성이 제일 좋지 않나 라고 생각합니다.
또한 마이그레이션 방법도 쉽고 직관적이어서, 유지보수할 때 큰 어려움 없이 작업할 수 있었습니다.
이번 포스트까지 작업된 전체 코드는 아래의 링크에서 확인하실 수 있습니다.
- https://github.com/Malloc72P/next-grid-app/tree/stage-2
- stage-2 브랜치를 확인해주세요
다음 포스트에선 AG Grid를 사용해서 화면을 구현해보려고 합니다.
긴 글 읽어주셔서 감사합니다.