데이터베이스 연동 기초 (예: SQLite)
간단한 데이터베이스를 Next.js 프로젝트에 연동하는 기초를 다룹니다.
Next.js 기초 입문 22회차: 데이터베이스 연동 기초 (SQLite)
안녕하세요! Next.js 기초 입문 과정의 스물두 번째 시간입니다. 이번 회차에서는 웹 애플리케이션의 핵심 요소 중 하나인 데이터베이스 연동에 대해 학습합니다. 특히, 가볍고 설정이 용이한 SQLite를 Next.js 프로젝트에 연동하는 기초적인 방법을 다루며, ORM(Object-Relational Mapping)을 활용하여 데이터를 효율적으로 관리하는 방법을 이해하는 데 중점을 둡니다.
📌 이번 회차 학습 목표
- Next.js 프로젝트에 SQLite 데이터베이스를 설정하고 초기화할 수 있습니다.
- ORM의 개념과 필요성을 이해하고, Prisma와 같은 ORM 도구를 사용할 수 있습니다.
- 데이터베이스 스키마를 정의하고 마이그레이션을 수행할 수 있습니다.
- Next.js API 라우트를 통해 데이터베이스의 데이터를 생성, 조회, 수정, 삭제할 수 있습니다.
- 프론트엔드에서 API를 호출하여 데이터베이스와 상호작용하는 방법을 이해합니다.
📝 개념 설명
1. 데이터베이스와 웹 애플리케이션
웹 애플리케이션은 사용자에게 동적인 콘텐츠를 제공하기 위해 데이터를 저장하고 관리해야 합니다. 이러한 데이터를 체계적으로 저장하고 효율적으로 접근하기 위한 시스템이 바로 데이터베이스(Database)입니다. 데이터베이스는 사용자 정보, 게시물, 상품 정보 등 다양한 종류의 데이터를 보관하며, 웹 애플리케이션은 이 데이터베이스와 상호작용하여 정보를 읽고 쓰는 작업을 수행합니다.
2. SQLite란?
SQLite는 서버 없이 동작하는 파일 기반의 경량 관계형 데이터베이스 관리 시스템(RDBMS)입니다. 별도의 데이터베이스 서버를 설치하거나 관리할 필요 없이, 단일 파일로 모든 데이터베이스 정보를 저장합니다. 이러한 특성 때문에 개발 초기 단계나 소규모 프로젝트, 임베디드 시스템 등에서 매우 유용하게 사용됩니다. Next.js 프로젝트에서는 개발 환경에서 빠르고 쉽게 데이터베이스 기능을 구현하는 데 적합합니다.
3. ORM(Object-Relational Mapping)이란?
ORM(Object-Relational Mapping)은 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑(연결)하는 기술입니다. ORM을 사용하면 SQL 쿼리를 직접 작성하는 대신, 프로그래밍 언어의 객체와 메서드를 사용하여 데이터베이스를 조작할 수 있습니다. 이는 개발 생산성을 높이고, 데이터베이스 종류에 따른 코드 변경을 최소화하며, SQL 인젝션과 같은 보안 취약점을 줄이는 데 도움을 줍니다.
4. Prisma ORM 소개
Next.js 생태계에서 널리 사용되는 ORM 중 하나는 Prisma입니다. Prisma는 데이터베이스 스키마 정의, 마이그레이션, 타입스크립트 지원, 강력한 쿼리 빌더 등 다양한 기능을 제공하여 개발자가 데이터베이스 작업을 효율적으로 수행하도록 돕습니다. 이번 회차에서는 Prisma를 사용하여 SQLite 데이터베이스를 연동하는 방법을 실습합니다.
💡 예제 & 실습: Next.js와 SQLite 연동하기
다음 단계에 따라 Next.js 프로젝트에 SQLite 데이터베이스를 연동하고 간단한 CRUD(Create, Read, Update, Delete) 작업을 수행해 보겠습니다.
단계 1: Next.js 프로젝트 생성 및 Prisma 설치
먼저 새로운 Next.js 프로젝트를 생성하고 Prisma를 설치합니다.
npx create-next-app@latest my-database-app --typescript --eslint
cd my-database-app
npm install prisma --save-dev
npx prisma init --datasource-provider sqlite
npx prisma init --datasource-provider sqlite 명령어는 prisma 디렉토리와 schema.prisma 파일을 생성하고, SQLite 데이터베이스를 사용하도록 설정합니다.
단계 2: 데이터베이스 스키마 정의
prisma/schema.prisma 파일을 열어 데이터베이스 모델을 정의합니다. 우리는 간단한 ‘Post’ 모델을 생성하여 게시물을 저장해 보겠습니다.
// prisma/schema.prisma
datasource db {
provider = 'sqlite'
url = env('DATABASE_URL')
}
generator client {
provider = 'prisma-client-js'
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post는 id, title, content, published, createdAt, updatedAt 필드를 가집니다. 각 필드의 타입과 속성(예: @id, @default)을 정의합니다.
단계 3: 마이그레이션 및 Prisma Client 생성
스키마를 정의했으면, 이를 실제 데이터베이스에 반영하고 Prisma Client를 생성해야 합니다.
npx prisma migrate dev --name init
이 명령어는 다음을 수행합니다:
prisma/migrations디렉토리에 마이그레이션 파일을 생성합니다.- SQLite 데이터베이스 파일(기본적으로
prisma/dev.db)을 생성하고,Post테이블을 만듭니다. - Prisma Client를 생성하여 코드에서 데이터베이스에 접근할 수 있도록 합니다.
단계 4: Prisma Client 인스턴스 생성
Next.js 애플리케이션에서 Prisma Client를 사용하기 위해, 전역적으로 사용할 수 있는 인스턴스를 생성합니다. lib/prisma.ts 파일을 생성합니다.
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!global.prisma) {
global.prisma = new PrismaClient()
}
prisma = global.prisma
}
export default prisma
이 코드는 개발 환경에서 핫 리로딩 시 Prisma Client 인스턴스가 여러 개 생성되는 것을 방지합니다.
단계 5: API 라우트를 이용한 데이터베이스 CRUD 작업
Next.js의 API 라우트를 사용하여 데이터베이스와 상호작용하는 엔드포인트를 만듭니다.
게시물 생성 (Create)
pages/api/posts.ts 파일을 생성하여 새 게시물을 생성하는 API를 만듭니다.
// pages/api/posts.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../lib/prisma'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const { title, content } = req.body
try {
const post = await prisma.post.create({
data: {
title,
content,
published: false,
},
})
res.status(201).json(post)
} catch (error) {
res.status(500).json({ message: 'Error creating post', error })
}
} else {
res.setHeader('Allow', ['POST'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
모든 게시물 조회 (Read)
위 pages/api/posts.ts 파일에 GET 요청을 처리하는 로직을 추가하여 모든 게시물을 조회합니다.
// pages/api/posts.ts (GET 요청 핸들러 추가)
import type { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../lib/prisma'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
// ... (기존 POST 로직)
} else if (req.method === 'GET') {
try {
const posts = await prisma.post.findMany()
res.status(200).json(posts)
} catch (error) {
res.status(500).json({ message: 'Error fetching posts', error })
}
} else {
res.setHeader('Allow', ['POST', 'GET'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
단일 게시물 조회 (Read by ID)
pages/api/posts/[id].ts 파일을 생성하여 특정 ID의 게시물을 조회합니다.
// pages/api/posts/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../lib/prisma'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const postId = req.query.id as string
if (req.method === 'GET') {
try {
const post = await prisma.post.findUnique({
where: { id: Number(postId) },
})
if (post) {
res.status(200).json(post)
} else {
res.status(404).json({ message: 'Post not found' })
}
} catch (error) {
res.status(500).json({ message: 'Error fetching post', error })
}
} else {
res.setHeader('Allow', ['GET'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
게시물 수정 (Update)
pages/api/posts/[id].ts 파일에 PUT 요청을 처리하는 로직을 추가하여 게시물을 수정합니다.
// pages/api/posts/[id].ts (PUT 요청 핸들러 추가)
import type { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../lib/prisma'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const postId = req.query.id as string
if (req.method === 'GET') {
// ... (기존 GET 로직)
} else if (req.method === 'PUT') {
const { title, content, published } = req.body
try {
const updatedPost = await prisma.post.update({
where: { id: Number(postId) },
data: {
title,
content,
published,
},
})
res.status(200).json(updatedPost)
} catch (error) {
res.status(500).json({ message: 'Error updating post', error })
}
} else {
res.setHeader('Allow', ['GET', 'PUT'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
게시물 삭제 (Delete)
pages/api/posts/[id].ts 파일에 DELETE 요청을 처리하는 로직을 추가하여 게시물을 삭제합니다.
// pages/api/posts/[id].ts (DELETE 요청 핸들러 추가)
import type { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../lib/prisma'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const postId = req.query.id as string
if (req.method === 'GET') {
// ... (기존 GET 로직)
} else if (req.method === 'PUT') {
// ... (기존 PUT 로직)
} else if (req.method === 'DELETE') {
try {
await prisma.post.delete({
where: { id: Number(postId) },
})
res.status(204).end()
} catch (error) {
res.status(500).json({ message: 'Error deleting post', error })
}
} else {
res.setHeader('Allow', ['GET', 'PUT', 'DELETE'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
단계 6: 프론트엔드에서 API 호출
pages/index.tsx 파일을 수정하여 게시물을 생성하고 조회하는 간단한 UI를 만듭니다.
// pages/index.tsx
import { useState, useEffect } from 'react'
interface Post {
id: number;
title: string;
content: string | null;
published: boolean;
createdAt: string;
updatedAt: string;
}
export default function Home() {
const [posts, setPosts] = useState<Post[]>([])
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const fetchPosts = async () => {
const res = await fetch('/api/posts')
const data = await res.json()
setPosts(data)
}
useEffect(() => {
fetchPosts()
}, [])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
})
setTitle('')
setContent('')
fetchPosts() // 게시물 목록 새로고침
}
return (
<div style={{ padding: '20px' }}>
<h1>Next.js 게시판</h1>
<h2>새 게시물 작성</h2>
<form onSubmit={handleSubmit}>
<input
type='text'
placeholder='제목'
value={title}
onChange={(e) => setTitle(e.target.value)}
style={{ marginRight: '10px', padding: '8px' }}
/>
<textarea
placeholder='내용'
value={content}
onChange={(e) => setContent(e.target.value)}
style={{ marginRight: '10px', padding: '8px' }}
></textarea>
<button type='submit' style={{ padding: '8px 15px' }}>게시물 추가</button>
</form>
<h2 style={{ marginTop: '30px' }}>게시물 목록</h2>
<ul>
{posts.map((post) => (
<li key={post.id} style={{ border: '1px solid #ccc', padding: '10px', marginBottom: '10px' }}>
<h3>{post.title}</h3>
<p>{post.content}</p>
<small>게시됨: {post.published ? '예' : '아니오'}</small>
</li>
))}
</ul>
</div>
)
}
이제 npm run dev 명령어로 애플리케이션을 실행하고 브라우저에서 http://localhost:3000에 접속하면, 게시물을 작성하고 조회하는 기능을 확인할 수 있습니다.
⚠️ 자주 틀리는 것 / 주의사항
DATABASE_URL환경 변수 설정 누락:.env파일에DATABASE_URL="file:./dev.db"와 같이 SQLite 파일 경로를 정확히 설정해야 합니다.- 마이그레이션 누락:
schema.prisma파일을 수정한 후에는 반드시npx prisma migrate dev명령어를 실행하여 데이터베이스 스키마를 업데이트해야 합니다. 그렇지 않으면 변경 사항이 반영되지 않습니다. - Prisma Client 인스턴스 중복 생성: 개발 환경에서 핫 리로딩 시
new PrismaClient()가 여러 번 호출되어 문제가 발생할 수 있습니다. 위 예제처럼global객체를 활용하여 싱글톤 패턴으로 관리하는 것이 좋습니다. - API 라우트 메서드 불일치: API 라우트에서
req.method를 확인하여 올바른 HTTP 메서드(GET, POST, PUT, DELETE 등)로 요청을 처리해야 합니다. - 타입 변환 오류: URL 파라미터(예:
req.query.id)는 문자열로 전달되므로, 데이터베이스에서 숫자 ID로 사용하려면Number()등으로 명시적 형 변환이 필요합니다.
- 데이터베이스는 웹 애플리케이션의 데이터를 저장하고 관리하는 핵심 시스템입니다.
- SQLite는 서버 없이 파일 기반으로 동작하는 경량 데이터베이스로, 개발 및 소규모 프로젝트에 적합합니다.
- ORM(Object-Relational Mapping)은 객체 지향 언어와 관계형 데이터베이스를 연결하여 SQL 없이 데이터를 조작하게 해주는 기술입니다.
- Prisma는 Next.js에서 널리 사용되는 ORM으로, 스키마 정의, 마이그레이션, 강력한 쿼리 기능을 제공합니다.
- Prisma를 사용하여 데이터베이스 스키마를 정의하고, 마이그레이션을 통해 실제 데이터베이스에 반영합니다.
- Next.js API 라우트에서 Prisma Client를 사용하여 데이터베이스의 CRUD(생성, 조회, 수정, 삭제) 작업을 수행할 수 있습니다.
🔗 다음 회차 예고
이번 회차에서는 Next.js와 SQLite를 연동하여 기본적인 데이터베이스 CRUD 작업을 학습했습니다. 다음 23회차에서는 ‘데이터 유효성 검사 및 에러 처리’를 주제로, 사용자 입력 데이터의 유효성을 검사하고, API 요청 및 데이터베이스 작업 중 발생할 수 있는 다양한 에러를 효과적으로 처리하는 방법에 대해 심도 있게 다룰 예정입니다. 안정적이고 견고한 웹 애플리케이션을 만들기 위한 필수적인 과정이므로, 다음 회차도 많은 기대 바랍니다.