프로젝트 기획 및 설계
온라인 북스토어의 사용자·문제·MVP를 정의하고 기술 스택, 화면·API 경계, 도서·주문 데이터 모델을 설계합니다.
17장에서는 앞의 장들에서 배운 Next.js의 핵심 개념과 고급 주제를 모아, 실제 웹 애플리케이션을 기획하고 설계하고 구현하는 과정을 다룹니다. 이론을 정리하는 데서 나아가 프로젝트를 시작하는 방법, 구조를 잡는 방법, 기술 스택을 고르는 기준, 기능을 구현하는 순서를 실전 기준으로 안내합니다. 이 장은 기획과 설계, 단계별 구현 가이드, 코드 리뷰와 최적화, 추가 기능 확장 제안 순서로 이어집니다.
이 글에서는 프로젝트의 기획과 설계에 초점을 맞춰, 아이디어를 구체화하고 요구사항을 정의하고 기술 스택을 고르고, 아키텍처를 설계하고 데이터를 모델링하는 흐름을 정리합니다.
프로젝트 아이디어 구체화 및 목표 설정
프로젝트를 시작할 때는 아이디어와 목표를 분명히 해야 합니다. 어떤 종류의 애플리케이션을 만들지, 누가 쓸지, 어떤 문제를 해결할지를 먼저 정리합니다.
아이디어 도출 및 선정
아이디어는 독서 기록 앱, 재료 관리 앱, 스터디 그룹 매칭 서비스, 간단한 이커머스 스토어처럼 자신이 관심 있거나 잘 아는 분야에서 찾습니다. 일상이나 업무에서 불편했던 점, 개선이 필요하다고 생각한 점을 찾아보고, 비슷한 기존 서비스가 있다면 그 장단점을 분석해 차별화할 요소를 찾습니다. 그리고 주어진 시간과 기술 역량 안에서 구현할 수 있는 아이디어를 고르며, 너무 거창한 아이디어보다 작고 핵심적인 기능부터 시작하는 것이 좋습니다.
이 장의 예시 프로젝트는 간단한 온라인 북스토어(Online Bookstore)입니다. 사용자가 쉽게 책을 검색하고, 상세 정보를 확인하고, 장바구니에 담아 주문할 수 있는 간단한 플랫폼이 필요하다는 문제에서 출발합니다. 대상 사용자는 책을 사는 데 관심 있는 일반 사용자이며, 사용하기 쉬운 화면, 빠른 검색, 간편한 구매 과정을 핵심 가치로 삼습니다.
프로젝트 목표 설정 (SMART 원칙)
고른 아이디어를 바탕으로 구체적인 목표를 정하며, SMART 원칙(Specific, Measurable, Achievable, Relevant, Time-bound)을 적용하면 좋습니다. 구체적(Specific)으로는 사용자가 책을 검색하고, 상세 페이지를 보고, 장바구니에 담아 가상으로 주문할 수 있는 웹 애플리케이션을 만드는 것입니다. 측정 가능(Measurable)한 목표는 최소 50권의 도서 데이터 구축, 검색 응답 시간 1초 이내, 장바구니와 주문 과정 구현입니다. 달성 가능(Achievable)성은 Next.js와 MongoDB나 PostgreSQL, Route Handler와 서버 액션 같은 기본 기술만으로 구현할 수 있다는 데 있습니다. 관련성(Relevant)은 Next.js에서 배운 내용을 모두 적용해 실제 서비스 개발 경험을 얻는 것이고, 기한(Time-bound)은 4주 안에 핵심 기능을 개발하고 배포하는 것입니다.
50권, 1초, 4주는 계획의 목표값입니다. 뒤의 시드는 2권짜리 예시이며, 검색 응답 시간을 측정한 결과를 보여 주는 것이 아닙니다. 측정할 때는 데이터의 규모와 기기, 네트워크 조건도 함께 정합니다.
목표와 별개로 작은 완료 기준도 정해 둡니다. 첫 실습의 완료 기준은 두 권의 도서가 보이고, 로그인한 사용자가 자기 장바구니를 바꾼 뒤 가상 주문으로 DB가 어떻게 바뀌는지 확인하는 것입니다. 50권과 1초 목표의 검사는 별도의 규모로 준비해야 합니다. 사용자의 동작, 서버에서 확인할 소유자, 저장할 값, 화면의 실패 안내를 한 흐름에 함께 적어 두면 구현 순서를 정하기 쉽습니다.
요구사항 정의 및 기능 목록 작성
프로젝트의 목표를 이루는 데 필요한 기능을 사용자 관점에서 구체적으로 정의합니다. 기능은 먼저 만들 MVP와 나중으로 미룰 추가 기능의 두 층으로 나눕니다. 각 기능을 "사용자가 무엇을 할 수 있는가"로 적고, 로그인이 필요한지와 어떤 데이터를 읽거나 바꾸는지도 함께 적어 두면 뒤의 화면 구조와 데이터 모델로 바로 이어집니다.
핵심 기능 (MVP)
최소 기능 제품(MVP, Minimum Viable Product)은 프로젝트의 핵심 가치를 전할 수 있는 최소한의 기능 묶음입니다. MVP를 먼저 개발해 빠르게 피드백을 받고, 기능을 점진적으로 넓힙니다. 이 프로젝트의 MVP는 다음과 같습니다.
- 사용자 인증: 장바구니와 주문을 사용자별로 나누기 위한 로그인, 로그아웃, 서버 세션 확인
- 도서 목록 조회: 모든 도서 목록을 페이지네이션과 함께 표시
- 도서 검색: 도서의 제목, 저자 등으로 검색
- 도서 상세 정보: 특정 도서의 제목, 저자, 설명, 가격, 이미지 등을 표시
- 장바구니: 도서 추가, 수량 변경, 도서 삭제
- 가상 주문: 장바구니의 도서를 가상으로 주문하며, 결제 시스템 연동은 MVP에서 뺍니다
추가 기능 (향후 확장 고려)
MVP 다음에 넓힐 수 있는 기능도 미리 구상해 둡니다. 사용자 리뷰와 평점, 위시리스트, 개인화된 추천 도서, 도서와 주문을 관리하는 관리자 페이지, Stripe나 Toss Payments 같은 실제 결제 시스템 연동, 국제화(i18n) 지원, 푸시 알림 등이 후보입니다. 이 가운데 몇 가지는 17장 4절에서 다시 다룹니다.
기술 스택 선정
Next.js를 바탕으로 하되, 백엔드, 데이터베이스, 스타일링, 배포까지 기술 스택 전반을 정합니다. 기술마다 고른 이유를 함께 적어 두면, 나중에 요구사항이 바뀌었을 때 어느 선택을 다시 검토할지 판단하기 쉽습니다. 아래에서는 화면을 만드는 프론트엔드, 데이터와 인증을 다루는 백엔드, 앱과 데이터베이스를 올릴 배포 환경의 순서로 정합니다.
프론트엔드 (Next.js 기반)
프레임워크는 Next.js이며, 정적인 정보 페이지는 정적 렌더링으로, 검색 결과와 사용자별 장바구니는 동적 렌더링으로, 장바구니, 주문, 검색 입력의 상호작용은 클라이언트 컴포넌트로 알맞게 나누어 씁니다. 데이터 페칭에는 fetch API와, 필요하면 SWR이나 TanStack Query를 씁니다. 안정성과 생산성을 위해 TypeScript를 쓰고, 스타일링은 CSS 모듈, Tailwind CSS, 선택적으로 Styled Components나 Emotion을 쓸 수 있지만 이 프로젝트에서는 Tailwind CSS를 주로 씁니다. 폼의 유효성 검사와 상태 관리에는 React Hook Form을 쓸 수 있습니다.
백엔드 및 API
API는 App Router의 Route Handler나 서버 액션으로 구현합니다. 간단한 애플리케이션이므로 별도의 백엔드 서버 없이 Next.js의 서버 기능을 활용합니다. 인증은 Auth.js로 소셜 로그인과 세션을 관리하며, 장바구니와 주문의 서버 액션은 인증된 사용자 ID를 기준으로 동작합니다.
데이터베이스는 NoSQL과 RDB 가운데 고릅니다. MongoDB는 스키마가 유연해 빠르게 개발할 수 있고 Mongoose ODM을 쓰며, PostgreSQL은 관계형 데이터와 안정성에 강하고 Prisma ORM이나 SQL을 직접 씁니다. 이 예시 프로젝트는 MongoDB와 Mongoose를 고릅니다.
배포
호스팅은 Next.js에 맞춰진 배포 플랫폼인 Vercel을, 데이터베이스 호스팅은 클라우드 MongoDB 서비스인 MongoDB Atlas를 씁니다. 앱과 데이터베이스가 서로 다른 서비스에 있으므로, Atlas의 연결 문자열은 다음 글의 MONGODB_URI 같은 서버 전용 환경 변수로 Vercel에 등록합니다. Atlas 쪽에서도 배포된 앱이 접속할 수 있도록 네트워크 접근 설정을 확인해야 하며, 이 연결이 막혀 있으면 빌드는 성공해도 데이터를 읽는 페이지에서 오류가 납니다.
아키텍처 설계
애플리케이션의 전체 구조와 데이터 흐름을 그림으로 정리합니다. 먼저 요청이 브라우저, Next.js 서버, 데이터베이스 사이를 어떻게 오가는지 시스템 그림으로 보고, 이어서 그 역할을 폴더와 파일에 나눕니다. 이 두 가지를 먼저 정해 두면 서버에서만 실행할 코드(DB 연결, 권한 검사)와 브라우저에서 실행할 코드(장바구니 버튼, 검색 입력)가 한 파일에 섞이지 않습니다.
시스템 아키텍처
Next · Bookstore request boundaries
북스토어의 화면·서버·저장소
화살표는 브라우저 → Next.js 서버 → MongoDB의 요청·데이터 처리 방향입니다. 세션 확인과 도서·장바구니·주문의 DB 조회·변경은 서버가 맡고, 화면에는 필요한 결과를 전달합니다.
기능의 책임을 나타낸 구조입니다. 정적 자산·캐시 응답은 CDN에서 처리할 수 있고, 렌더링과 Server Action이 별도 서버로 분리된다는 뜻은 아닙니다.
위 그림은 북스토어의 요청이 브라우저에서 Next.js 서버를 거쳐 MongoDB로 이어지는 방향을 보여 줍니다. 목록과 장바구니 UI가 있는 브라우저가 Next.js 서버에 요청하면, 렌더링, 인증, 액션을 맡은 서버가 MongoDB Atlas의 도서, 장바구니, 주문을 조회하거나 바꾸고 화면에는 필요한 결과만 전합니다. 그림의 설명처럼 이는 기능의 역할을 나타낸 구조이며, 정적 자산과 캐시된 응답은 CDN에서 처리할 수 있고, 렌더링과 서버 액션이 서로 다른 서버로 나뉜다는 뜻은 아닙니다.
Auth.js는 서버 세션에서 사용자 식별 정보를 제공하고, 장바구니와 주문 액션은 이 ID로 조회 대상을 제한합니다. 데이터베이스 연결 문자열과 권한 검사는 서버 코드에만 둡니다.
폴더/파일 구조 설계
App Router를 바탕으로 프로젝트 구조를 설계합니다. 다음 트리에서 # 뒤는 설명이며, 실제 파일이나 폴더 이름에는 넣지 않습니다.
데이터 모델링
애플리케이션이 다룰 데이터의 구조를 정의하며, 여기서는 NoSQL인 MongoDB를 기준으로 예시를 듭니다. MongoDB는 테이블 대신 컬렉션에 JSON과 비슷한 문서를 저장하고, 기본 설정에서는 문서의 모양을 데이터베이스가 강제하지 않습니다. 그래서 이 프로젝트는 Mongoose 스키마로 각 문서에 들어갈 필드와 타입을 코드에서 정합니다. 아래에서는 먼저 필요한 엔티티를 정하고, 그중 Book의 스키마를 코드로 봅니다.
주요 엔티티 식별
주요 엔티티는 네 가지입니다. Book은 도서 정보이고, CartItem은 장바구니에 담긴 도서 항목, Order는 주문 정보입니다. User는 Auth.js의 공급자와 세션이 관리하는 사용자 식별 정보이며, 이 MVP는 별도의 User 모델을 만들지 않습니다.
CartItem은 bookId와 수량을 저장해 도서의 현재 정보와 연결합니다. Order의 각 항목은 bookId와 구매 시점의 가격인 priceAtPurchase를 함께 저장하므로, 이후 도서 가격이 바뀌어도 주문 금액을 현재 가격으로 다시 계산하지 않습니다. userId 문자열은 각 장바구니와 주문의 소유자를 구분합니다.
스키마 정의 (MongoDB/Mongoose 예시)
각 스키마는 폴더 구조에서 정한 models 파일에 하나씩 나누어 둡니다. 다음 Book 모델은 그대로 복사해 쓸 수 있는 예시이며, CartItem과 Order 모델은 다음 글에서 같은 방식으로 구현합니다.
import mongoose, { Schema, type Model } from 'mongoose';
// Book 스키마 정의
export interface IBook {
_id: mongoose.Types.ObjectId;
title: string;
author: string;
description: string;
price: number;
imageUrl: string;
isbn: string;
publishedDate: Date;
genre: string[];
stock: number;
}
const BookSchema = new Schema<IBook>({
title: { type: String, required: true },
author: { type: String, required: true },
description: { type: String, required: true },
price: { type: Number, required: true },
imageUrl: { type: String, required: true },
isbn: { type: String, required: true, unique: true },
publishedDate: { type: Date, default: Date.now },
genre: [{ type: String }],
stock: { type: Number, default: 0 },
}, { timestamps: true });
const Book = (mongoose.models.Book as Model<IBook> | undefined)
?? mongoose.model<IBook>('Book', BookSchema);
export default Book;unique: true는 Mongoose의 검증기가 아니라 MongoDB의 고유 인덱스를 만들기 위한 선언이므로, 실제로 인덱스가 만들어져 있어야 중복된 ISBN을 막을 수 있습니다. 지금의 Book 스키마에는 제목이나 저자 검색용 인덱스, 가격과 재고가 음수가 아닌 정수인지 확인하는 검증이 없으므로, 이미 선언한 제약과 앞으로 추가할 정책을 구분해 둡니다.
정리
- 프로젝트는 문제, 대상 사용자, 핵심 가치를 정한 뒤 SMART 원칙으로 목표를 세우고, 목표값과 첫 실습의 작은 완료 기준을 구분합니다.
- 이 장의 북스토어 MVP는 인증, 도서 목록과 검색, 상세 정보, 장바구니, 가상 주문이며, 결제와 리뷰 같은 기능은 이후로 미룹니다.
- 기술 스택은 Next.js App Router, TypeScript, Tailwind CSS, Auth.js, MongoDB와 Mongoose, Vercel과 MongoDB Atlas입니다.
- 세션 확인과 DB 조회, 변경은 서버가 맡고, 장바구니와 주문은 서버 세션의 사용자 ID로 소유자를 구분합니다.
- 주문 항목은 구매 시점의 가격을 따로 저장하며, 스키마의
unique는 인덱스 선언일 뿐 값의 범위 검증까지 하지 않습니다.
연습 문제
1. 현재 가격과 구매 시점 가격
어떤 도서의 현재 가격이 35,000원에서 36,000원으로 바뀌었습니다. 이전 주문에 수량 2와 priceAtPurchase: 35000이 남아 있다면, 그 주문의 금액과 지금 장바구니에 같은 책 2권을 담았을 때의 금액은 각각 얼마일까요? 가격을 반드시 0 이상의 정수로 저장해야 한다는 정책은 Book 스키마에 이미 구현되어 있을까요?
해설 보기
이전 주문은 구매 가격을 보존하므로 35,000 × 2 = 70,000원이고, 현재 Book의 가격을 읽는 장바구니는 36,000 × 2 = 72,000원입니다. 금액이 다르다는 이유로 주문의 구매 가격을 현재 가격으로 덮어쓰지 않습니다.
Book 스키마의 price에는 Number와 required만 있고, 최솟값이나 정수 검사는 선언되어 있지 않습니다. 그런 업무 정책이 필요하다면 min 같은 검증과 그 검증을 확인하는 테스트를 추가해야 합니다.
2. MVP에 넣을 기능 고르기
팀원이 "리뷰와 평점이 없으면 아무도 책을 사지 않을 것"이라며 첫 배포에 리뷰 기능을 넣자고 합니다. 4주 기한과 MVP의 목적을 생각하면 어떻게 판단하는 편이 좋을까요?
해설 보기
MVP의 목적은 핵심 가치인 검색, 상세 확인, 구매 흐름을 최소한의 기능으로 먼저 전해 피드백을 받는 것입니다. 리뷰와 평점은 작성, 조회, 수정, 신고, 평점 집계, 작성자 권한 검사까지 필요해 범위가 큽니다. 4주 안에 핵심 흐름을 완성하고 배포하는 것이 먼저이므로, 리뷰는 추가 기능 목록에 두고 MVP를 배포한 뒤 사용자의 반응을 보고 우선순위를 다시 정하는 편이 좋습니다.
3. 장바구니에 가격을 저장하지 않는 이유
CartItem에 bookId와 수량만 저장하고 가격은 저장하지 않습니다. 반면 Order 항목에는 priceAtPurchase를 저장합니다. 두 모델이 다르게 설계된 이유는 무엇일까요?
해설 보기
장바구니는 아직 구매가 확정되지 않은 상태이므로, 사용자가 볼 때마다 도서의 현재 가격과 재고를 반영해야 합니다. 가격을 장바구니에 복사해 두면 도서 가격이 바뀌었을 때 오래된 가격으로 주문하는 문제가 생길 수 있습니다. 주문은 구매가 확정된 기록이므로, 이후 도서 가격이 바뀌어도 그때 실제로 낸 금액이 남아야 합니다. 그래서 주문 항목에는 구매 시점의 가격을 따로 저장합니다.
다음 글 단계별 구현 가이드에서는 이 설계를 바탕으로 프로젝트를 만들고, 인증과 데이터 모델부터 차례로 구현합니다.