포스트

[이제와서 시작하는 Next.js 마스터하기 #10] 데이터베이스 연동 실전

[이제와서 시작하는 Next.js 마스터하기 #10] 데이터베이스 연동 실전

“풀스택 개발의 핵심!” - Prisma로 데이터베이스를 쉽고 안전하게!

🎯 이 글에서 배울 내용

  • Prisma ORM 설정
  • 스키마 정의와 마이그레이션
  • CRUD 작업
  • 관계형 데이터 모델링
  • 트랜잭션과 최적화

예상 소요 시간: 50분


📋 시작하기 전에: PostgreSQL 설치

Prisma를 사용하려면 먼저 데이터베이스가 필요합니다. 이 튜토리얼에서는 PostgreSQL을 사용합니다.

PostgreSQL이란?

PostgreSQL은 가장 인기 있는 오픈소스 관계형 데이터베이스입니다.

1
2
3
4
5
데이터베이스 = 데이터를 저장하는 창고 🏪
- 사용자 정보 저장
- 게시물 저장
- 댓글 저장
- 관계 설정 (사용자 → 게시물)

설치 방법

Windows 사용자

방법 1: 공식 설치 프로그램 (추천)

  1. PostgreSQL 다운로드
  2. 설치 파일 실행
  3. 설치 과정에서 비밀번호 설정 (잊지 마세요!)
  4. 포트: 5432 (기본값 유지)
  5. 설치 완료!

설치 확인:

1
2
3
# 명령 프롬프트에서
psql --version
# PostgreSQL 16.x 출력되면 성공!

방법 2: Docker 사용 (개발자 추천)

1
docker run --name postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres
macOS 사용자

방법 1: Homebrew (추천)

1
2
3
4
5
6
7
8
# PostgreSQL 설치
brew install postgresql@16

# 서비스 시작
brew services start postgresql@16

# 설치 확인
psql --version

방법 2: Postgres.app (GUI 앱)

  1. Postgres.app 다운로드
  2. Applications 폴더로 이동
  3. 앱 실행
  4. Initialize 버튼 클릭

방법 3: Docker

1
docker run --name postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres
Linux (Ubuntu/Debian) 사용자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# PostgreSQL 설치
sudo apt update
sudo apt install postgresql postgresql-contrib

# 서비스 시작
sudo systemctl start postgresql
sudo systemctl enable postgresql

# 설치 확인
psql --version

# PostgreSQL 사용자로 전환
sudo -i -u postgres

# psql 접속
psql

데이터베이스 생성

PostgreSQL 설치 후 개발용 데이터베이스를 만들어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
# psql 접속 (비밀번호 입력 필요)
psql -U postgres

# 데이터베이스 생성
CREATE DATABASE myapp_dev;

# 데이터베이스 목록 확인
\l

# 종료
\q

💡 Tip: Docker를 사용하면 설치 없이 바로 시작할 수 있습니다!

1
2
3
4
5
6
7
8
9
# Docker로 PostgreSQL 실행
docker run --name my-postgres \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -e POSTGRES_DB=myapp_dev \
  -p 5432:5432 \
  -d postgres

# 접속 확인
docker exec -it my-postgres psql -U postgres -d myapp_dev

연결 정보 확인

설치가 완료되면 다음 정보를 확인하세요:

1
2
3
4
5
호스트: localhost
포트: 5432
사용자: postgres
비밀번호: (설치 시 설정한 비밀번호)
데이터베이스: myapp_dev

데이터베이스 URL 형식:

1
2
3
4
postgresql://사용자:비밀번호@호스트:포트/데이터베이스명

예시:
postgresql://postgres:mysecretpassword@localhost:5432/myapp_dev

이 URL을 .env 파일에 DATABASE_URL로 저장할 것입니다!

🎯 빠른 시작 (Docker 추천)

Docker가 설치되어 있다면 가장 빠릅니다:

1
2
3
4
5
6
7
# 1. PostgreSQL 실행 (한 줄로!)
docker run --name nextjs-db -e POSTGRES_PASSWORD=password -e POSTGRES_DB=nextjs_dev -p 5432:5432 -d postgres

# 2. .env 파일에 추가
echo 'DATABASE_URL="postgresql://postgres:password@localhost:5432/nextjs_dev"' > .env

# 3. 완료! 이제 Prisma 사용 가능 🎉

🗃️ Prisma 설정

1. 설치

1
2
npm install prisma @prisma/client
npx prisma init

2. 환경 변수

# .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"

3. 스키마 정의

// prisma/schema.prisma
generator 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?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        String     @id @default(cuid())
  title     String
  content   String
  published Boolean    @default(false)
  author    User       @relation(fields: [authorId], references: [id])
  authorId  String
  tags      Tag[]
  comments  Comment[]
  createdAt DateTime   @default(now())
  updatedAt DateTime   @updatedAt
}

model Tag {
  id    String @id @default(cuid())
  name  String @unique
  posts Post[]
}

model Comment {
  id        String   @id @default(cuid())
  content   String
  post      Post     @relation(fields: [postId], references: [id])
  postId    String
  author    String
  createdAt DateTime @default(now())
}

4. 마이그레이션

1
2
3
4
5
# 마이그레이션 생성
npx prisma migrate dev --name init

# Prisma Client 생성
npx prisma generate

5. Prisma Client 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = global as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: ['query', 'error', 'warn'],
  });

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

✍️ CRUD 작업

Create (생성)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// app/actions/posts.ts
'use server';

import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
  const authorId = formData.get('authorId') as string;

  const post = await prisma.post.create({
    data: {
      title,
      content,
      author: {
        connect: { id: authorId }
      }
    },
    include: {
      author: true
    }
  });

  revalidatePath('/blog');
  return post;
}

Read (조회)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// app/blog/page.tsx
import { prisma } from '@/lib/prisma';

export default async function BlogPage() {
  const posts = await prisma.post.findMany({
    where: {
      published: true
    },
    include: {
      author: {
        select: {
          name: true,
          email: true
        }
      },
      _count: {
        select: {
          comments: true
        }
      }
    },
    orderBy: {
      createdAt: 'desc'
    },
    take: 10
  });

  return (
    <div className="p-8">
      <h1 className="text-4xl font-bold mb-8">블로그</h1>
      {posts.map(post => (
        <article key={post.id} className="mb-6 p-6 border rounded">
          <h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
          <p className="text-gray-600 mb-2">작성자: {post.author.name}</p>
          <p className="text-sm text-gray-500">
            댓글 {post._count.comments}</p>
        </article>
      ))}
    </div>
  );
}

Update (수정)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'use server';

export async function updatePost(id: string, formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  const post = await prisma.post.update({
    where: { id },
    data: {
      title,
      content,
      updatedAt: new Date()
    }
  });

  revalidatePath(`/blog/${id}`);
  return post;
}

Delete (삭제)

1
2
3
4
5
6
7
8
9
'use server';

export async function deletePost(id: string) {
  await prisma.post.delete({
    where: { id }
  });

  revalidatePath('/blog');
}

🔗 관계형 데이터

Many-to-Many 관계

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 포스트에 태그 추가
export async function addTagToPost(postId: string, tagName: string) {
  await prisma.post.update({
    where: { id: postId },
    data: {
      tags: {
        connectOrCreate: {
          where: { name: tagName },
          create: { name: tagName }
        }
      }
    }
  });

  revalidatePath(`/blog/${postId}`);
}

// 포스트의 태그 조회
const post = await prisma.post.findUnique({
  where: { id: postId },
  include: {
    tags: true
  }
});

💾 트랜잭션

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 여러 작업을 하나의 트랜잭션으로
export async function transferPost(postId: string, newAuthorId: string) {
  await prisma.$transaction(async (tx) => {
    // 1. 포스트 소유자 변경
    await tx.post.update({
      where: { id: postId },
      data: { authorId: newAuthorId }
    });

    // 2. 알림 생성
    await tx.notification.create({
      data: {
        userId: newAuthorId,
        message: `새로운 포스트가 할당되었습니다`
      }
    });

    // 3. 로그 기록
    await tx.activityLog.create({
      data: {
        action: 'TRANSFER_POST',
        postId,
        userId: newAuthorId
      }
    });
  });
}

⚡ 성능 최적화

1. 선택적 필드 가져오기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 나쁨: 모든 필드 가져오기
const user = await prisma.user.findUnique({
  where: { id }
});

// ✅ 좋음: 필요한 필드만
const user = await prisma.user.findUnique({
  where: { id },
  select: {
    id: true,
    name: true,
    email: true
  }
});

2. 병렬 쿼리

1
2
3
4
5
6
7
8
9
// ❌ 순차: 느림
const user = await prisma.user.findUnique({ where: { id } });
const posts = await prisma.post.findMany({ where: { authorId: id } });

// ✅ 병렬: 빠름
const [user, posts] = await Promise.all([
  prisma.user.findUnique({ where: { id } }),
  prisma.post.findMany({ where: { authorId: id } })
]);

3. 페이지네이션

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export async function getPosts(page: number = 1, pageSize: number = 10) {
  const skip = (page - 1) * pageSize;

  const [posts, total] = await Promise.all([
    prisma.post.findMany({
      skip,
      take: pageSize,
      orderBy: { createdAt: 'desc' }
    }),
    prisma.post.count()
  ]);

  return {
    posts,
    total,
    pages: Math.ceil(total / pageSize)
  };
}

🎯 오늘 배운 내용 정리

  1. Prisma 설정
    • 스키마 정의
    • 마이그레이션
  2. CRUD 작업
    • Create, Read, Update, Delete
    • 관계형 데이터
  3. 고급 기능
    • 트랜잭션
    • 성능 최적화

📚 시리즈 네비게이션


“데이터베이스 마스터하기!” 🗃️

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.