🔥딱 8일간! 인프런x토스x허먼밀러 역대급 혜택

[국내 최초] 인프런 풀스택 클론코딩 Part 1 - Next.js x NestJS 후기

[국내 최초] 인프런 풀스택 클론코딩 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 등

image

실제 인프런 사이트를 클론 코딩하면서 프론트엔드와 백엔드 구조 전반을 설계/개발하는 과정 중심으로 구성되어 있습니다.



💻 프로젝트 결과물

image

image



🧩 사용 기술 및 인사이트

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에서 줄 끝에 빨간 줄이 생기는 현상이 있었고, 다음과 같이 해결할 수 있었습니다.

image

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 배포 강의도 무척 기대가 됩니다!

댓글을 작성해보세요.

채널톡 아이콘