Prisma + StackRender: 데이터베이스를 설계하고 백엔드 구축을 시작하세요

발행: (2026년 1월 4일 오전 04:38 GMT+9)
13 min read
원문: Dev.to

Source: Dev.to

번역할 텍스트가 제공되지 않았습니다. 번역이 필요한 전체 내용(코드 블록을 제외한 마크다운 텍스트)을 알려주시면 한국어로 번역해 드리겠습니다.

우리가 할 일

  • StackRender를 사용하여 PostgreSQL 데이터베이스를 시각적으로 설계합니다
  • 데이터베이스를 즉시 배포합니다
  • 스키마를 Prisma로 자동 가져옵니다
  • 백엔드 엔드포인트를 구축하고 테스트를 시작합니다

사전 요구 사항

  • Node.js 설치
  • PostgreSQL 데이터베이스 (로컬 또는 원격)
  • Prisma 설치 및 Node.js 프로젝트에 설정

Note: Prisma가 처음이라면, 계속하기 전에 공식 설정 가이드를 따라 주세요.

Source:

빈 프로젝트에서 시작하기

우리는 Prisma가 이미 설정된 새 Node.js 프로젝트로 시작합니다. schema.prisma 파일을 열면 모델도, 관계도, 정의된 것이 전혀 없는 완전한 빈 파일임을 알 수 있습니다. 동시에 연결된 PostgreSQL 데이터베이스도 비어 있습니다(테이블이나 제약조건이 없음). 바로 이 지점이 완벽한 시작점입니다.

generator client {
  provider = "prisma-client-js"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

StackRender를 사용해 스키마 설계하기

스키마를 직접 설계하는 대신 StackRender라는 무료 오픈소스 데이터베이스 스키마 생성기를 사용합니다.

  1. StackRender에서 새 데이터베이스 생성

    • 이름: ecommerce_db
    • 데이터베이스 유형: PostgreSQL 선택
  2. 빈 다이어그램이 표시됩니다. 여기서 스키마를 직접 설계하거나 기존 데이터베이스를 가져올 수 있습니다. 이번 예시에서는 AI가 초기 설계를 담당하도록 합니다.

  3. AI 프롬프트 – StackRender의 AI 프롬프트 기능에 다음을 입력합니다:

    Design a multi‑vendor ecommerce database
  4. 몇 초 안에 StackRender가 테이블, 필드, 관계를 포함한 전체 데이터베이스 다이어그램을 생성합니다. 모든 요소는 완전히 편집 가능하므로 테이블 이름을 바꾸고, 컬럼을 조정하고, 관계를 미세 조정한 뒤 진행할 수 있습니다.

  5. 스키마가 만족스러우면:

    • StackRender에서 Code 섹션을 엽니다
    • Export 버튼을 눌러 생성된 SQL 스크립트를 내보냅니다
  6. 내보낸 SQL 스크립트를 PostgreSQL 데이터베이스에 실행합니다(예: pgAdmin 사용). 실행이 완료되면 모든 테이블, 제약조건, 관계가 자동으로 생성됩니다. 이제 손으로 SQL을 작성하지 않아도 데이터베이스가 실시간으로 구축됩니다.

실시간 데이터베이스를 Prisma와 동기화하기

데이터베이스가 준비되었으니 스키마를 Prisma로 가져옵니다:

npx prisma db pull

Prisma가 데이터베이스를 introspect하여 기존 테이블과 관계를 기반으로 schema.prisma 파일을 자동으로 생성합니다. 아래는 결과 스키마의 일부(간략히 표시)입니다.

generator client {
  provider = "prisma-client-js"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model cart_items {
  id         Int       @id @default(autoincrement())
  cart_id    Int
  product_id Int
  quantity   Int
  added_at   DateTime? @default(now()) @db.Timestamptz(6)
  carts      carts     @relation(fields: [cart_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
  products   products  @relation(fields: [product_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}

model carts {
  id         Int          @id @default(autoincrement())
  user_id    Int          @unique
  created_at DateTime?    @default(now()) @db.Timestamptz(6)
  updated_at DateTime?    @default(now()) @db.Timestamptz(6)
  cart_items cart_items[]
  users      users        @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}

/* ... additional models omitted for brevity ... */

팁: 생성된 모델에는 모든 관계, 열거형, 기본값이 포함되어 있어 바로 사용할 수 있는 Prisma 클라이언트를 제공합니다.

다음 단계

  1. Prisma 클라이언트 생성

    npx prisma generate
  2. API 라우트를 구축 시작 (예: Express, Fastify, NestJS 사용)하고 ../src/generated/prisma에서 생성된 클라이언트를 가져옵니다.

  3. 실제 데이터베이스에 대한 통합 테스트 작성하여 엔드포인트를 검증합니다.

  4. 반복 – 스키마를 조정해야 하면 StackRender로 돌아가 다이어그램을 수정하고, SQL을 다시 내보낸 뒤 실행하고, npx prisma db pull을 다시 실행합니다.

요약

StackRender의 시각적 디자이너와 Prisma의 인스펙션을 결합하면 다음을 할 수 있습니다:

  • 수동 SQL 작성을 건너뛰세요
  • Prisma 모델을 데이터베이스와 완벽하게 동기화 상태로 유지하세요
  • “백엔드‑우선” 개발 사이클을 가속화하세요

다음 프로젝트에서 이 워크플로를 시도해 보고 설정 시간을 몇 시간 단축해 보세요!

Prisma 스키마 (정리됨)

model cart_items {
  id          Int      @id @default(autoincrement())
  cart_id     Int
  product_id  Int
  quantity    Int
  cart        carts    @relation(fields: [cart_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
  product     products @relation(fields: [product_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}

model categories {
  id          Int        @id @default(autoincrement())
  name        String
  description String?
  products    products[]
}

model order_items {
  id          Int      @id @default(autoincrement())
  order_id    Int
  product_id  Int
  quantity    Int
  price       Decimal  @db.Decimal(10, 2)
  orders      orders   @relation(fields: [order_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
  products    products @relation(fields: [product_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}

model orders {
  id               Int                         @id @default(autoincrement())
  user_id          Int
  total_amount     Decimal                     @db.Decimal(10, 2)
  currency         orders_currency_enum
  status           orders_order_status_enum   @default(pending)
  placed_at        DateTime?                  @default(now()) @db.Timestamptz(6)
  shipped_at       DateTime?                  @db.Timestamptz(6)
  delivered_at     DateTime?                  @db.Timestamptz(6)
  cancelled_at     DateTime?                  @db.Timestamptz(6)
  users            users                       @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
  order_items      order_items[]
  shipments        shipments?
  payments         payments?
}

model payments {
  id               Int                         @id @default(autoincrement())
  order_id         Int                         @unique
  amount           Decimal                     @db.Decimal(10, 2)
  method           String?
  status           payments_payment_status_enum @default(unpaid)
  transaction_id   String?                     @unique
  payment_date     DateTime?                   @default(now()) @db.Timestamptz(6)
  orders           orders                      @relation(fields: [order_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}

model product_images {
  id          Int      @id @default(autoincrement())
  product_id  Int
  url         String
  alt_text    String?
  products    products @relation(fields: [product_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}

model products {
  id              Int              @id @default(autoincrement())
  vendor_id       Int
  category_id     Int?
  name            String
  description     String?
  price           Decimal          @db.Decimal(10, 2)
  stock_quantity  Int
  sku             String?          @unique
  created_at      DateTime?        @default(now()) @db.Timestamptz(6)
  updated_at      DateTime?        @default(now()) @db.Timestamptz(6)
  cart_items      cart_items[]
  order_items     order_items[]
  product_images  product_images[]
  categories      categories?      @relation(fields: [category_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
  vendors         vendors          @relation(fields: [vendor_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
  reviews         reviews[]
}

model reviews {
  id          Int       @id @default(autoincrement())
  product_id  Int
  user_id     Int
  rating      Int
  comment     String?
  review_date DateTime? @default(now()) @db.Timestamptz(6)
  products    products  @relation(fields: [product_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
  users       users     @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}

model shipments {
  id                 Int                               @id @default(autoincrement())
  order_id           Int                               @unique
  shipment_date      DateTime?

Source:

@db.Timestamptz(6)
  tracking_number    String?                           @unique
  carrier            String?
  fulfillment_status shipments_fulfillment_status_enum @default(not_fulfilled)
  created_at         DateTime?                         @default(now()) @db.Timestamptz(6)
  updated_at         DateTime?                         @default(now()) @db.Timestamptz(6)
  orders             orders                            @relation(fields: [order_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}

model users {
  id               Int       @id @default(autoincrement())
  username         String    @unique
  email            String    @unique
  password_hash    String
  first_name       String?
  last_name        String?
  shipping_address String?
  billing_address  String?
  created_at       DateTime? @default(now()) @db.Timestamptz(6)
  updated_at       DateTime? @default(now()) @db.Timestamptz(6)
  carts            carts?
  orders           orders[]
  reviews          reviews[]
}

model vendor_payouts {
  id             Int       @id @default(autoincrement())
  vendor_id      Int
  payout_date    DateTime? @default(now()) @db.Timestamptz(6)
  amount         Decimal   @db.Decimal(10, 2)
  transaction_id String?   @unique
  vendors        vendors   @relation(fields: [vendor_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}

model vendors {
  id             Int              @id @default(autoincrement())
  name           String
  email          String           @unique
  phone_number   String?
  address        String?
  created_at     DateTime?        @default(now()) @db.Timestamptz(6)
  updated_at     DateTime?        @default(now()) @db.Timestamptz(6)
  order_items    order_items[]
  products       products[]
  vendor_payouts vendor_payouts[]
}

/* ---------- Enums ---------- */

enum orders_currency_enum {
  USD
  EUR
  GBP
}

enum orders_order_status_enum {
  pending
  processing
  shipped
  delivered
  cancelled
}

enum payments_payment_status_enum {
  unpaid
  paid
  refunded
}

enum shipments_fulfillment_status_enum {
  not_fulfilled
  fulfilled
  partially_fulfilled
}

Source:

간단한 백엔드 기능 (POST /product)

다음 예시는 POST 엔드포인트를 만드는 방법을 보여줍니다. 이 엔드포인트는:

  1. 클라이언트로부터 제품 데이터를 받습니다.
  2. Zod 로 페이로드를 검증합니다.
  3. Prisma 를 사용해 제품(및 이미지)을 영속화합니다.
  4. 새로 생성된 제품을 벤더, 카테고리, 이미지와 함께 반환합니다.

라우트 구현 (TypeScript)

import { Request, Response } from "express";
import { prisma } from "./prismaClient"; // adjust the import to your setup
import { createProductSchema, CreateProductImageInput } from "./schemas";

router.post("/product", async (request: Request, response: Response) => {
  try {
    // 1️⃣ Validate request body
    const result = createProductSchema.safeParse(request.body);
    if (!result.success) {
      response.status(400).json(result);
      return;
    }

    const { data } = result;

    // 2️⃣ Create product + nested images
    const product = await prisma.products.create({
      data: {
        ...data,
        product_images: {
          create: data.product_images.map(
            (productImage: CreateProductImageInput) => productImage
          ),
        },
      },
      include: {
        vendors: true,
        categories: true,
        product_images: true,
      },
    });

    // 3️⃣ Respond with the created entity
    response.status(200).json({ success: true, data: product });
  } catch (error) {
    console.error(error);
    response.status(500).json({ error, success: false });
  }
});

엔드포인트가 수행하는 작업

단계설명
수신제품과 그 이미지 배열을 설명하는 JSON 페이로드를 받습니다.
검증Zod (createProductSchema) 를 사용해 페이로드가 기대하는 형태인지 확인합니다.
영속화product_images 에 대한 중첩 create 와 함께 prisma.products.create 를 호출합니다.
반환관련 vendor, category, product_images 를 포함한 전체 제품 레코드를 반환합니다.

엔드‑투‑엔드 검증

  1. 초기 상태products 테이블이 비어 있습니다.
  2. 테스트 요청 전송 (예: Postman 또는 curl 사용) – 유효한 페이로드와 함께 요청을 보냅니다.
  3. 결과products 에 새 행이 추가되고, product_images 에도 해당 행이 생성됩니다.
  4. 응답 – API가 생성된 제품과 연관된 데이터를 함께 반환합니다.

이를 통해 다음이 확인됩니다:

  • Prisma 스키마가 데이터베이스에 올바르게 매핑되었습니다.
  • 생성된 Prisma 클라이언트가 기대대로 동작합니다.
  • 검증 레이어(Zod)가 DB에 도달하기 전에 잘못된 입력을 차단합니다.

Why This Workflow Is Valuable

  • Visual DB design – 다이어그램이나 스키마 파일로 시작합니다; 손으로 작성한 SQL이 필요 없습니다.
  • Automatic model generation – Prisma가 스키마를 읽어 타입‑안전 모델을 자동으로 생성합니다.
  • Rapid prototyping – 스키마에서 작동하는 엔드포인트까지 몇 분 안에 구현할 수 있습니다.
  • Consistency – 타입, 검증, 데이터베이스 제약 조건이 항상 동기화됩니다.

PrismaPostgreSQL으로 백엔드를 구축한다면, 시각적 설계 도구(예: StackRender)와 결합하여 설정 시간을 몇 시간, 심지어 며칠까지 단축할 수 있습니다.

리소스

행복한 코딩! 🚀

  • 웹사이트
  • Github
Back to Blog

관련 글

더 보기 »

NextJS (App Router)에서 데이터베이스 연결

소개 이 게시물에서는 Prisma ORM을 사용하여 Next.js 애플리케이션의 App Router를 데이터베이스에 연결하는 신뢰할 수 있는 방법을 살펴보겠습니다. Prisma ORM이란? Prisma O...