[국내 최초] 인프런 풀스택 클론코딩 Part 1 - Next.js x NestJS 후기
✨ 들어가며기존에 Supabase + Next.js 기반의 사이드 프로젝트를 진행하면서, Supabase에 대한 정보가 많지 않아 시행착오를 겪었습니다. 그러던 중 로펀님의[풀스택 입문] Firebase보다 10배 좋은 Supabase와[풀스택 완성] Supabase로 웹사이트 3개 클론하기 (Next.js 14)강의를 통해 제가 놓쳤던 부분들을 많이 보완할 수 있었고, 사이드 프로젝트에도 직접 적용해볼 수 있어 좋은 경험이 되었습니다.이후 이름만 들어봤던 NestJS와 Prisma를 활용한 백엔드 구성에 대해 궁금함이 생겼고, 실제 업무에서도 써볼 수 있을 것 같다는 판단이 들어 이 강의를 수강하게 되었습니다. 🧱 강의 소개- 강의명: [국내 최초] 인프런 풀스택 클론코딩 Part 1 - Next.js x NestJS - 강사: 로펀 - 기술 스택: Next.js 15 (App Router) + NestJS + PostgreSQL + Prisma + AWS S3/CloudFront + Shadcn/UI 등실제 인프런 사이트를 클론 코딩하면서 프론트엔드와 백엔드 구조 전반을 설계/개발하는 과정 중심으로 구성되어 있습니다.💻 프로젝트 결과물🧩 사용 기술 및 인사이트✅ NestJS & Prisma 백엔드 구성강의에서는 NestJS의 의존성 주입, 모듈화, 서비스/컨트롤러 구조를 활용한 확장성 있는 백엔드 설계를 다룹니다.또한 Prisma를 활용해 아래와 같이 모델을 정의하고 PostgreSQL과 연결하여 데이터베이스 연동을 진행했습니다./backend/prisma/schema.prismadatasource db { provider = "postgresql" url = env("DATABASE_URL") } generator prismaClassGenerator { provider = "prisma-class-generator" dryRun = "false" separateRelationFields = "false" } generator client { provider = "prisma-client-js" } model Account { id String @id @default(cuid()) userId String @map("user_id") type String provider String providerAccountId String @map("provider_account_id") refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@map("accounts") } model Session { id String @id @default(cuid()) sessionToken String @unique @map("session_token") userId String @map("user_id") expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("sessions") } model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? @map("email_verified") hashedPassword String? @map("hashed_password") image String? bio String? accounts Account[] sessions Session[] courses Course[] courseEnrollments CourseEnrollment[] courseReviews CourseReview[] courseQuestions CourseQuestion[] courseComments CourseComment[] lectureActivities LectureActivity[] @@map("users") } model VerificationToken { identifier String token String expires DateTime @@unique([identifier, token]) @@map("verification_tokens") } model Course { id String @id @default(uuid()) slug String @unique title String shortDescription String? @map("short_description") description String? @map("description") thumbnailUrl String? @map("thumbnail_url") price Int @default(0) discountPrice Int? @map("discount_price") level String @default("BEGINEER") status String @default("DRAFT") instructorId String @map("instructor_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") sections Section[] lectures Lecture[] categories CourseCategory[] courseEnrollments CourseEnrollment[] instructor User @relation(fields: [instructorId], references: [id]) courseReviews CourseReview[] courseQuestions CourseQuestion[] @@map("courses") } model Section { id String @id @default(uuid()) title String description String? order Int courseId String @map("course_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) lectures Lecture[] @@map("sections") } model Lecture { id String @id @default(uuid()) title String description String? order Int duration Int? isPreview Boolean @default(false) @map("is_preview") sectionId String @map("section_id") courseId String @map("course_id") videoStorageInfo Json? @map("video_storage_info") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") section Section @relation(fields: [sectionId], references: [id], onDelete: Cascade) course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) activities LectureActivity[] @@map("lectures") } model CourseCategory { id String @id @default(uuid()) name String slug String @unique description String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") courses Course[] @@map("course_categories") } model CourseEnrollment { id String @id @default(uuid()) userId String @map("user_id") courseId String @map("course_id") enrolledAt DateTime @default(now()) @map("enrolled_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, courseId]) @@map("course_enrollments") } model CourseReview { id String @id @default(uuid()) content String rating Int userId String @map("user_id") courseId String @map("course_id") instructorReply String? @map("instructor_reply") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, courseId]) @@map("course_reviews") } model CourseQuestion { id String @id @default(uuid()) title String content String userId String @map("user_id") courseId String @map("course_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) courseComments CourseComment[] @@map("course_questions") } model CourseComment { id String @id @default(uuid()) content String userId String @map("user_id") questionId String @map("question_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") courseQuestions CourseQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("course_comments") } model LectureActivity { id String @id @default(uuid()) userId String @map("user_id") lectureId String @map("lecture_id") progress Int @default(0) isCompleted Boolean @default(false) @map("is_completed") lastWatchedAt DateTime @default(now()) @map("last_watched_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) lecture Lecture @relation(fields: [lectureId], references: [id], onDelete: Cascade) @@unique([userId, lectureId]) @@map("lecture_activities") } 이를 통해 관계형 데이터 설계 및 실제 쿼리 구현에 대한 감을 익힐 수 있었습니다. 🎨Shadcn UI + TailwindCSS + Cursor AI개인적으로는 TailwindCSS를 선호하지 않지만, 이번 강의에서 함께 다룬 Shadcn UI 라이브러리를 통해 Tailwind 기반 컴포넌트를 쉽고 빠르게 구성할 수 있다는 점을 새롭게 알게 되었습니다.실제로 로그인, 대시보드 UI 등을 구현할 때 Shadcn UI 컴포넌트를 활용해 프론트엔드를 빠르게 구성할 수 있었고, 컴포넌트의 재사용성이나 커스터마이징 측면에서도 꽤 실용적이라는 인상을 받았습니다.또한, Cursor AI의 에이전트 모드를 활용해 UI를 구현하는 노하우를 알게 되어, 실무에서도 개발 생산성을 크게 높일 수 있을 것이라는 기대감이 생겼습니다. ⚠ 시행착오 & 해결 방법NestJS를 사용하면서 Windows 환경에서 개행문자(EOL) 이슈가 발생했습니다.VSCode에서 줄 끝에 빨간 줄이 생기는 현상이 있었고, 다음과 같이 해결할 수 있었습니다.1. Ctrl + Shift + P 또는 F1 키 입력2. Change End Of Line Sequence 검색3. LF로 변경 🛫 S3 + CloudFront영상을 S3에 업로드 후 CloudFront를 통한 스트리밍 구현까지 다루어, 실제 서비스에서 사용되는 미디어 배포 방식을 경험할 수 있었습니다.✨ 느낀 점과 다음 목표이번 강의를 통해 기획 → DB 설계 → API 작성 → 프론트 연동 → 인프라(S3+ CloudFront)까지서비스 전체 흐름을 실습할 수 있었고, 프론트엔드 개발자로서의 시야를 한층 넓힐 수 있었습니다.NestJS와 Prisma를 통한 구조적인 백엔드 설계는 처음이었지만,이제는 스스로 서버를 설계할 수 있다는 자신감을 얻었습니다.✅ 마무리단순한 클론 코딩을 넘어서 실제 서비스에 근접한 구조와 스택을 경험할 수 있어 정말 유익한 강의였습니다.풀스택 개발을 지향하는 분들, 특히 프론트엔드 개발자 분들이 백엔드 기초를 잡아보기에 정말 좋은 강의입니다.👀 Part 2에서 강의 탐색·수강·수강평·Q&A 기능도 기대되며, 최종적으로 AWS 배포 강의도 무척 기대가 됩니다!