# [Next.js + TS 실전] 2. Prisma + SQLite로 DB 붙이기

[Next.js + TS 실전] 2. Prisma + SQLite로 DB 붙이기

실전 1편까지 CRUD는 완성됐습니다.
문제는 서버를 재시작하면 메모가 사라진다는 것입니다.
이번 편은 Prisma + SQLite로 진짜 저장을 붙입니다.


이 편에서 할 일

  1. Prisma + SQLite 설치·초기화
  2. Memo 테이블(스키마) 정의
  3. lib/prisma.ts 클라이언트 만들기
  4. lib/memos.ts를 배열 → DB 쿼리로 교체
  5. 시드(초기 데이터) 넣고 재시작해도 남는지 확인

페이지·Server Action 코드는 거의 안 건드립니다.
바뀌는 건 주로 lib/memos.ts입니다. 이게 이번 편의 핵심입니다.


왜 Prisma + SQLite인가

선택 이유
SQLite 파일 하나로 동작. 로컬 학습에 제일 단순
Prisma 스키마 → 타입 생성. TypeScript와 궁합이 좋음

나중에 PostgreSQL로 바꿀 때도, 스키마의 providerDATABASE_URL만 바꾸면 비슷한 코드로 갈 수 있습니다.
지금은 로컬에서 영속화가 목표입니다.


전체 그림

페이지 / Server Action
        ↓
   lib/memos.ts   ← 함수 이름은 그대로
        ↓
   lib/prisma.ts
        ↓
   SQLite 파일 (prisma/dev.db)

getMemos, addMemo 같은 함수 시그니처를 유지하면
actions.ts와 페이지는 그대로 두어도 됩니다.


1) 패키지 설치

memo-app 폴더에서:

npm install @prisma/client
npm install -D prisma

초기화 (SQLite):

npx prisma init --datasource-provider sqlite

생성되는 것:

prisma/schema.prisma
.env

.env 예시:

DATABASE_URL="file:./dev.db"

file:./dev.dbprisma/ 폴더 기준 상대 경로입니다.
실제 DB 파일은 prisma/dev.db에 생깁니다.

.env는 Git에 올리지 마세요.
create-next-app.gitignore.env*를 넣어 둔 경우가 많습니다.


2) 스키마 정의

prisma/schema.prisma를 아래처럼 맞춥니다.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Memo {
  id        String   @id @default(cuid())
  title     String
  content   String
  createdAt DateTime @default(now())
}

포인트:

필드 의미
@id 기본 키
@default(cuid()) 고유 id 자동 생성 (Date.now() 대체)
DateTime DB에는 날짜 타입, 화면에는 문자열로 변환해서 씀
@default(now()) 생성 시각 자동

테이블을 만들고 Prisma Client를 생성합니다.

npx prisma migrate dev --name init

성공하면:

  • prisma/dev.db 생성
  • prisma/migrations/ 생성
  • @prisma/client 타입이 갱신됨

3) Prisma Client 싱글톤

Next.js 개발 모드에서는 Hot Reload 때문에
PrismaClient를 매번 new 하면 연결이 쌓일 수 있습니다.

경로: lib/prisma.ts

import { PrismaClient } from "@prisma/client";

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

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

개발에서만 globalThis에 붙여 두는 패턴입니다.
공식 문서에서도 비슷한 예시를 씁니다.


4) lib/memos.ts를 DB로 교체

배열과 delay를 지우고, Prisma로 바꿉니다.

import type { Memo } from "@/types/memo";
import { prisma } from "@/lib/prisma";

function toMemo(row: {
  id: string;
  title: string;
  content: string;
  createdAt: Date;
}): Memo {
  return {
    id: row.id,
    title: row.title,
    content: row.content,
    createdAt: row.createdAt.toISOString().slice(0, 10),
  };
}

export async function getMemos(): Promise<Memo[]> {
  const rows = await prisma.memo.findMany({
    orderBy: { createdAt: "desc" },
  });

  return rows.map(toMemo);
}

export async function getMemoById(id: string): Promise<Memo | undefined> {
  const row = await prisma.memo.findUnique({
    where: { id },
  });

  return row ? toMemo(row) : undefined;
}

export async function addMemo(input: {
  title: string;
  content: string;
}): Promise<Memo> {
  const row = await prisma.memo.create({
    data: {
      title: input.title,
      content: input.content,
    },
  });

  return toMemo(row);
}

export async function updateMemo(
  id: string,
  input: { title: string; content: string }
): Promise<Memo | undefined> {
  try {
    const row = await prisma.memo.update({
      where: { id },
      data: {
        title: input.title,
        content: input.content,
      },
    });

    return toMemo(row);
  } catch {
    return undefined;
  }
}

export async function deleteMemo(id: string): Promise<boolean> {
  try {
    await prisma.memo.delete({
      where: { id },
    });
    return true;
  } catch {
    return false;
  }
}

포인트:

  • 함수 이름·반환 타입은 그대로 → Action/페이지 수정 최소
  • toMemo로 DB Date → 화면용 string 변환
  • 없는 id update/delete는 Prisma가 에러 → try/catchundefined / false
  • 가짜 delay는 제거 (로딩 UI는 DB 속도에 맡김)

types/memo.tsMemo 타입은 그대로 둡니다.
DB 모델과 화면 모델을 toMemo로 연결하는 방식입니다.


5) 초기 데이터(시드) 넣기 (선택)

DB가 비어 있으면 목록이 텅 빕니다.
연습용 데이터를 넣고 싶다면:

prisma/seed.ts:

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
  await prisma.memo.createMany({
    data: [
      {
        title: "Next.js 시작",
        content: "App Router로 프로젝트 만들었다.",
      },
      {
        title: "TypeScript props",
        content: "MemoCard에 타입을 붙였다.",
      },
    ],
  });
}

main()
  .then(() => prisma.$disconnect())
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

package.json 맨 바깥prisma 키를 추가합니다.
(주의: { "prisma": ... }를 파일에 따로 하나 더 붙이면 JSON이 깨집니다.)

예:

{
  "name": "memo-app",
  "scripts": { "...": "..." },
  "dependencies": { "...": "..." },
  "devDependencies": { "...": "..." },
  "prisma": {
    "seed": "npx tsx prisma/seed.ts"
  }
}

tsx가 없으면:

npm install -D tsx

시드 전에 반드시 마이그레이션을 먼저 하세요. (Memo 테이블이 있어야 함)

npx prisma migrate dev --name init
npx prisma db seed

또는 Prisma Studio로 직접 넣어도 됩니다.

npx prisma studio

브라우저에서 테이블을 GUI로 볼 수 있습니다.


6) 동작 확인

  1. npm run dev
  2. /memos/new에서 메모 추가
  3. 터미널에서 서버 중지 후 다시 npm run dev
  4. /memos에 방금 메모가 남아 있으면 성공

이전(메모리 배열)이라면 여기서 사라졌을 겁니다.


Action / 페이지는?

대부분 수정할 필요 없습니다.

createMemo, updateMemoAction, deleteMemoAction
이미 addMemo / updateMemo / deleteMemo만 호출합니다.

lib/memos.ts 구현만 바뀌었을 뿐입니다.

이게 좋은 설계입니다.

UI · Action · 데이터 레이어를 나누면,
저장소만 바꿔도 화면 코드는 버틸 수 있다.


.gitignore 확인

아래가 무시되는지 확인하세요.

.env
.env*.local
prisma/dev.db
prisma/dev.db-journal

DB 파일과 환경 변수는 저장소에 올리지 않는 편이 안전합니다.
팀원/다른 PC에서는 migrate로 스키마를 다시 만듭니다.

npx prisma migrate dev

자주 하는 실수

1. migrate를 안 돌림
prisma.memo가 없거나 테이블이 없어 에러

2. lib/prisma.ts 없이 매 파일에서 new PrismaClient()
→ 개발 중 연결 폭주 가능

3. createdAt을 Date 그대로 JSX에 뿌림
toMemo로 문자열 변환 권장 (기존 UI와 맞춤)

4. .envDATABASE_URL 오타
→ DB 파일을 못 찾아 실패

5. 예전에 쓰던 id 1, 2를 하드코딩한 링크
→ 이제 id는 cuid()라 긴 문자열입니다. UI의 Link는 memo.id를 쓰면 문제 없습니다.

6. loading이 너무 짧아져서 “로딩이 안 보여요”
→ 정상입니다. 원하면 loading.tsx는 그대로 두고, 느린 네트워크에서만 눈에 띕니다.


이 편에서 가져갈 것

  • 메모리 배열은 학습용, 실서비스는 DB가 필요하다
  • Prisma = 스키마 + 타입 안전한 쿼리
  • SQLite = 로컬에 파일 하나로 시작하기 좋다
  • 데이터 레이어만 바꾸면 CRUD UI/Action은 재사용된다
  • toMemo로 DB 모델과 화면 타입을 연결한다

다음 편 예고

[Next.js + TS 실전] 3. Vercel에 배포하기

  • 빌드·환경 변수
  • SQLite의 한계와 배포 시 선택지
  • 메모 앱을 URL로 열어보기

로컬 DB까지 붙였으니, 다음은 다른 사람이 접속할 수 있게 만드는 단계입니다.

미리 한 줄: Vercel 같은 서버리스 환경에서는 SQLite 파일 DB가 잘 안 맞을 수 있습니다.
다음 편에서 “왜”와 대안(예: Postgres)을 같이 정리합니다.


시리즈 목차 (실전)

  1. 메모 수정과 삭제
  2. Prisma + SQLite로 DB 붙이기 ← 지금
  3. Vercel 배포 (예정)

입문·실전 1편과의 연결

이전 이번
const memos = [...] prisma.memo.findMany
Date.now() id cuid()
서버 재시작 → 데이터 소멸 서버 재시작 → 데이터 유지
delay()로 로딩 연습 실제 I/O

함수 이름은 같고, 속이 DB로 바뀐 것이 이번 편의 전부입니다.