iBetter Books
수정

Ch 07. Prisma로 데이터베이스 연결하기

데이터베이스와 친해지는 가장 쉬운 방법

직접 SQL을 쓰는 것도 나쁘지 않습니다. 하지만 TypeScript 프로젝트에서 SQL 쿼리를 문자열로 작성하다 보면 타입 검사가 안 되고, 자동완성도 안 됩니다. Prisma는 이 문제를 해결합니다.

Prisma는 TypeScript 친화적인 ORM(Object-Relational Mapper)입니다. 데이터베이스 스키마를 Prisma Schema 언어로 정의하면, 타입이 완벽하게 정의된 클라이언트 코드를 자동으로 생성해줍니다.

설치

npm install prisma @prisma/clientnpx prisma init```text`prisma init` 명령이 실행되면 두 가지가 생성됩니다.- `prisma/schema.prisma`: 데이터베이스 스키마 파일- `.env`: 데이터베이스 연결 문자열이 들어갈 환경변수 파일### `prisma/schema.prisma` 작성데이터베이스 연결 정보와 모델(테이블)을 정의합니다.```prisma// 파일: prisma/schema.prismagenerator client {  provider = "prisma-client-js"}datasource db {  provider = "postgresql"  url      = env("DATABASE_URL")}model User {  id        String   @id @default(cuid())  email     String   @unique  name      String  createdAt DateTime @default(now())  posts     Post[]}model Post {  id          String   @id @default(cuid())  title       String  content     String  slug        String   @unique  published   Boolean  @default(false)  publishedAt DateTime?  createdAt   DateTime @default(now())  updatedAt   DateTime @updatedAt  author      User     @relation(fields: [authorId], references: [id])  authorId    String}```text### `.env` 파일에 데이터베이스 URL 설정```bash# 파일: .envDATABASE_URL="postgresql://username:password@localhost:5432/mydb?schema=public"```textSQLite를 사용하면 더 간단합니다. 파일 하나로 데이터베이스가 생깁니다.```bash# SQLite 사용 시DATABASE_URL="file:./dev.db"```textSQLite를 쓸 때는 `schema.prisma` `provider` 바꿔야 합니다.```prismadatasource db {  provider = "sqlite"  url      = env("DATABASE_URL")}```text### 마이그레이션 실행스키마를 실제 데이터베이스에 적용합니다.```bashnpx prisma migrate dev --name init```text 명령은 세 가지를 합니다.1. SQL 마이그레이션 파일 생성 (`prisma/migrations/`)2. 데이터베이스에 마이그레이션 적용3. Prisma Client 재생성### 싱글턴 패턴으로 Prisma Client 공유개발 환경에서는 파일을 저장할 때마다 모듈이 다시 로드됩니다. 그러면 `new PrismaClient()` 여러 번 호출되어 데이터베이스 연결이 너무 많이 생깁니다. 싱글턴 패턴으로 이를 방지합니다.```ts// 파일: lib/prisma.tsimport { PrismaClient } from '@prisma/client';const globalForPrisma = globalThis as unknown as {  prisma: PrismaClient | undefined;};export const prisma =  globalForPrisma.prisma ??  new PrismaClient({    log: process.env.NODE_ENV === 'development' ? ['query'] : [],  });if (process.env.NODE_ENV !== 'production') {  globalForPrisma.prisma = prisma;}```text이제 어디서든 `import { prisma } from '@/lib/prisma'` 같은 인스턴스를 사용합니다.### Server Component에서 Prisma로 데이터 조회```tsx// 파일: app/posts/page.tsximport { prisma } from '@/lib/prisma';export default async function PostsPage() {  // 출판된 게시글을 최신순으로 10개 가져옵니다  const posts = await prisma.post.findMany({    where: { published: true },    orderBy: { publishedAt: 'desc' },    take: 10,    include: {      author: {        select: { name: true }, // author의 name만 가져옵니다      },    },  });  return (    <main>      <h1>블로그</h1>      <ul>        {posts.map((post) => (          <li key={post.id}>            <h2>{post.title}</h2>            <p>작성자: {post.author.name}</p>            <time>              {post.publishedAt?.toLocaleDateString('ko-KR')}            </time>          </li>        ))}      </ul>    </main>  );}```text### Server Action에서 Prisma로 데이터 저장```ts// 파일: app/actions/post.ts'use server';import { prisma } from '@/lib/prisma';import { revalidatePath } from 'next/cache';import { redirect } from 'next/navigation';function createSlug(title: string): string {  return title    .toLowerCase()    .replace(/[^a-z0-9가-힣\s]/g, '')    .replace(/\s+/g, '-')    .trim();}export async function createPost(formData: FormData) {  const title = formData.get('title') as string;  const content = formData.get('content') as string;  if (!title?.trim() || !content?.trim()) {    throw new Error('제목과 내용을 입력해주세요.');  }  const slug = createSlug(title);  const post = await prisma.post.create({    data: {      title: title.trim(),      content: content.trim(),      slug,      published: true,      publishedAt: new Date(),      authorId: 'user-id-from-session', // 실제로는 세션에서 가져옵니다    },  });  revalidatePath('/posts');  redirect(`/posts/${post.slug}`);}```text### Prisma Studio로 데이터 확인```bashnpx prisma studio

브라우저에서 http://localhost:5555를 열면 데이터베이스의 데이터를 시각적으로 확인하고 편집할 수 있습니다. 개발 중 데이터를 직접 수정하거나 확인할 때 유용합니다.

이 파트를 마치며

데이터를 가져오고, 저장하고, 캐시를 관리하고, 실제 데이터베이스와 연결하는 방법까지 모두 배웠습니다. fetch의 캐시 옵션으로 렌더링 전략을 결정하고, Server Actions로 데이터를 변경하고, revalidateTag로 캐시를 갱신하는 패턴이 Next.js의 핵심 데이터 흐름입니다.

다음 파트에서는 인증을 다룹니다. 로그인한 사용자만 접근할 수 있는 페이지, 미들웨어로 요청을 가로채는 방법, NextAuth.js로 소셜 로그인을 구현하는 방법을 배웁니다.