실전 예제: 로그인과 CRUD가 있는 미니 SaaS 만들기
이번에는 "진짜 서비스"의 뼈대를 만든다
할 일 앱과 날씨 대시보드를 만들고 나면 이런 생각이 듭니다. "이제 화면은 만들 수 있을 것 같은데, 실제 서비스는 어떻게 만들지?" 실제 서비스에는 대개 로그인한 사용자만 볼 수 있는 페이지가 있고, 사용자가 만든 데이터가 데이터베이스에 저장되며, 그 데이터는 만들고 읽고 고치고 지울 수 있어야 합니다. 이 네 가지 작업을 보통 CRUD라고 부릅니다. Create, Read, Update, Delete의 앞글자입니다.
이번 장에서는 작은 미니 SaaS를 만듭니다. SaaS는 Software as a Service의 줄임말로, 브라우저에서 로그인해 사용하는 웹 서비스를 뜻합니다. 우리가 만들 앱은 거창하지 않습니다. 사용자가 회원가입하고 로그인한 뒤, 자기 프로젝트를 만들고, 각 프로젝트 안에 메모를 남기는 앱입니다. 이름은 MemoDesk라고 하겠습니다.
완성 목표는 다음과 같습니다.
회원가입, 로그인, 로그아웃을 할 수 있다.
로그인하지 않은 사용자는 대시보드에 들어갈 수 없다.
사용자는 자기 프로젝트만 볼 수 있다.
프로젝트를 만들고, 목록을 보고, 상세 페이지에 들어가고, 수정하고, 삭제할 수 있다.
프로젝트 안에 메모를 만들고, 수정하고, 삭제할 수 있다.
폼 입력값이 잘못되면 친절한 오류 메시지를 보여 준다.
Codex가 만든 코드를 실제 명령으로 검증한다.
이 장에서 중요한 것은 "한 번에 다 만들어 달라"고 하지 않는 습관입니다. 인증, 데이터 모델, API 라우트, 폼, 검증, 보안은 서로 얽혀 있습니다. 초보자가 한 번에 맡기면 결과물이 커지고, 어디서 문제가 생겼는지 알기 어렵습니다. 그래서 이번 실습은 아주 작은 단위로 나눕니다. 구현하고, 실행하고, 검증한 뒤 다음 단계로 넘어갑니다.

사용할 스택 정하기
이번 예제는 Next.js App Router를 기준으로 진행합니다. Next.js의 Route Handler는 app 디렉터리 안에서 route.ts 파일로 GET, POST, PATCH, DELETE 같은 HTTP 요청을 처리할 수 있게 해 줍니다.1 쉽게 말해, Next.js 프로젝트 안에 프론트엔드 화면과 백엔드 API를 함께 둘 수 있습니다.
데이터베이스는 초보자가 시작하기 쉬운 SQLite를 사용하고, ORM은 Prisma를 사용합니다. ORM은 데이터베이스 테이블을 TypeScript 코드처럼 다룰 수 있게 해 주는 도구입니다. 비밀번호 해시는 bcryptjs, 입력값 검증은 zod를 사용합니다. 해시는 비밀번호 원문을 그대로 저장하지 않고, 되돌리기 어렵게 변환해 저장하는 방식입니다.
실전 서비스에서는 Auth.js, Clerk, WorkOS, Supabase Auth 같은 인증 솔루션을 사용할 수 있습니다. 하지만 이번 장에서는 학습을 위해 이메일, 비밀번호, 세션 쿠키 기반 인증을 직접 구현합니다. 직접 만들어 보면 나중에 인증 라이브러리를 쓰더라도 내부 구조를 훨씬 잘 이해할 수 있습니다.
완성 구조는 대략 이렇게 됩니다.
memo-desk/
app/
api/
auth/
signup/route.ts
login/route.ts
logout/route.ts
me/route.ts
projects/
route.ts
[projectId]/route.ts
[projectId]/memos/route.ts
[projectId]/memos/[memoId]/route.ts
dashboard/page.tsx
login/page.tsx
signup/page.tsx
projects/[projectId]/page.tsx
components/
ProjectForm.tsx
MemoForm.tsx
lib/
auth.ts
db.ts
validators.ts
prisma/
schema.prisma
AGENTS.md
package.json여기서 app/api 아래 파일들은 서버 역할을 합니다. app/dashboard/page.tsx 같은 파일은 화면 역할을 합니다. lib/auth.ts에는 현재 로그인한 사용자를 확인하는 공통 함수를 둡니다. lib/validators.ts에는 이메일 형식, 비밀번호 길이, 프로젝트 제목 길이 같은 검증 규칙을 모읍니다.
Codex에게 먼저 컨텍스트를 먹인다
Codex는 똑똑하지만, 내 프로젝트의 의도까지 자동으로 알지는 못합니다. 따라서 작업을 시작하기 전에 "이 프로젝트가 무엇이고, 어떤 규칙으로 작업해야 하는지"를 알려 줘야 합니다. 앞 장에서 만든 AGENTS.md가 여기서 빛을 발합니다.
프로젝트 루트에 다음과 비슷한 내용을 준비합니다.
# MemoDesk 작업 지침
## 프로젝트 목표
Next.js App Router 기반의 미니 SaaS를 만든다.
사용자는 회원가입/로그인 후 자기 프로젝트와 메모를 관리한다.
## 기술 스택
- Next.js App Router
- TypeScript
- Prisma
- SQLite
- bcryptjs
- zod
## 데이터 모델
- User: id, email, passwordHash, createdAt
- Project: id, name, description, ownerId, createdAt, updatedAt
- Memo: id, content, projectId, authorId, createdAt, updatedAt
## 인증 방식
- 이메일/비밀번호 로그인
- 비밀번호는 bcrypt로 해시한다.
- 세션 토큰은 httpOnly 쿠키에 저장한다.
- 보호된 API는 현재 로그인한 사용자만 접근할 수 있다.
## 실행 명령
- 개발 서버: npm run dev
- 린트: npm run lint
- 타입 체크: npm run typecheck
- 테스트: npm run test
- Prisma 반영: npx prisma migrate dev
## 작업 규칙
- 한 번에 큰 기능을 모두 구현하지 않는다.
- 변경 후 수정 파일 목록과 검증 명령 결과를 보고한다.
- 인증/보안 관련 코드는 반드시 주의점도 함께 설명한다.
- 현재 사용자의 데이터만 조회/수정/삭제하도록 ownerId 조건을 빠뜨리지 않는다.이 파일은 Codex에게 주는 프로젝트 기억입니다. Codex App, CLI, IDE 확장을 어디에서 쓰든 같은 원칙을 반복해서 알려 주는 역할을 합니다. Codex는 App, CLI, IDE 확장, Cloud처럼 여러 접점에서 사용할 수 있고, Cloud에서는 작업을 백그라운드에서 병렬로 실행할 수도 있습니다.2 하지만 초보자는 일단 로컬에서 작은 단위로 검증하는 흐름을 익히는 것이 안전합니다.
작업 시작 프롬프트는 이렇게 씁니다.
현재 프로젝트는 Next.js App Router + TypeScript 기반 미니 SaaS입니다.
AGENTS.md의 규칙을 읽고, 먼저 현재 폴더 구조와 package.json을 확인해 주세요.
이번 목표는 로그인과 CRUD가 있는 MemoDesk 앱을 단계별로 구현하는 것입니다.
아직 코드를 수정하지 말고 다음만 해 주세요.
1. 현재 프로젝트 구조 요약
2. 필요한 패키지 목록 제안
3. 구현 단계를 인증, 데이터 모델, API, 화면, 검증, 테스트 순서로 나눈 계획
4. 위험한 보안 포인트 목록
네트워크 사용 가능 여부를 묻고, DB 마이그레이션이 필요한 시점에는 먼저 알려 주세요.좋은 프롬프트의 핵심은 "지금 당장 코딩하지 말라"는 제동 장치입니다. 초보자는 Codex가 바로 파일을 고치기 시작하면 따라가기 어렵습니다. 먼저 구조를 요약하게 만들고, 계획을 확인한 뒤 구현을 맡기는 편이 안전합니다.
Codex CLI나 IDE 확장을 사용할 때 로그인 방식도 확인해 두면 좋습니다. Codex CLI와 IDE 확장은 ChatGPT 로그인과 API 키 로그인을 지원하고, Codex Cloud는 ChatGPT 로그인이 필요합니다.3 팀이나 회사 계정에서는 어떤 로그인 방식이 허용되는지 미리 확인해야 합니다.
프로젝트 생성과 패키지 설치
새 프로젝트라면 다음처럼 시작할 수 있습니다.
npx create-next-app@latest memo-desk \
--ts \
--app \
--eslint
cd memo-desk
npm install prisma @prisma/client bcryptjs zod
npm install -D @types/bcryptjs vitest tsx
npx prisma init --datasource-provider sqlite이미 프로젝트가 있다면 Codex에게 먼저 package.json을 읽게 한 뒤, 없는 패키지만 추가하게 합니다.
package.json을 확인하고, 이번 장의 미니 SaaS 구현에 필요한 패키지 중 없는 것만 설치 명령으로 제안해 주세요.
바로 설치하지 말고 다음 형식으로 알려 주세요.
- 필요한 패키지
- 이유
- 설치 명령
- 설치 후 확인할 명령Codex가 제안한 명령을 확인한 뒤 실행합니다. 패키지 설치는 네트워크를 사용하므로, 권한 모드에 따라 승인이 필요할 수 있습니다. 여기서도 무작정 승인하지 말고 패키지 이름을 확인하세요. 인증, 암호화, 데이터베이스 관련 패키지는 특히 중요합니다.
데이터 모델부터 만든다
앱의 중심은 데이터입니다. 화면보다 먼저 데이터 모델을 정해야 합니다. 우리가 만들 모델은 User, Project, Memo 세 가지입니다.
prisma/schema.prisma는 이런 모양이 됩니다.
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
createdAt DateTime @default(now())
projects Project[]
memos Memo[]
}
model Project {
id String @id @default(cuid())
name String
description String?
ownerId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
memos Memo[]
@@index([ownerId])
}
model Memo {
id String @id @default(cuid())
content String
projectId String
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
@@index([projectId])
@@index([authorId])
}초보자가 여기서 꼭 이해해야 할 부분은 ownerId와 authorId입니다. 프로젝트는 어떤 사용자의 것인지 알아야 하고, 메모도 누가 작성했는지 알아야 합니다. 이 값이 있어야 "현재 로그인한 사용자의 데이터만 보여 주기"가 가능합니다.
Codex에게는 이렇게 요청합니다.
prisma/schema.prisma에 User, Project, Memo 모델을 추가해 주세요.
조건:
- User.email은 unique여야 합니다.
- User.passwordHash에는 비밀번호 원문이 아니라 해시만 저장합니다.
- Project는 ownerId로 User와 연결합니다.
- Memo는 projectId와 authorId를 가집니다.
- ownerId, projectId, authorId에는 조회 성능을 위해 index를 추가합니다.
- SQLite에서 동작해야 합니다.
수정 파일은 prisma/schema.prisma로 제한하세요.
수정 후 npx prisma validate를 실행하고 결과를 알려 주세요.이 프롬프트에서 중요한 표현은 "수정 파일 제한"과 "검증 명령"입니다. Codex가 데이터 모델을 고쳤다면 바로 다음 명령을 실행합니다.
npx prisma validate
npx prisma migrate dev --name init마이그레이션은 데이터베이스 구조를 실제 파일에 반영하는 작업입니다. Prisma가 dev.db 같은 SQLite 파일과 prisma/migrations 폴더를 만들 것입니다.
DB 연결 파일 만들기
Next.js 개발 서버는 파일이 바뀔 때 다시 로드됩니다. 이때 Prisma Client가 여러 번 생성되면 개발 중 경고가 생길 수 있습니다. 그래서 lib/db.ts에 공통 클라이언트를 만듭니다.
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma?: PrismaClient;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: ['error', 'warn'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}Codex에게 맡길 때는 다음처럼 합니다.
lib/db.ts를 만들어 PrismaClient 싱글턴을 구성해 주세요.
Next.js 개발 환경에서 hot reload 때문에 PrismaClient가 중복 생성되지 않도록 globalThis 패턴을 사용해 주세요.
수정 파일은 lib/db.ts만 허용합니다.
완료 후 TypeScript 오류 가능성이 있는지 설명해 주세요.코드가 짧아 보여도 이런 공통 파일은 이후 모든 API에서 사용됩니다. 작은 기초 파일일수록 초반에 깔끔하게 만들어야 합니다.
입력값 검증 규칙 만들기
사용자가 보내는 값은 믿으면 안 됩니다. 브라우저 화면에서 검증을 했더라도, 누군가는 API에 직접 잘못된 요청을 보낼 수 있습니다. 그래서 클라이언트 검증과 서버 검증을 함께 해야 합니다. HTML5에는 required, email 같은 기본 검증 기능과 Constraint Validation API가 있고, 이런 기본 기능을 활용하면 폼 검증 부담을 줄일 수 있습니다.4 하지만 서버에서는 반드시 다시 검증해야 합니다.
lib/validators.ts를 만듭니다.
import { z } from 'zod';
export const signupSchema = z.object({
email: z.string().email('올바른 이메일을 입력해 주세요.'),
password: z.string().min(8, '비밀번호는 8자 이상이어야 합니다.'),
});
export const loginSchema = signupSchema;
export const projectSchema = z.object({
name: z.string().min(1, '프로젝트 이름은 필수입니다.').max(80),
description: z.string().max(500).optional().or(z.literal('')),
});
export const memoSchema = z.object({
content: z.string().min(1, '메모 내용은 필수입니다.').max(2000),
});Codex 요청은 이렇게 나눕니다.
lib/validators.ts를 만들어 zod 검증 스키마를 작성해 주세요.
포함할 스키마:
- signupSchema: email, password
- loginSchema: email, password
- projectSchema: name, description
- memoSchema: content
조건:
- 사용자에게 보여 줄 한국어 오류 메시지를 포함합니다.
- 이메일 형식, 비밀번호 8자 이상, 필수값, 최대 길이를 검증합니다.
- 다른 파일은 수정하지 마세요.검증 규칙은 한곳에 모아야 유지보수가 쉽습니다. 나중에 비밀번호 최소 길이를 10자로 바꾸고 싶을 때 여러 파일을 뒤질 필요가 없습니다.
인증을 한 번에 맡기지 않는다
이제 가장 중요한 인증입니다. 초보자가 자주 하는 실수는 이렇게 요청하는 것입니다.
회원가입, 로그인, 로그아웃, 세션 유지, 보호된 페이지까지 전부 구현해 줘.이 요청은 너무 큽니다. Codex가 많은 파일을 한 번에 고치고, 문제가 생기면 원인을 찾기 어려워집니다. 대신 이렇게 나눕니다.
첫 번째는 회원가입입니다.
회원가입 API만 구현해 주세요.
목표:
POST /api/auth/signup
요청 body:
{ "email": "[email protected]", "password": "password123" }
성공 기준:
- 이메일과 비밀번호를 zod로 검증합니다.
- 이미 가입된 이메일이면 409를 반환합니다.
- 비밀번호는 bcrypt로 해시해서 User.passwordHash에 저장합니다.
- 성공 시 passwordHash를 제외한 사용자 id, email을 반환합니다.
수정 파일 범위:
- app/api/auth/signup/route.ts
- 필요한 경우 lib/validators.ts만
실행 검증:
- npm run lint
- 가능하면 curl 예시를 함께 제시예상 구현은 이런 형태입니다.
import bcrypt from 'bcryptjs';
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { signupSchema } from '@/lib/validators';
export async function POST(req: Request) {
const body = await req.json();
const parsed = signupSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ message: parsed.error.issues[0]?.message ?? '입력값을 확인해 주세요.' },
{ status: 400 },
);
}
const { email, password } = parsed.data;
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
return NextResponse.json({ message: '이미 가입된 이메일입니다.' }, { status: 409 });
}
const passwordHash = await bcrypt.hash(password, 12);
const user = await prisma.user.create({
data: { email, passwordHash },
select: { id: true, email: true, createdAt: true },
});
return NextResponse.json({ user }, { status: 201 });
}검증은 실제 요청으로 합니다.
curl -i -X POST http://localhost:3000/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"password123"}'여기서 확인해야 할 것은 "성공 응답이 온다"가 전부가 아닙니다. 같은 이메일로 한 번 더 요청했을 때 409가 오는지도 봐야 합니다. 비밀번호를 123으로 보냈을 때 400이 오는지도 확인해야 합니다.
세션 쿠키 만들기
회원가입 다음은 로그인입니다. 로그인은 이메일과 비밀번호를 확인하고, 성공하면 세션을 만들어 쿠키에 저장합니다. 이번 예제에서는 학습용으로 Session 모델을 추가하겠습니다.
prisma/schema.prisma에 다음 모델을 추가합니다.
model Session {
id String @id @default(cuid())
token String @unique
userId String
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([expiresAt])
}그리고 User 모델에는 관계를 추가합니다.
sessions Session[]Codex에게는 먼저 모델 추가만 시킵니다.
Prisma schema에 Session 모델을 추가해 주세요.
조건:
- token은 unique입니다.
- userId로 User와 연결합니다.
- expiresAt을 둡니다.
- User에 sessions 관계를 추가합니다.
- SQLite에서 동작해야 합니다.
수정 후 npx prisma validate를 실행해 주세요.
마이그레이션 명령은 실행하지 말고 제안만 해 주세요.검증 후 직접 마이그레이션합니다.
npx prisma migrate dev --name add_session이제 lib/auth.ts를 만듭니다. 여기에는 세션 생성, 현재 사용자 조회, 로그아웃에 쓸 함수를 둡니다.
import crypto from 'node:crypto';
import { cookies } from 'next/headers';
import { prisma } from '@/lib/db';
export const SESSION_COOKIE_NAME = 'memo_desk_session';
export function createSessionToken() {
return crypto.randomBytes(32).toString('hex');
}
export function getSessionExpiresAt() {
return new Date(Date.now() + 1000 * 60 * 60 * 24 * 7);
}
export async function getCurrentUser() {
const cookieStore = await cookies();
const token = cookieStore.get(SESSION_COOKIE_NAME)?.value;
if (!token) return null;
const session = await prisma.session.findUnique({
where: { token },
include: { user: { select: { id: true, email: true, createdAt: true } } },
});
if (!session || session.expiresAt < new Date()) {
return null;
}
return session.user;
}쿠키는 브라우저가 서버에 자동으로 보내는 작은 값입니다. 세션 쿠키에는 사용자 id를 그대로 넣기보다 예측하기 어려운 토큰을 넣고, 서버 DB에서 토큰에 해당하는 사용자를 찾는 편이 안전합니다.
로그인과 로그아웃 구현하기
로그인 API 요청 프롬프트입니다.
로그인 API를 구현해 주세요.
목표:
POST /api/auth/login
요청 body:
{ "email": "[email protected]", "password": "password123" }
성공 기준:
- loginSchema로 입력값을 검증합니다.
- 사용자가 없거나 비밀번호가 틀리면 401을 반환합니다.
- bcrypt.compare로 비밀번호를 확인합니다.
- 로그인 성공 시 Session을 생성하고 httpOnly 쿠키를 설정합니다.
- 쿠키 옵션에는 httpOnly, sameSite=lax, secure는 production에서만 true, path=/, expires를 포함합니다.
- 응답에는 passwordHash를 포함하지 않습니다.
수정 파일:
- app/api/auth/login/route.ts
- 필요한 경우 lib/auth.ts
완료 후 curl 테스트 예시를 알려 주세요.로그아웃은 더 작습니다.
로그아웃 API를 구현해 주세요.
목표:
POST /api/auth/logout
성공 기준:
- 현재 세션 쿠키를 읽습니다.
- DB에서 해당 세션을 삭제합니다.
- 브라우저 쿠키도 만료시킵니다.
- 항상 안전하게 { ok: true }를 반환합니다.
수정 파일:
- app/api/auth/logout/route.ts
- 필요한 경우 lib/auth.ts현재 로그인 사용자를 확인하는 API도 만듭니다.
현재 로그인 사용자 확인 API를 구현해 주세요.
목표:
GET /api/auth/me
성공 기준:
- 로그인되어 있으면 { user: { id, email, createdAt } } 반환
- 로그인되어 있지 않으면 401 반환
- passwordHash는 절대 반환하지 않음
수정 파일:
- app/api/auth/me/route.ts이제 브라우저에서 회원가입, 로그인, 내 정보 확인까지 API 흐름이 이어져야 합니다. 아직 화면이 없어도 괜찮습니다. API가 먼저 안정되어야 화면이 붙습니다.
보호된 페이지 만들기
대시보드는 로그인한 사용자만 볼 수 있어야 합니다. Next.js App Router에서는 서버 컴포넌트에서 현재 사용자를 확인하고, 없으면 로그인 페이지로 보낼 수 있습니다.
import { redirect } from 'next/navigation';
import { getCurrentUser } from '@/lib/auth';
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user) {
redirect('/login');
}
return (
<main>
<h2>대시보드</h2>
<p>{user.email}님, 환영합니다.</p>
</main>
);
}프롬프트는 이렇게 씁니다.
보호된 대시보드 페이지를 구현해 주세요.
목표:
- /dashboard는 로그인한 사용자만 접근할 수 있습니다.
- getCurrentUser()가 null이면 /login으로 redirect합니다.
- 로그인된 경우 사용자 이메일과 프로젝트 목록 영역을 보여 줍니다.
수정 파일:
- app/dashboard/page.tsx
아직 프로젝트 CRUD는 구현하지 말고, 빈 목록 UI만 만들어 주세요.여기서도 "아직 CRUD는 구현하지 말라"고 막았습니다. 작은 성공을 쌓는 것이 중요합니다.
프로젝트 CRUD API 만들기
이제 프로젝트를 만들고, 읽고, 수정하고, 삭제하는 API를 만듭니다. 가장 중요한 조건은 현재 로그인한 사용자의 데이터만 다루는 것입니다.
목록과 생성은 /api/projects/route.ts에 둡니다.
export async function GET() {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ message: '로그인이 필요합니다.' }, { status: 401 });
}
const projects = await prisma.project.findMany({
where: { ownerId: user.id },
orderBy: { updatedAt: 'desc' },
});
return NextResponse.json({ projects });
}여기서 where: { ownerId: user.id }가 핵심입니다. 이 조건이 빠지면 다른 사용자의 프로젝트가 보일 수 있습니다. Codex가 CRUD를 만들 때 가장 흔히 놓칠 수 있는 부분입니다.
생성은 이렇게 갑니다.
export async function POST(req: Request) {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ message: '로그인이 필요합니다.' }, { status: 401 });
}
const body = await req.json();
const parsed = projectSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ message: parsed.error.issues[0]?.message ?? '입력값을 확인해 주세요.' },
{ status: 400 },
);
}
const project = await prisma.project.create({
data: {
name: parsed.data.name,
description: parsed.data.description || null,
ownerId: user.id,
},
});
return NextResponse.json({ project }, { status: 201 });
}Codex 프롬프트입니다.
프로젝트 목록 조회와 생성 API를 구현해 주세요.
목표:
- GET /api/projects: 현재 로그인한 사용자의 프로젝트만 반환
- POST /api/projects: 현재 로그인한 사용자에게 프로젝트 생성
조건:
- 로그인하지 않으면 401
- projectSchema로 입력값 검증
- 생성 시 ownerId는 body에서 받지 말고 현재 로그인 사용자 id를 사용
- 목록은 updatedAt desc 정렬
수정 파일:
- app/api/projects/route.ts
검증:
- npm run lint
- 로그인 쿠키가 있을 때와 없을 때의 curl 테스트 예시 제시상세, 수정, 삭제는 /api/projects/[projectId]/route.ts에 둡니다.
프로젝트 상세, 수정, 삭제 API를 구현해 주세요.
목표:
- GET /api/projects/[projectId]
- PATCH /api/projects/[projectId]
- DELETE /api/projects/[projectId]
권한 조건:
- 모든 요청은 로그인 필요
- projectId만으로 조회하지 말고 반드시 ownerId: currentUser.id 조건을 함께 사용
- 존재하지 않거나 내 프로젝트가 아니면 404 반환
수정 조건:
- PATCH는 projectSchema로 검증
- DELETE는 삭제 후 { ok: true } 반환
수정 파일:
- app/api/projects/[projectId]/route.ts여기서 403이 아니라 404를 반환하는 이유는, 다른 사용자의 프로젝트가 실제로 존재하는지 알려 주지 않기 위해서입니다. "권한이 없습니다"라고 말하면 그 id의 데이터가 있다는 힌트를 줄 수 있습니다. 작은 차이지만 보안에서는 이런 습관이 중요합니다.
메모 CRUD API 연결하기
메모는 프로젝트 아래에 속합니다. 따라서 메모를 만들 때도 "이 프로젝트가 현재 사용자의 것인지" 먼저 확인해야 합니다.
Codex에게 이렇게 요청합니다.
프로젝트 안의 메모 목록 조회와 생성 API를 구현해 주세요.
목표:
- GET /api/projects/[projectId]/memos
- POST /api/projects/[projectId]/memos
권한 조건:
- 로그인 필요
- projectId가 현재 로그인 사용자의 프로젝트인지 먼저 확인
- 내 프로젝트가 아니거나 없으면 404
생성 조건:
- memoSchema로 content 검증
- authorId는 현재 로그인 사용자 id 사용
- projectId는 URL 파라미터 사용
수정 파일:
- app/api/projects/[projectId]/memos/route.ts메모 수정과 삭제도 같은 원칙입니다.
메모 수정과 삭제 API를 구현해 주세요.
목표:
- PATCH /api/projects/[projectId]/memos/[memoId]
- DELETE /api/projects/[projectId]/memos/[memoId]
권한 조건:
- 로그인 필요
- projectId는 현재 로그인 사용자의 프로젝트여야 함
- memoId는 해당 projectId 안에 있어야 함
- 조건이 맞지 않으면 404
수정 조건:
- PATCH는 memoSchema 검증
- DELETE는 { ok: true } 반환
수정 파일:
- app/api/projects/[projectId]/memos/[memoId]/route.tsCRUD API를 AI에게 만들게 할 때는 "관계 조건"을 반드시 말해야 합니다. 단순히 "메모 삭제 API 만들어 줘"라고 하면 memoId만 보고 삭제하는 코드가 나올 수 있습니다. 그러면 다른 사람의 메모 id를 알기만 해도 삭제할 수 있는 위험한 API가 됩니다.
화면을 붙이기 전에 API 테스트부터 한다
프론트엔드 화면을 만들기 전에 API가 제대로 동작하는지 확인합니다. Codex에게 테스트 스크립트를 만들게 해도 좋고, 처음에는 curl로 직접 확인해도 됩니다.
예를 들어 로그인 후 쿠키를 파일에 저장합니다.
curl -i -c cookies.txt -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"password123"}'프로젝트를 생성합니다.
curl -i -b cookies.txt -X POST http://localhost:3000/api/projects \
-H "Content-Type: application/json" \
-d '{"name":"첫 번째 프로젝트","description":"Codex로 만든 미니 SaaS"}'목록을 조회합니다.
curl -i -b cookies.txt http://localhost:3000/api/projects로그인 쿠키 없이 조회했을 때는 401이어야 합니다.
curl -i http://localhost:3000/api/projectsCodex에게 검증을 맡길 때는 이렇게 말합니다.
지금 구현된 인증과 프로젝트 API를 검증해 주세요.
실행할 것:
1. npm run lint
2. npm run typecheck가 package.json에 있으면 실행
3. 개발 서버가 필요하면 내가 실행할 수 있도록 명령을 알려 주고 대기
4. curl로 회원가입, 로그인, 프로젝트 생성, 목록 조회, 비로그인 접근 실패를 확인할 시나리오 작성
주의:
- 실제 비밀번호나 시크릿을 출력하지 마세요.
- 실패한 명령이 있으면 로그를 요약하고 원인 후보를 제시하세요.초보자는 "Codex가 잘 됐다고 말했다"에서 멈추면 안 됩니다. 반드시 터미널에서 실제 명령 결과를 확인해야 합니다. Codex의 설명은 안내서이고, 실행 결과가 판정문입니다.
회원가입과 로그인 화면 만들기
API가 준비되었으니 화면을 붙입니다. 로그인 화면은 클라이언트 컴포넌트로 만들면 이해하기 쉽습니다. 사용자가 입력한 이메일과 비밀번호를 fetch로 /api/auth/login에 보내고, 성공하면 /dashboard로 이동합니다.
Codex 프롬프트입니다.
회원가입 페이지와 로그인 페이지를 만들어 주세요.
목표:
- /signup: 이메일, 비밀번호 입력 후 POST /api/auth/signup 호출
- /login: 이메일, 비밀번호 입력 후 POST /api/auth/login 호출
- 성공 시 /dashboard로 이동
- 실패 시 서버의 message를 화면에 표시
UX 조건:
- 제출 중에는 버튼을 비활성화하고 "처리 중..." 표시
- 이메일 input에는 type="email" 사용
- 비밀번호 input에는 minLength={8} 사용
- 빈 값 제출을 막기 위해 required 사용
- 로그인 페이지에는 회원가입 링크 제공
- 회원가입 페이지에는 로그인 링크 제공
수정 파일:
- app/signup/page.tsx
- app/login/page.tsx여기서 type="email", required, minLength는 브라우저 기본 검증을 활용하는 부분입니다. 하지만 서버 검증을 대체하지 않습니다. 사용자가 개발자 도구나 외부 도구로 API를 직접 호출할 수 있기 때문입니다.
로그인 화면에서 다룰 상태는 네 가지입니다.
입력 중인 값
제출 중인지 여부
성공 후 이동
실패 메시지
네 가지 상태가 모두 있어야 사용자가 "버튼을 눌렀는데 아무 반응이 없는" 답답한 상황을 피할 수 있습니다.
프로젝트 목록과 생성 폼 만들기
대시보드에는 프로젝트 목록과 생성 폼이 필요합니다. 처음부터 화려하게 만들 필요는 없습니다. 빈 목록일 때 "아직 프로젝트가 없습니다. 첫 프로젝트를 만들어 보세요."라고 보여 주는 것이 더 중요합니다.
Codex 요청입니다.
/dashboard 페이지에 프로젝트 목록과 생성 폼을 연결해 주세요.
목표:
- 서버 컴포넌트에서 getCurrentUser로 로그인 확인
- 현재 사용자의 프로젝트 목록을 DB에서 직접 조회
- 빈 목록이면 안내 문구 표시
- ProjectForm 클라이언트 컴포넌트에서 프로젝트 생성
- 생성 성공 후 목록이 갱신되도록 router.refresh() 사용
수정 파일:
- app/dashboard/page.tsx
- components/ProjectForm.tsx
조건:
- ProjectForm은 name, description을 입력받습니다.
- 제출 중 로딩 상태를 표시합니다.
- 서버 오류 message를 화면에 표시합니다.Next.js App Router에서는 서버 컴포넌트가 DB를 직접 조회할 수 있습니다. 그래서 목록 조회는 API를 거치지 않고 서버에서 바로 할 수도 있습니다. 반면 폼 제출은 클라이언트 상호작용이므로 API를 호출하도록 만들면 초보자가 흐름을 이해하기 쉽습니다.
상세, 수정, 삭제 화면 만들기
프로젝트 상세 페이지에서는 프로젝트 정보와 메모 목록을 보여 줍니다. 수정과 삭제 기능도 넣습니다.
요청 프롬프트입니다.
프로젝트 상세 페이지를 구현해 주세요.
목표:
- /projects/[projectId] 페이지 생성
- 로그인하지 않으면 /login으로 redirect
- projectId와 currentUser.id 조건으로 프로젝트 조회
- 없으면 notFound()
- 프로젝트 이름, 설명, 메모 목록 표시
- 메모가 없으면 빈 상태 메시지 표시
- MemoForm으로 새 메모 작성
수정 파일:
- app/projects/[projectId]/page.tsx
- components/MemoForm.tsx수정과 삭제는 UI 실수가 잦은 영역입니다. 삭제 버튼을 누르자마자 삭제하면 사용자가 실수했을 때 복구하기 어렵습니다. 최소한 confirm()으로 확인하게 만듭니다.
프로젝트 상세 페이지에 프로젝트 수정과 삭제 UI를 추가해 주세요.
조건:
- 수정은 간단한 인라인 폼으로 처리합니다.
- 삭제 버튼 클릭 시 confirm으로 확인합니다.
- 삭제 성공 시 /dashboard로 이동합니다.
- 삭제 실패 시 오류 메시지를 표시합니다.
- 다른 사용자의 프로젝트를 수정/삭제하는 요청은 API에서 이미 막고 있으므로, 프론트엔드에서도 서버 오류를 그대로 보여 줍니다.
수정 파일:
- app/projects/[projectId]/page.tsx 또는 필요한 클라이언트 컴포넌트메모 수정과 삭제도 같은 패턴입니다.
메모 목록에 수정과 삭제 기능을 추가해 주세요.
조건:
- 각 메모 옆에 수정, 삭제 버튼을 둡니다.
- 수정 클릭 시 textarea로 바뀌고 저장/취소 버튼을 보여 줍니다.
- 삭제 전 confirm을 표시합니다.
- 성공 후 router.refresh()로 목록을 갱신합니다.
- 로딩 중에는 중복 제출을 막습니다.초보자는 여기서 "화면은 잘 되는데 가끔 두 번 저장돼요" 같은 문제를 만납니다. 대부분 제출 중 버튼 비활성화가 없어서 생깁니다. 로딩 상태는 단순한 장식이 아니라 데이터 중복을 막는 안전장치입니다.
폼 검증과 UX 예외 체크리스트
이제 빠뜨린 예외를 점검합니다. 기능을 만드는 것보다 예외를 챙기는 일이 실제 서비스에 더 가깝습니다.
Codex에게 다음 체크리스트를 기준으로 리뷰를 요청합니다.
현재 회원가입, 로그인, 프로젝트 CRUD, 메모 CRUD의 폼 검증과 UX 예외 처리를 리뷰해 주세요.
체크할 항목:
- 필수값 누락
- 이메일 형식 오류
- 비밀번호 길이 부족
- 중복 가입
- 로그인 실패
- 권한 없음 또는 내 데이터가 아님
- 존재하지 않는 projectId, memoId
- 로딩 상태
- 빈 목록 화면
- 삭제 확인
- 서버 오류 메시지 표시
- 중복 제출 방지
요청:
1. 현재 구현된 항목과 빠진 항목을 표로 정리
2. 빠진 항목을 고치기 위한 최소 수정 계획 제안
3. 바로 수정하지 말고 확인을 기다리기초보자에게 특히 중요한 예외는 빈 목록 화면입니다. 데이터가 없을 때 아무것도 표시하지 않으면 사용자는 앱이 고장 났다고 느낍니다. "아직 프로젝트가 없습니다"라는 한 줄이 서비스의 완성도를 크게 올립니다.
서버 오류 메시지도 조심해야 합니다. 개발 중에는 자세한 오류가 편하지만, 사용자에게 데이터베이스 오류나 스택 트레이스를 그대로 보여 주면 안 됩니다. 사용자에게는 "잠시 후 다시 시도해 주세요"처럼 안전한 메시지를 보여 주고, 자세한 내용은 서버 로그에 남기는 편이 좋습니다.
Codex에게 자동 검증 루틴을 맡기기
이제 Codex가 만든 코드를 검증하는 루틴을 만듭니다. package.json에 다음 스크립트가 있으면 좋습니다.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test": "vitest run"
}
}Next.js 버전에 따라 lint 명령은 달라질 수 있습니다. Codex에게 현재 package.json에 맞게 조정하라고 시키면 됩니다.
검증 요청 프롬프트입니다.
현재 변경 사항을 검증해 주세요.
실행 순서:
1. npm run typecheck
2. npm run lint
3. npm run test가 존재하면 실행
4. npm run build
조건:
- 실패하면 실패 로그의 핵심 부분을 요약해 주세요.
- 추측으로 고치지 말고, 원인을 파일/라인 단위로 좁힌 뒤 수정 계획을 제시하세요.
- 수정이 필요한 경우 한 번에 하나의 원인만 고치고 다시 검증해 주세요.여기서 중요한 말은 "한 번에 하나의 원인만 고치기"입니다. 실패가 여러 개 나왔을 때 Codex가 한꺼번에 많은 파일을 고치면 새로운 문제가 섞입니다. 한 문제를 고치고 다시 실행하는 루프가 더 안전합니다.
API 호출 테스트를 자동화하고 싶다면 간단한 스크립트를 만들 수 있습니다.
scripts/smoke-test.ts를 만들어 로컬 서버에 대해 간단한 API 스모크 테스트를 실행하게 해 주세요.
테스트 흐름:
1. 랜덤 이메일로 회원가입
2. 로그인해서 쿠키 저장
3. 프로젝트 생성
4. 프로젝트 목록 조회
5. 메모 생성
6. 메모 수정
7. 메모 삭제
8. 프로젝트 삭제
9. 로그아웃
조건:
- 실패 시 어떤 단계에서 실패했는지 출력
- 실제 서버는 별도로 npm run dev로 실행되어 있다고 가정
- 테스트 계정 이메일은 [email protected] 형식 사용스모크 테스트는 연기가 나는지만 보는 테스트입니다. 모든 세부사항을 검증하지는 않지만, 앱의 핵심 흐름이 끊어졌는지 빠르게 알려 줍니다.
보안 코드는 반드시 사람이 읽는다
인증과 권한 코드는 Codex에게 맡겨도, 최종 책임은 사람에게 있습니다. 특히 다음 항목은 직접 확인해야 합니다.
비밀번호 원문이 DB에 저장되지 않고 bcrypt 해시만 저장되는가
로그인 실패 메시지가 "이 이메일은 가입되어 있음" 같은 정보를 과하게 노출하지 않는가
세션 쿠키에
httpOnly,sameSite,secure,path,expires가 적절히 설정되어 있는가.env,DATABASE_URL, 세션 시크릿 같은 값이 클라이언트 코드에 노출되지 않는가프로젝트 조회, 수정, 삭제에 항상
ownerId: currentUser.id조건이 포함되는가메모 수정, 삭제가 해당 프로젝트와 현재 사용자 조건을 함께 확인하는가
API 응답에
passwordHash, 세션 토큰, 내부 오류 스택이 포함되지 않는가배포 전 테스트 계정, 임시 시크릿, 디버그 로그가 제거되었는가
Codex에게 보안 리뷰를 요청할 때는 이렇게 말합니다.
인증과 권한 관련 보안 리뷰를 해 주세요.
검토 범위:
- app/api/auth/**
- app/api/projects/**
- lib/auth.ts
- prisma/schema.prisma
중점 확인:
- passwordHash 처리
- 세션 쿠키 옵션
- 환경변수 노출 가능성
- ownerId 조건 누락
- projectId/memoId 권한 검증
- 에러 메시지 정보 노출
- 삭제 API 안전성
출력 형식:
- 심각도: 높음/중간/낮음
- 위치: 파일 경로와 함수명
- 문제 설명
- 수정 제안
바로 수정하지 말고 리뷰 결과만 먼저 보여 주세요.보안 리뷰에서는 "바로 수정하지 말라"가 특히 중요합니다. 보안 수정은 의도를 이해하고 적용해야 합니다. Codex가 제안한 수정이 맞는지 읽고, 납득한 뒤 반영하세요.
자주 만나는 실패와 해결 프롬프트
실습 중 자주 만나는 오류가 있습니다. 오류가 나왔을 때는 화면을 캡처하거나 로그를 요약하지 말고, 가능하면 원문을 그대로 Codex에게 전달합니다.
Prisma Client 오류가 나면 이렇게 요청합니다.
아래 Prisma 오류를 분석해 주세요.
오류 로그:
[여기에 터미널 오류 붙여넣기]
확인할 것:
- schema.prisma와 실제 DB 마이그레이션이 어긋났는지
- npx prisma generate가 필요한지
- DATABASE_URL이 올바른지
- 어떤 명령을 어떤 순서로 실행해야 하는지
바로 파일 수정하지 말고 진단부터 해 주세요.쿠키가 저장되지 않으면 이렇게 요청합니다.
로그인은 성공하는데 브라우저에서 세션이 유지되지 않습니다.
상황:
- POST /api/auth/login 응답은 200입니다.
- /api/auth/me는 401을 반환합니다.
- 개발 환경은 http://localhost:3000입니다.
확인할 것:
- Set-Cookie 헤더가 내려오는지
- secure 옵션이 개발 환경에서 true로 설정되어 있지 않은지
- sameSite, path 설정이 적절한지
- fetch 요청에 credentials 옵션이 필요한 상황인지권한 문제가 의심되면 이렇게 요청합니다.
다른 사용자의 프로젝트 접근이 차단되는지 확인하고 싶습니다.
테스트 시나리오를 작성해 주세요.
1. 사용자 A 가입/로그인/프로젝트 생성
2. 사용자 B 가입/로그인
3. 사용자 B가 A의 projectId로 GET/PATCH/DELETE 요청
4. 기대 상태 코드 확인
필요하면 scripts/authz-test.ts 형태의 테스트 스크립트를 제안해 주세요.권한 테스트는 귀찮지만 중요합니다. CRUD가 "내 데이터만" 다루는지 확인하지 않으면 SaaS의 기본이 무너집니다.
완성 후 커밋하기
모든 검증이 끝났다면 변경 사항을 확인합니다.
git status
git diff
npm run typecheck
npm run lint
npm run buildCodex에게 커밋 전 요약을 맡깁니다.
현재 변경 사항을 커밋하기 전에 요약해 주세요.
출력:
- 변경된 파일 목록
- 기능별 변경 요약
- 실행한 검증 명령과 결과
- 아직 남은 TODO
- 추천 커밋 메시지 3개
주의:
- git commit은 직접 실행하지 마세요.추천 커밋 메시지는 이런 식이면 좋습니다.
feat: add authentication and project memo CRUD커밋은 사람이 직접 합니다.
git add .
git commit -m "feat: add authentication and project memo CRUD"이번 장에서 얻은 것
이번 장에서 만든 앱은 작지만, 실제 서비스의 핵심 구조를 거의 모두 담고 있습니다. 사용자가 있고, 로그인 세션이 있고, 사용자별 데이터가 있고, API와 폼이 연결되어 있으며, 생성·조회·수정·삭제 흐름이 있습니다. 이 구조를 한 번 경험하면 많은 웹 서비스가 비슷한 뼈대를 가지고 있다는 사실이 보입니다.
더 중요한 배움은 Codex와 협업하는 방식입니다. 인증과 CRUD를 한 번에 던지지 않고, 데이터 모델, 회원가입, 로그인, 보호된 페이지, 프로젝트 API, 메모 API, 화면, 검증, 보안 리뷰로 나눴습니다. 각 단계마다 수정 파일 범위와 성공 기준, 실패 시 확인할 로그를 지정했습니다. 이것이 초보자가 Codex를 안전하게 쓰는 방법입니다.
다음 장에서는 이 앱을 더 보기 좋고 사용하기 편하게 다듬습니다. 스크린샷과 디자인 요구사항을 Codex에게 전달해 프론트엔드 UI를 개선하고, 빈 상태, 로딩, 버튼, 카드 레이아웃, 반응형 화면을 더 실제 서비스답게 만들어 보겠습니다.
참고
1Getting Started: Route Handlers | Next.js
2Codex Cloud | OpenAI Developers
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.