![[국내 최초] 인프런 풀스택 클론코딩 Part 1 - Next.js x NestJS 후기](https://cdn.inflearn.com/public/files/blogs/ea27c44b-165e-4a7e-ab50-9a45c383fa12/화면 캡처 2025-06-01 033705.jpg)
[국내 최초] 인프런 풀스택 클론코딩 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.prisma
datasource 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 배포 강의도 무척 기대가 됩니다!
댓글을 작성해보세요.