일상/IT

Next.js 비디오 플레이어 만들기: 업로드, 썸네일 자동 생성, 워터마크까지 한 번에 구현하는 방법

얇은생각 2026. 4. 5. 07:30
반응형

2026년 Next.js 비디오 플레이어 앱 구축 가이드: 업로드, 썸네일, 워터마크, 화질 제어, 그리고 확장 가능한 미디어 기반 만들기

정말 쓸 만한 Next.js 비디오 플레이어 앱을 만들고 싶다면, 단순히 페이지에 <video> 태그 하나 넣는 것으로는 부족합니다. 재생 버튼만 있다고 끝나는 문제가 아니거든요. 업로드가 있어야 하고, 썸네일이 있어야 하고, 메타데이터도 필요합니다. 파일은 안전하게 다뤄야 하고, 라이브러리 화면도 있어야 하며, 시청 페이지도 필요합니다. 미디어가 무거워졌을 때도 앱이 느려 보이지 않도록 전달 최적화까지 신경 써야 하죠.

진짜 일은 그때부터 시작됩니다.

 

그리고 2026년 기준으로 보면, 이 일의 중요성은 더 커졌습니다. 사용자는 비디오가 즉시 뜨기를 기대합니다. 모바일이든 데스크톱이든 매끈하게 보여야 한다고 생각합니다. 업로드는 당연히 잘 되어야 하고, 미리보기는 빨리 떠야 하며, 네트워크 상태가 조금 좋지 않아도 재생은 자연스럽게 이어지길 원합니다. 이제 “비디오 기능”은 단일 기능이 아닙니다. 작은 규모의 미디어 시스템이라고 보는 편이 훨씬 정확합니다.

이 글에서는 Next.js, TypeScript, App Router, 그리고 ImageKit 스타일의 미디어 전달 방식을 바탕으로, 그런 시스템의 실전형 기반을 어떻게 만드는지 차근차근 설명합니다. 단순하게 가져갈 수 있는 부분은 최대한 단순하게, 조심해야 할 부분은 충분히 신중하게 설계합니다. 실제 비디오 파일, 썸네일, 워터마크 이미지는 저장·전달·최적화·변환에 특화된 미디어 플랫폼에 맡기고, 앱은 제목, 설명, 타임스탬프, 워터마크 설정 같은 메타데이터를 직접 관리합니다. 그래야 제품 로직의 주도권을 앱이 계속 쥘 수 있습니다.

 

앞으로 설명할 모든 내용은 이 분리 원칙 위에서 움직입니다.

 

비디오 앱은 단순한 플레이어가 아닙니다. 저장소, 보안, 메타데이터, 전달 최적화, UX가 하나의 화면 아래에서 함께 돌아가는 시스템입니다.

 

이 글은 React나 Next.js를 처음 배우는 입문서를 목표로 하지 않습니다. 기본적인 TypeScript, React 상태 관리, App Router 규칙, 서버 라우트, client component, 환경 변수 개념은 이미 알고 있다고 가정합니다. 여기서의 핵심은, 아무 기능도 없는 빈 Next.js 앱을 실제로 쓸 수 있는 비디오 플랫폼의 기반으로 바꾸는 설계와 구현 판단입니다.

 

 


 

어두운 SaaS 스타일의 데스크톱 화면 중앙에 해변 일몰 영상이 있는 대형 비디오 플레이어가 보이고, 주변에 보안 업로드, 클라우드 전송, 화질 설정, 워터마크를 상징하는 아이콘이 배치된 현대적인 비디오 플랫폼 인터페이스.

 

이 비디오 앱으로 실제로 무엇을 할 수 있나

이 구성을 마치고 나면, 사용자가 실제로 필요로 하는 핵심 기능이 갖춰진 작동 가능한 기반이 생깁니다.

  • 비디오 파일 업로드
  • 선택적으로 썸네일 업로드
  • 선택적으로 워터마크 이미지 업로드
  • 다음과 같은 비디오 메타데이터 저장
    • title
    • description
    • file path
    • file name
    • thumbnail path
    • 생성일
    • 워터마크 설정

 

  • 업로드된 콘텐츠를 비디오 라이브러리에서 보여주기
  • 각 비디오를 전용 시청 페이지에서 열기
  • 재생 전 포스터 이미지 표시
  • 브라우저 기본 재생 제어 지원
    • 재생/일시정지
    • 볼륨 조절
    • 전체 화면
    • 재생 속도 변경
    • 지원되는 경우 PIP(Picture-in-Picture)
    • 지원되는 경우 다운로드

 

  • 사용자가 비디오 화질을 직접 변경할 수 있도록 제공
  • 필요할 경우 출력 포맷 변경
  • 썸네일을 따로 올리지 않았을 때, 비디오 첫 프레임으로 자동 썸네일 생성
  • 이미지 워터마크를 비디오 오버레이로 적용
  • 이후 프로덕션 수준의 앱으로 확장할 수 있도록 유연한 기반 유지

 

마지막 항목이 특히 중요합니다. 이것은 완성형 서비스가 아닙니다. 하지만 나중에 확장 가능한, 단단한 출발점은 분명히 됩니다.

 

 


비디오 앱이 생각보다 빨리 복잡해지는 이유

많은 개발자가 비디오를 과소평가합니다. 시작만 보면 쉬워 보이기 때문이죠. 비디오 엘리먼트를 하나 넣고, 파일 하나 연결하면 일단 재생은 되니까요.

그런데 거기서 끝이 아닙니다.

 

“파일 하나 재생하기”에서 “실제로 쓸 수 있는 비디오 제품 만들기”로 넘어가는 순간, 복잡도는 갑자기 뛰어오릅니다. 그때부터는 이런 것들이 필요해집니다.

  • 안전한 업로드
  • 파일 크기 처리
  • 썸네일
  • 시청 페이지
  • 메타데이터 저장
  • 목록 화면
  • 더 빠른 첫 로딩
  • 더 나은 전달 방식
  • 오버레이나 브랜드 요소
  • 포맷 처리
  • 확장 가능한 미디어 인프라

 

그래서 비디오 앱은 단순한 이미지 갤러리나 정적 콘텐츠 페이지와는 본질적으로 다릅니다. 무겁고, 상태가 많고, 전달 성능의 영향을 훨씬 크게 받습니다.

비유하자면 이렇습니다. 이미지 앱은 대개 “보여주는 것”에 가깝고, 비디오 앱은 “조율하는 것”에 가깝습니다. 저장, 재생, 변환, 사용자 경험이 동시에 맞물려 돌아가야 합니다.

 

이걸 전부 직접 만들려고 들면, 금세 아래 같은 것들을 다시 만들고 있는 자신을 발견하게 됩니다.

  • 스토리지 레이어
  • 미디어 변환 서비스
  • CDN
  • 업로드 인증
  • 메타데이터 영속화
  • 처리 파이프라인 비슷한 무언가

 

그래서 미디어 플랫폼을 쓰는 게 훨씬 합리적입니다. 미디어 플랫폼은 미디어에 강하고, 애플리케이션은 제품 로직에 강하면 됩니다.

 

무거운 파일은 미디어 인프라가 잘 처리하는 곳에 두고, 그 파일의 의미는 애플리케이션이 이해할 수 있는 곳에 두는 것이 좋습니다.

 

 


 

이 아키텍처를 아주 쉽게 풀어보면

파일과 라우트 구조로 들어가기 전에, 먼저 큰 그림부터 보는 편이 좋습니다.

이 앱은 미디어 자산애플리케이션 메타데이터를 명확하게 분리합니다.

 

미디어 자산은 미디어 플랫폼에 둡니다

여기에는 다음이 포함됩니다.

  • 비디오 파일
  • 업로드된 썸네일
  • 워터마크 이미지

 

미디어 플랫폼은 다음 역할을 맡습니다.

  • 저장
  • 전달
  • 캐싱
  • 최적화
  • URL 기반 변환

 

메타데이터는 앱이 관리합니다

여기에는 다음이 포함됩니다.

  • title
  • description
  • file path
  • file name
  • thumbnail path
  • 생성일
  • 선택적인 watermark config
  • 선택적인 화질 및 포맷 선호값

 

첫 번째 버전에서는 완전한 데이터베이스 대신 로컬 JSON 파일에 메타데이터를 저장합니다. 물론 진지한 멀티 유저 프로덕션 서비스를 영원히 이렇게 운영하자는 뜻은 아닙니다. 다만, 아키텍처를 빠르게 검증하기에는 아주 영리한 선택입니다.

 

프론트엔드가 비밀 키로 직접 업로드하지는 않습니다

대신 흐름은 이렇게 갑니다.

  1. 프론트엔드가 백엔드에 업로드 권한을 요청합니다.
  2. 백엔드는 private credential을 이용해 임시 업로드 토큰을 만듭니다.
  3. 프론트엔드는 그 임시 토큰으로 파일 하나를 업로드합니다.
  4. 이후 메타데이터는 여러분의 API를 통해 저장합니다.

 

이게 중요한 이유는 간단합니다. 브라우저에 private media key를 노출하는 건, 사실상 아무나 와서 스토리지를 악용할 수 있게 문을 열어두는 것과 비슷하기 때문입니다.

 

 


사용자 입장에서 보면 경험은 이렇게 흘러갑니다

이 앱을 이해하는 가장 좋은 방법은 사용자 흐름을 따라가 보는 것입니다.

 

1. 사용자가 업로드 페이지에 들어옵니다

입력할 수 있는 값은 다음과 같습니다.

  • 비디오 제목
  • 비디오 설명
  • 비디오 파일
  • 선택적인 썸네일 파일
  • 선택적인 워터마크 파일

 

2. 앱이 선택된 자산을 업로드합니다

  • 비디오는 videos 폴더로 들어갑니다.
  • 썸네일은 thumbnails 폴더로 들어갑니다.
  • 워터마크는 watermarks 폴더로 들어갑니다.

 

3. 앱이 메타데이터를 저장합니다

제목, 설명, 파일 경로, 파일 이름, 생성일, 워터마크 설정을 메타데이터 저장소에 기록합니다.

 

4. 라이브러리 페이지가 갱신됩니다

업로드된 비디오는 다음 정보를 담은 카드로 노출됩니다.

  • 썸네일
  • 제목
  • 날짜

 

5. 사용자가 시청 페이지를 엽니다

플레이어는 다음 요소를 불러옵니다.

  • 포스터 이미지
  • 실제 비디오
  • 재생 제어 UI
  • 선택적 워터마크 오버레이
  • 조절 가능한 화질
  • 선택적 포맷 변환

 

썸네일을 한 번도 업로드하지 않았더라도 괜찮습니다. 앱이 비디오 첫 프레임으로 썸네일을 자동 생성하면 되니까요.

이런 식의 유연한 fallback이 있어야 도구가 “어쩌다 되는 수준”이 아니라 “완성도 있게 느껴지는 수준”으로 올라갑니다.

 

 


Step 1: Next.js 프로젝트 초기화

먼저 새 Next.js 앱을 만듭니다. 프로젝트 이름은 my-video-player 같은 임시 이름이면 충분합니다.

npx create-next-app my-video-player

 

이 명령을 실행하려면 당연히 Node.js가 먼저 설치되어 있어야 합니다.

설정은 다음처럼 가져가면 됩니다.

  • TypeScript: Yes
  • ESLint: Yes
  • React Compiler: No
  • Tailwind: No
  • Use code inside src directory: Yes
  • Use App Router: Yes
  • Customize import alias: No

 

이 조합은 꽤 깔끔합니다. 여기서는 Tailwind를 쓰지 않습니다. 스타일은 독립적인 CSS 파일로 분리해서 다룰 예정이기 때문입니다. 덕분에 관심사가 유틸리티 클래스가 아니라 미디어 처리 로직에 더 잘 모입니다.

프로젝트를 만든 뒤에는 반드시 프로젝트 디렉터리로 이동한 다음 패키지를 설치해야 합니다.

cd my-video-player

 

사소해 보이지만 중요한 부분입니다. 잘못된 디렉터리에서 패키지를 설치하는 건, 아주 지루하지만 시간을 꽤 많이 잡아먹는 실수 중 하나입니다.

 

 


Step 2: 핵심 패키지 설치하기

이 기반을 만들기 위해 필요한 패키지는 두 개입니다.

 

@imagekit/next

이 패키지는 다음 역할을 맡습니다.

  • 업로드
  • 이미지 컴포넌트
  • 비디오 컴포넌트
  • URL 생성
  • 서버 측 업로드 인증 파라미터 생성

 

설치는 이렇게 합니다.

npm i @imagekit/next

 

uuid

각 비디오에 고유 식별자를 부여하기 위해 사용합니다.

npm i uuid

 

이 두 패키지만으로도 핵심 흐름은 충분히 만들 수 있습니다.

 

 


Step 3: 미디어 플랫폼과 환경 변수 구성

미디어 계정을 만든 뒤 대시보드에서 developer options 영역으로 들어갑니다. 필요한 값은 그곳에 있습니다.

핵심 환경 변수는 다음 세 가지입니다.

  • NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT
  • IMAGEKIT_PUBLIC_KEY
  • IMAGEKIT_PRIVATE_KEY

 

각 변수의 역할

NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT

이 값은 프론트엔드에 공개되어도 됩니다. 이미지와 비디오 컴포넌트가 전달 URL을 조합할 때 사용합니다.

 

IMAGEKIT_PUBLIC_KEY

업로드 인증 정보를 생성할 때 쓰입니다. 필요한 경우 프론트엔드 업로드 흐름 안으로 전달될 수 있습니다.

 

IMAGEKIT_PRIVATE_KEY

이 값은 반드시 서버에만 남아 있어야 합니다. 절대 브라우저에 직접 노출되면 안 됩니다.

이 분리는 타협할 수 없습니다.

 

아주 흔한 실수 하나

환경 변수를 바꾸거나 추가했는데도 아무것도 안 되는 것처럼 보이면, 개발 서버를 다시 시작하세요.
정말로요. 그냥 다시 시작하면 됩니다.

썸네일이 안 뜨거나 미디어가 로드되지 않는 문제는 대체로 둘 중 하나입니다.

  • 환경 변수 이름을 잘못 적었거나
  • 개발 서버를 재시작하지 않았거나

 

이런 문제는 처음엔 왠지 복잡해 보이는데, 원인을 알고 나면 너무 평범해서 허탈할 정도입니다.

 

 


Step 4: 앱 구조 뼈대 만들기

디렉터리 구조가 명확하면 전체 흐름을 이해하기 훨씬 쉬워집니다.

src 아래에 다음 폴더를 만듭니다.

  • lib
  • styles
  • types
  • components

 

그리고 components 안에는 video, 그 안에는 다시 player 폴더를 둡니다.

대략적인 구조는 이렇게 가져가면 됩니다.

src/
  app/
    api/
      upload-auth/
        route.ts
      videos/
        route.ts
        [id]/
          route.ts
    upload/
      page.tsx
    watch/
      [id]/
        page.tsx
    page.tsx
  components/
    video/
      video-upload.tsx
      video-library.tsx
      player/
        simple-player.tsx
  lib/
    video-storage.ts
  styles/
    globals.css
    library.css
    upload.css
    watch.css
  types/
    video.ts

 

 

네이밍에 대한 짧은 메모

처음에 라이브러리 목록 컴포넌트를 video-player.tsx처럼 이름 붙여도 기술적으로는 동작합니다. 하지만 정직한 이름은 아닙니다. 이 컴포넌트는 플레이어가 아니라 목록 화면이니까요. video-library.tsx로 바꾸면 나중에 코드를 다시 봤을 때 훨씬 이해하기 쉽습니다.

 

스타일 파일은 분리해서 관리합니다

스타일 계층은 기능 로직과 의도적으로 분리합니다. 실전에서는 다음 네 개 CSS 파일을 미리 준비해 두는 방식이 효율적입니다.

  • globals.css
  • library.css
  • upload.css
  • watch.css

 

이렇게 해두면 업로드 흐름, 데이터 이동, 재생 동작에 더 집중할 수 있습니다. 시각적인 클래스 하나하나를 처음부터 모두 손으로 쓰는 데 시간을 쏟지 않아도 됩니다.

 

기본 Scaffold 정리하기

자동 생성된 자산 중 더 이상 쓰지 않는 것들은 정리합니다.

  • 더는 사용하지 않는 기본 CSS module import
  • 필요 없는 아이콘 파일

 

예를 들어 홈 페이지에서 아직도 page.module.css를 import하고 있어서 오류가 난다면, 그 import를 지우고 잠시 fragment나 아주 단순한 컴포넌트만 반환하도록 바꾸면 됩니다. 이런 정리가 보통 부트스트랩 직후 첫 번째 정리 작업이 됩니다.

 

 


 

Step 5: 데이터 모델 정의하기

무언가를 저장하기 전에, 이 앱에서 “비디오”가 정확히 무엇인지부터 분명하게 정의해야 합니다.

 

Video 인터페이스

비디오 메타데이터에는 다음 필드가 들어가는 것이 좋습니다.

  • id: string
  • title: string
  • description: string
  • filePath: string
  • fileName: string
  • thumbnailPath?: string
  • duration?: number
  • createdAt: string
  • watermark?: WatermarkConfig
  • quality?: number
  • format?: "auto" | "mp4" | "webm"

 

이 형태는 깔끔하고 제품 친화적입니다. “앱이 비디오에 대해 알아야 할 것”을 정리한 것이지, 미디어 플랫폼의 내부 구조를 억지로 그대로 따라가려는 설계는 아닙니다.

 

WatermarkConfig 인터페이스

워터마크는 다음처럼 정의합니다.

  • imagePath: string
  • position: "top_left" | "top_right" | "bottom_left" | "bottom_right"
  • opacity?: number
  • width?: number

 

이렇게 해두면 워터마크 동작이 명시적이고 재사용 가능해집니다.

게다가 나중을 생각해도 좋습니다. 사용자가 워터마크 위치를 직접 고르게 하고 싶나요? 이미 준비돼 있습니다. 브랜드 템플릿별로 너비를 다르게 주고 싶나요? 어렵지 않습니다. 나중에 opacity 슬라이더를 붙이고 싶나요? 필드가 이미 있으니 연결만 하면 됩니다.

 

 


 

Step 6: 우선은 단순한 메타데이터 저장소로 시작하기

프로덕션 환경이라면 결국 실제 데이터베이스로 가게 될 가능성이 높습니다.

하지만 첫 번째 작동 버전에서는 로컬 JSON 파일이 꽤 똑똑한 선택입니다.

예를 들어 이런 파일을 둡니다.

data/videos.json

 

이 파일은 가벼운 메타데이터 저장소 역할을 합니다. 실제 비디오 파일이나 이미지 자산은 저장하지 않습니다. 앱이 필요로 하는 메타데이터만 저장합니다.

 

왜 이 방식이 유용한가

일단 구조를 끝까지 만들 수 있을 만큼 단순해집니다.

이게 생각보다 훨씬 중요합니다.

너무 많은 개발자가 핵심 제품 흐름이 정말 괜찮은지 확인도 하기 전에 PostgreSQL, 오브젝트 스토리지 스키마, 관리자 도구, 백그라운드 작업부터 뛰어듭니다. 반면 로컬 JSON 저장소는 경험 자체를 먼저 검증하게 해줍니다.

 

저장 모듈이 맡아야 할 역할

video-storage.ts는 크게 세 가지 역할만 맡으면 됩니다.

 

1. readData()

  • data 디렉터리가 존재하는지 확인
  • videos.json 파일이 존재하는지 확인
  • 없으면 아래 형태로 초기화
  • { "videos": [] }
  • UTF-8로 파일 읽기
  • JSON 파싱

 

2. writeData(data)

  • JSON.stringify(data, null, 2)로 직렬화
  • 파일에 다시 기록

 

3. 외부에 공개할 헬퍼 함수

  • getAllVideos()
  • getVideoById(id)
  • saveVideo(video)

 

이 정도면 데이터베이스를 붙이지 않고도 충분히 이해 가능한 영속화 레이어가 생깁니다.

 

왜 JSON.stringify(..., null, 2)가 중요한가

사람이 읽을 수 있는 데이터는 생각보다 훨씬 가치가 큽니다.

들여쓰기된 JSON은 개발 중에 직접 열어 확인하기가 쉽습니다. 특히 ID, 파일 경로, 타임스탬프가 제대로 저장되고 있는지 검증할 때 큰 도움이 됩니다.

 

 


 

Step 7: 서버가 발급한 토큰으로 업로드를 보호하기

이건 전체 설계에서 가장 중요한 결정 중 하나입니다.

프론트엔드에서 private media key로 직접 업로드하도록 만들면 안 됩니다.

대신 아래와 같은 API 라우트를 만듭니다.

/app/api/upload-auth/route.ts

 

이 라우트는 다음을 해야 합니다.

  1. IMAGEKIT_PRIVATE_KEY 읽기
  2. IMAGEKIT_PUBLIC_KEY 읽기
  3. 두 값이 모두 존재하는지 검증
  4. 서버 측 헬퍼로 업로드 인증 파라미터 생성
  5. 생성된 인증 값을 JSON으로 반환

 

응답에는 대략 이런 값들이 포함됩니다.

  • token
  • signature
  • expiration time
  • public key

 

왜 이게 중요한가

프론트엔드는 공개 영역이고, 서버는 아닙니다.

브라우저에 private upload credential을 실어 보내는 순간, 누구나 그것을 들여다보고 사용할 수 있습니다. 그러면 별도 보호 장치가 없는 한 아무나 여러분의 미디어 계정에 파일을 업로드할 수 있게 됩니다.

 

반면 서버가 발급한, 만료 시간이 있는 업로드 토큰은 훨씬 안전합니다. 누가 업로드할 수 있는지, 얼마나 자주 가능한지, 어떤 조건에서 허용할지를 앱이 직접 통제할 수 있게 되죠.

쉽게 말하면 이런 겁니다. 백엔드는 건물 전체 열쇠를 건네주는 게 아닙니다. 특정 목적에만 잠깐 쓸 수 있는 보관표 같은 것을 주는 겁니다.

좋은 업로드 보안은 업로드를 불가능하게 만드는 데 있지 않습니다. 업로드가 의도된 흐름 안에서만 일어나도록 만드는 데 있습니다.

 

이 라우트의 에러 처리

키가 없으면 아래 같은 메시지를 반환하면 됩니다.

  • "ImageKit keys not configured"

 

인증 정보 생성에 실패하면 아래 같은 메시지를 반환하면 됩니다.

  • "Failed to generate credentials"

 

이 라우트는 신뢰와 편의 사이를 연결해 주는 다리 같은 역할을 합니다.

 

 


 

Step 8: 비디오 API 만들기

이제 API 레이어가 두 개 더 필요합니다.

  • 전체 비디오를 다루는 API
  • ID로 단일 비디오를 가져오는 API

 

/api/videos

이 라우트는 두 가지를 처리합니다.

  • GET → 전체 목록 반환
  • POST → 새 비디오 저장

 

GET 동작

응답은 다음처럼 반환하면 됩니다.

{ "videos": [...] }

 

문제가 생기면 다음을 반환합니다.

  • "Failed to fetch videos" 같은 에러 메시지
  • 상태 코드 500

 

POST 동작

요청 본문을 읽어서 새로운 Video 객체를 만듭니다.

기본값은 예를 들어 이렇게 둘 수 있습니다.

  • title이 비어 있으면 → "Untitled Video"
  • description이 비어 있으면 → ""
  • thumbnail path가 없으면 → ""
  • createdAt → new Date().toISOString()

 

ID는 uuidv4()로 생성합니다.

요청에 워터마크 객체가 있으면 저장하고, 없으면 undefined로 둡니다.

그 다음 저장 헬퍼로 객체를 저장하고, 상태 코드 201과 함께 반환합니다.

에러가 나면 다음을 반환합니다.

  • "Failed to create video"
  • 상태 코드 500

 

/api/videos/[id]

이 라우트는 ID로 단일 비디오를 가져옵니다.

동적 라우트 파라미터를 읽고, 해당 비디오를 조회한 뒤 있으면 반환합니다.

비디오가 없으면:

  • "Video not found"
  • 상태 코드 404

 

조회 중 에러가 나면:

  • "Failed to fetch video"
  • 상태 코드 500

 

이 정도면 라이브러리 페이지와 시청 페이지를 모두 구동하기에 충분합니다.

 

 


 

Step 9: 전역 레이아웃 구성하기

루트 레이아웃은 두 가지 중요한 역할을 맡습니다.

  1. 앱 전체를 media provider로 감싸기
  2. 모든 페이지에 일관된 내비게이션 껍데기 제공하기

 

ImageKitProvider로 앱 감싸기

public URL endpoint를 다음 값으로 전달합니다.

  • process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT

 

이렇게 해두면 앱 전체에서 미디어 관련 컴포넌트를 훨씬 쉽게 사용할 수 있습니다.

 

단순한 내비게이션 추가하기

최소 구성으로도 충분합니다.

  • / → Library
  • /upload → Upload

 

이것만 있어도 앱의 구조가 바로 생깁니다. 사용자는 지금 어디 있는지, 다음에 어디로 갈 수 있는지 항상 알 수 있습니다.

이런 레이아웃은 단순해 보이지만 실제로 꽤 많은 일을 해줍니다. 경험을 표준화하고, 각 페이지에서 중복되는 구조를 덜어내 줍니다.

 

 


 

Step 10: 업로드 경험 구현하기

이제 앱이 비로소 살아 움직이기 시작합니다.

video-upload.tsx는 반드시 client component여야 합니다. 다음이 필요하기 때문입니다.

  • 제어된 입력값
  • 파일 선택
  • 비동기 업로드 상태
  • 에러 상태
  • 클라이언트 측 이동

 

그래서 파일 맨 위에는 이렇게 선언합니다.

"use client"

 

필요한 상태

업로드 폼 상태는 작게 유지하는 편이 좋습니다.

 

Form 상태

  • title
  • description

 

파일 상태

아래 항목을 담는 record 구조

  • video
  • thumbnail
  • watermark

 

UI 상태

  • loading
  • error

 

그리고 업로드 성공 후 시청 페이지로 보내기 위해 useRouter()를 사용합니다.

 

업로드 헬퍼

uploadFile(file, folder) 같은 헬퍼를 만들어 다음 흐름으로 동작하게 합니다.

  1. /api/upload-auth 호출
  2. 인증 JSON 읽기
  3. 미디어 SDK로 파일 업로드
  4. 알맞은 폴더에 파일 배치

 

폴더는 이렇게 나누면 좋습니다.

  • videos → /videos
  • thumbnails → /thumbnails
  • watermarks → /watermarks

 

지금은 작아 보이지만, 라이브러리가 커지면 이런 구조가 정말 많이 도움 됩니다.

 

폼 필드 구성

업로드 폼은 다음 입력을 받아야 합니다.

  • Title — 텍스트 입력
  • Description — 텍스트 입력
  • Video file — 필수, video/*
  • Thumbnail file — 선택, image/*
  • Watermark file — 선택, image/*

 

에러가 있다면 폼 상단 근처에 눈에 띄게 보여주는 것이 좋습니다.

제출 버튼은 다음 경우 비활성화합니다.

  • 업로드가 이미 진행 중일 때
  • 비디오 파일이 선택되지 않았을 때

 

그리고 요청이 진행되는 동안 버튼 텍스트를 Upload에서 **Uploading...**으로 바꿔 줍니다.

 

파일 변경 핸들러

재사용 가능한 setFile(key) 헬퍼가 이상적입니다. 이 함수는 input change handler를 반환하고, 선택된 파일을 해당 key에 저장합니다.

  • video
  • thumbnail
  • watermark

 

덕분에 거의 똑같은 핸들러를 세 번 쓰지 않아도 됩니다.

 

제출 흐름

핵심 흐름은 아래와 같습니다.

  1. 기본 폼 제출 동작 막기
  2. 비디오 파일이 있는지 확인
  3. 에러 상태 초기화
  4. loading을 true로 변경
  5. Promise.all로 자산 병렬 업로드
  6. 수집한 메타데이터를 /api/videos로 전송
  7. 성공 시 /watch/[id]로 이동
  8. 실패 시 읽기 쉬운 에러 메시지 표시
  9. 마지막에 loading을 false로 복구

 

왜 Promise.all이 잘 맞는가

비디오, 썸네일, 워터마크는 서로 독립적인 업로드입니다. 순차적으로 처리할 이유가 없습니다.

그러니 Promise.all로 다음을 병렬 처리하면 됩니다.

  • 비디오 업로드
  • 썸네일 업로드(있는 경우)
  • 워터마크 업로드(있는 경우)

 

이렇게 하면 UI도 더 빨라지고 구현도 더 깔끔해집니다.

 

메타데이터 Payload

업로드가 끝나면 백엔드로 다음 정보를 POST 합니다.

  • title
  • description
  • filePath
  • fileName
  • thumbnailPath
  • watermark

 

워터마크가 있다면 객체는 이렇게 구성하면 됩니다.

  • imagePath
  • position: "bottom_right"
  • opacity: 0.7
  • width: 120

 

워터마크가 없으면 undefined를 보내고, 나중에 플레이어에서 해당 변환만 건너뛰게 하면 됩니다.

이 조건부 구조가 예쁜 이유는 시청 페이지가 단순해지기 때문입니다. 워터마크가 있으면 오버레이를 적용하고, 없으면 아무것도 하지 않으면 끝입니다.

 

아주 현실적인 예외 상황 하나

테스트용 비디오가 너무 크면 업로드에 실패할 수 있습니다. 특히 무료 플랜에서는 더 그렇습니다. 이 구현 기준으로 체감된 실제 제한은 계정 조건에 따라 대략 20MB~40MB 수준이었습니다.

이건 실제 사용자도 그대로 겪는 문제입니다. 그래서 나중에는 아마 이런 것들을 추가하고 싶어질 겁니다.

  • 클라이언트 측 파일 크기 검증
  • 더 나은 업로드 에러 메시지
  • 필요하다면 플랜 제한 안내

 


Step 11: 업로드 페이지 추가하기

업로드 페이지 자체는 아주 작게 유지해도 됩니다.

필요한 건 딱 두 가지입니다.

  • Upload Videos 같은 페이지 제목
  • 그 아래에 VideoUpload 컴포넌트

 

이 패턴은 Next.js에서 계속 써먹기 좋습니다. 페이지는 가볍게 두고, 실제 상호작용 로직은 컴포넌트 안으로 밀어 넣는 방식이죠.

 

 


Step 12: 비디오 라이브러리 만들기

이제 업로드된 콘텐츠를 보여줄 차례입니다.

라이브러리 컴포넌트는 다음 props를 받아서:

  • videos: Video[]

 

클릭 가능한 카드 그리드 형태로 렌더링합니다.

 

빈 상태는 꼭 챙겨야 합니다

비디오가 하나도 없을 때 빈 페이지를 보여주면 안 됩니다. 대신 아래를 렌더링하세요.

  • 친절한 empty state 메시지
  • 업로드 페이지로 가는 링크

 

아주 작은 디테일처럼 보여도 UX에서는 영향이 큽니다. 비어 있는 제품 화면은 언제나 “지금 무엇을 하면 되는지” 알려줘야 합니다.

 

각 카드에 들어갈 정보

라이브러리 카드에는 다음이 들어갑니다.

  • 썸네일
  • 비디오 제목
  • 업로드 날짜

 

카드 전체는 다음 경로로 링크되면 됩니다.

/watch/[video.id]

 

썸네일 로직: 업로드 우선, 자동 생성 차선

썸네일 소스는 fallback 구조를 따르는 것이 좋습니다.

  1. video.thumbnailPath가 있으면 그것을 사용
  2. 없으면 다음 경로로 첫 프레임 썸네일 생성
  3. ${video.filePath}/ik-thumbnail.jpeg

 

이건 전체 구조에서 가장 쓸모 있는 기능 중 하나입니다.

업로더가 썸네일 필드를 건너뛰더라도, 앱 화면이 텅 비어 보이지 않기 때문입니다.

 

자동 썸네일 생성이 특히 좋은 이유

사용자는 일관되지 않습니다.

누군가는 매번 썸네일을 올립니다.
누군가는 절대 올리지 않습니다.
누군가는 깜빡합니다.
누군가는 테스트하면서 아무 파일이나 넣습니다.
누군가는 모바일에서 올리기 때문에 단계를 최소화하고 싶어 합니다.

자동 썸네일 생성은 이런 인간적인 불규칙함으로부터 UI를 지켜줍니다.

 

라이브러리에서 이미지 변환 적용하기

미디어 이미지 컴포넌트를 쓰고, 썸네일에는 다음 정도의 변환을 적용하면 됩니다.

  • width: 320
  • height: 180

 

이렇게 하면 깔끔한 16:9 카드 썸네일이 나옵니다.

여기서 URL 기반 변환의 힘이 정말 잘 드러납니다. 이미지를 직접 리사이즈하는 게 아니라, 필요한 크기로 전달해 달라고 미디어 레이어에 요청하는 방식이기 때문입니다.

 

날짜 표시하기

저장된 createdAt 값은 ISO 문자열입니다. 날짜만 보여주고 싶다면 날짜 부분만 깔끔하게 추출하면 됩니다.

이렇게 하면 전체 타임스탬프를 다 보여주지 않고도, 사용자가 “언제 올라온 영상인지” 빠르게 파악할 수 있습니다.

 

성능과 관련된 중요한 디테일 하나

자동 생성 썸네일은 첫 요청 시 약간 시간이 걸릴 수 있습니다. 정상입니다. 시스템이 썸네일을 만들고 캐시에 올려야 하니까요.

그 다음부터는 훨씬 빠르게 뜹니다.

이건 변환 기능을 다룰 때 꼭 알아야 할 감각입니다.

  • 첫 요청 = 어느 정도의 처리 비용 발생
  • 이후 요청 = 캐시된 결과 빠르게 전달

 

이 개념을 이해하면 “왜 한 번은 느렸고 두 번째부터는 빨라졌지?” 같은 순간이 훨씬 덜 낯설어집니다.

 

 


Step 13: 진짜 플레이어 만들기

이제 핵심입니다.

simple-player.tsx는 client component로 만들고, 두 개의 props를 받습니다.

  • video
  • urlEndpoint

 

이 컴포넌트의 역할은 다음 요소들을 하나로 묶는 것입니다.

  • 원본 미디어 파일
  • 선택적 변환
  • 포스터 이미지
  • 재생 제어
  • 사용자가 바꿀 수 있는 화질 설정

 

먼저 아주 작은 경로 헬퍼부터

오버레이 입력값을 만들 때 경로 앞의 슬래시가 문제를 일으키는 경우가 있습니다. 그래서 앞의 슬래시를 제거하는 작은 헬퍼가 있으면 좋습니다.

 

예를 들어 경로가:

/abc/def.png

 

이렇게 생겼다면, 헬퍼로 다음처럼 바꿉니다.

abc/def.png

 

이렇게 하면 오버레이 입력 포맷이 더 안정적입니다.

 

화질 상태 추가하기

개념적으로 화질 상태는 이렇게 초기화하면 됩니다.

  • 비디오에 저장된 quality가 있으면 그 값을 사용
  • 없으면 기본값 50

 

왜 50이냐고요? 테스트와 데모에 쓰기 좋은, 무난한 중간값이기 때문입니다.

 

화질 제어가 중요한 이유

화질은 단순히 설정 화면에 있는 숫자가 아닙니다. 실제 파일 전달 방식에 영향을 줍니다.

화질을 낮추면 보통:

  • 더 빨리 로드되고
  • 전송량이 줄고
  • 압축 흔적이 더 보일 수 있습니다

 

반대로 화질을 높이면 보통:

  • 파일이 커지고
  • 로딩이 느려지고
  • 화면이 더 깨끗해집니다

 

그런데 실제로는 이 균형이 생각보다 꽤 관대합니다. 예를 들어 80 정도만 되어도 100보다 체감상 더 빨리 로드되면서, 대부분의 사용자 눈에는 거의 차이가 안 날 수 있습니다.

반대로 10 정도까지 낮추면 거칠어 보이기 시작하지만, 대신 훨씬 빨라질 수 있습니다.

핵심은 바로 그 체감입니다. 개발자든 사용자든, 이 숫자가 어떤 트레이드오프를 만드는지 직접 느낄 수 있게 되죠.

가장 빠른 비디오 플레이어는 화려한 제어 UI를 가진 플레이어가 아니라, 사용자가 기다림을 느끼기 전에 더 적은 데이터를 요청하는 플레이어입니다.

 

화질 범위 제한하기

화질은 다음 범위로 제한합니다.

  • 최소값: 1
  • 최대값: 100

 

화질 입력 핸들러는 다음을 해야 합니다.

  • 숫자 읽기
  • 유효하지 않은 값 거르기
  • 허용 범위로 clamp 하기
  • 반올림 처리

 

그래야 말도 안 되는 값이나 깨진 입력이 최종 전달 URL을 망치지 않습니다.

 

변환 리스트 구성하기

이제 플레이어가 강력해지는 지점입니다.

먼저 항상 포함되는 변환은:

  • 현재 선택된 화질

 

그리고 조건부로 다음을 추가합니다.

  • video.format이 있으면 포맷 변환
  • video.watermark가 있으면 워터마크 오버레이 변환

 

포맷 변환

비디오 메타데이터에 포맷이 있다면 다음 같은 값을 쓸 수 있습니다.

  • auto
  • mp4
  • webm

 

이렇게 하면 전달 레이어가 상황에 맞는 포맷으로 파일을 반환할 수 있습니다.

 

워터마크 변환

워터마크 설정이 있다면, 다음 정보를 이용해 이미지 오버레이 변환을 추가합니다.

  • 워터마크 이미지 경로
  • 설정된 위치
  • 너비 값(없으면 기본값 120)

 

위치는 메타데이터에서 가져오고, 없다면 "bottom_right"를 기본값으로 두면 무난합니다.

워터마크 메타데이터에는 opacity도 포함되어 있으니, 나중에 오버레이를 좀 더 자연스럽게 보이게 하고 싶다면 이 값도 실제 변환 파라미터에 연결할 수 있습니다.

 

포스터 이미지 생성

포스터 이미지도 라이브러리와 같은 fallback 로직을 따르는 것이 좋습니다.

  1. video.thumbnailPath가 있으면 사용
  2. 없으면 다음 경로 사용
  3. ${video.filePath}/ik-thumbnail.jpeg

 

그 다음 미디어 URL 빌더를 이용해 최종 포스터 URL을 만들고, 예를 들어 다음 정도의 큰 변환을 적용할 수 있습니다.

  • width: 1920
  • height: 1080

 

이렇게 해야 플레이어가 검은 화면으로 열리는 대신, 큰 미리보기 이미지와 함께 훨씬 완성도 있게 시작됩니다.

 

비디오 컴포넌트 렌더링

최종 렌더링에는 다음 요소가 들어가야 합니다.

  • media URL endpoint
  • 비디오 파일 경로
  • 변환 리스트
  • 포스터 이미지
  • controls
  • playsInline
  • preload="metadata"
  • 전체 너비 스타일
  • 16 / 9 비율

 

여기서 특히 유용한 트릭이 하나 있습니다.

  • key={quality}

 

이렇게 두면 화질 값이 바뀔 때 비디오 컴포넌트가 다시 마운트되기 때문에, 새로운 변환 URL이 실제로 반영되도록 도와줍니다.

 

기본 제공 재생 UI를 쓰는 것이 여전히 좋은 이유

모든 제어 UI를 직접 다시 만들고 싶은 유혹이 들 수 있습니다. 때로는 그게 맞습니다. 하지만 대부분의 경우 꼭 그래야 하는 건 아닙니다.

기본 controls를 켜 두기만 해도 사용자는 이미 익숙한 동작을 사용할 수 있습니다.

  • 재생/일시정지
  • 탐색 바 이동
  • 볼륨 조절
  • 전체 화면
  • 재생 속도 조절

 

브라우저에 따라서는 여기에 더해:

  • PIP
  • 다운로드

 

같은 기능도 기본으로 얻을 수 있습니다.

기반 구조를 만드는 단계에서는 이 선택이 아주 적절합니다. 커스텀 제어 로직에 프로젝트를 묻어버리지 않으면서도, 충분히 괜찮은 플레이어를 빠르게 확보할 수 있으니까요.

 

화질 입력 UI 추가하기

플레이어 아래에 단순한 숫자 입력 하나 두면 충분합니다.

아래 요소를 연결하세요.

  • 라벨
  • min
  • max
  • value
  • onChange

 

화질 값이 곧바로 변환 URL에 영향을 주기 때문에, 숫자를 바꾸는 순간 비디오가 어떻게 전달될지가 달라집니다.

그래서 이 화질 제어는 단순한 장식이 아니라, 실제 동작을 바꾸는 조작으로 느껴집니다.

 

Hydration 경고 처리

숫자 입력 필드는 server/client 혼합 렌더링 환경에서 가끔 hydration 경고를 일으킬 수 있습니다. 그런 경우에는 input에 suppressHydrationWarning을 추가하는 것이 현실적인 해결책입니다.

화려한 해결책은 아니지만, 분명히 유용합니다.

 

 


 

Step 14: 시청 페이지 만들기

시청 페이지는 앞에서 만든 모든 것을 하나로 묶어 줍니다.

다음 위치에 페이지를 만듭니다.

/app/watch/[id]/page.tsx

 

이 페이지는 다음을 해야 합니다.

  1. 동적 id 읽기
  2. 저장소에서 비디오 메타데이터 조회
  3. 비디오가 없으면 404 반환
  4. 비디오가 있으면 플레이어와 메타데이터 렌더링

 

동적 렌더링 사용하기

이 버전은 로컬 JSON 파일을 메타데이터 저장소로 쓰고 있기 때문에, 동적 렌더링을 강제하는 선택이 꽤 합리적입니다.

그래야 시청 페이지가 정적인 가정에 기대지 않고, 방금 저장된 메타데이터도 제대로 반영할 수 있습니다.

 

시청 페이지에 보여줄 것

최소한이지만 충분히 강한 시청 페이지는 다음 요소를 포함합니다.

  • “Back to Library” 링크
  • 비디오 플레이어
  • 비디오 제목
  • 비디오 설명

 

이 정도만 있어도 시청 페이지는 단순한 임베드 상자가 아니라, 하나의 목적지처럼 느껴집니다.

 

비디오가 없을 때 처리

ID에 해당하는 레코드가 없다면 notFound()를 호출하는 것이 맞습니다.

모든 에러를 숨길 필요는 없습니다. 오히려 분명한 404가 가장 신뢰할 수 있는 UI 응답인 경우도 많습니다.

 

 


 

Step 15: 워터마킹이 실제로 동작하는 방식

이 시스템에서 가장 흥미로운 부분 중 하나입니다.

워터마크는 단순히 플레이어 위에 HTML 이미지를 얹는 방식이 아닙니다. 변환된 미디어 전달 URL 자체의 일부로 들어갑니다.

이 차이는 꽤 중요합니다.

비디오 URL 안에 다음 변환 파라미터가 들어가면:

  • 화질
  • 포맷
  • 워터마크 오버레이

 

플레이어가 받는 비디오는 이미 변환된 결과물에 가깝습니다. 즉, 워터마크가 해당 요청에 대해 반환되는 영상에 녹아든 형태가 됩니다.

 

왜 이 방식이 강력한가

이 말은 곧 다음을 의미합니다.

  1. 워터마크가 적용된 결과가 비디오 전달 결과 자체의 일부가 된다.
  2. 같은 변환 URL을 다른 앱이나 페이지, 환경에서도 그대로 쓸 수 있다.
  3. 플레이어 UI만이 이 변환의 소비자가 아니라, 변환된 자산 자체를 재사용할 수 있다.

 

즉, 로직이 휴대 가능합니다.

 

실제로 보게 되는 현상

워터마크를 우하단에 두면, 플레이어 컨트롤이 떠 있는 동안에는 그 뒤에 가려져 잘 안 보일 때가 있습니다.

그래서 가끔은 “워터마크가 적용 안 된 것 같은데?”라는 착각을 하게 됩니다. 하지만 적용이 안 된 게 아닙니다.

컨트롤이 사라지면 워터마크가 분명히 보이기 시작합니다.

이건 좋은 교훈이기도 합니다. 모든 “UI가 안 보인다” 문제는 데이터 문제는 아닙니다. 때로는 단순히 레이어가 겹친 인터페이스 문제일 뿐입니다.

 

 


 

Step 16: 성능, 캐시, 그리고 왜 어떤 요청은 나중에 더 빨라 보이는가

변환된 자산을 처음 요청할 때—특히 자동 생성 썸네일이나 새롭게 변환된 비디오 변형본일 때—플랫폼이 처리 시간을 조금 필요로 할 수 있습니다.

그래서 첫 요청은 느리게 느껴질 수 있습니다.

하지만 그 이후에는 캐시가 경험을 크게 바꿉니다.

예를 들어, 한 번 요청했던 화질의 워터마크 적용 비디오를 다시 열면 처음보다 훨씬 빠르게 뜰 수 있습니다.

그게 정상입니다.

 

왜 이 감각이 중요한가

개발자는 기능을 한 번 테스트하고 지연을 보면 구조 자체가 잘못됐다고 생각하기 쉽습니다. 하지만 미디어 변환에서는 첫 지연과 반복 지연이 같은 사건이 아닙니다.

항상 같은 일을 측정하고 있는 게 아니기 때문입니다.

  • 첫 요청 → 생성 + 캐시
  • 반복 요청 → 캐시된 결과 전달

 

썸네일이나 변환 재생을 디버깅할 때는 이 차이를 이해하는 것이 특히 중요합니다.

 


 

Step 17: 수동 화질 제어 vs 적응형 비트레이트 스트리밍

이 글에서 만든 화질 입력 UI는 유용합니다. 전달 방식의 트레이드오프를 직접 눈으로 확인할 수 있게 해주기 때문이죠. 하지만 비디오 경험이 성숙해질수록, 이런 판단을 시스템이 자동으로 해주길 원하게 됩니다.

그때 등장하는 것이 적응형 비트레이트 스트리밍(ABR) 입니다.

 

ABR이 하는 일

ABR은 사용자의 가용 대역폭에 따라 비디오 화질을 자동으로 바꿉니다.

즉, 사용자가 10, 50, 100을 직접 입력하지 않아도 시스템이 알아서:

  • 빠른 시작을 위해 낮은 화질로 시작하고
  • 대역폭이 허용되면 화질을 올리고
  • 네트워크가 흔들릴 때도 재생을 더 부드럽게 유지합니다

 

특히 모바일 환경이나 불안정한 네트워크에서 효과가 큽니다.

 

왜 ABR이 매력적인가

대부분의 사용자는 화질을 직접 관리하고 싶어 하지 않습니다. 그냥 영상이 잘 나오길 원합니다.

그리고 많은 경우 최고의 경험은 “최고 화질”이 아니라, “충분히 빨리 로드되어 기다림이 느껴지지 않는 최적 화질”입니다.

 

대신 감수해야 할 점

중요한 제약도 있습니다. 적응형 비트레이트 스트리밍은 모든 변환과 항상 잘 호환되는 것은 아니고, 가장 단순한 플레이어 구성과도 완전히 맞아떨어지지 않을 수 있습니다.

그래서 보통은 제품별로 우선순위를 정해야 합니다.

  • 오버레이, 포맷 조정, URL 기반 커스터마이징 같은 풍부한 변환
  • 혹은 가장 빠르고 유연한 스트리밍 전달 방식

 

세밀한 변환 제어가 더 중요하다면, 단일 변환 URL 접근이 직관적이고 강력합니다.

반대로 대규모 스트리밍 효율이 더 중요하다면, HLS 스타일의 적응형 전달 구조가 더 매력적입니다. 실전에서는 master asset 변형본과 적절한 변환 경로를 이용해 스트리밍 URL을 만드는 식으로 이어지는 경우가 많습니다.

결국 정답은 제품 성격에 달려 있습니다.

 

 


 

Step 18: 테스트하면서 꼭 기억해둘 만한 현실적인 포인트

정말 유용한 구현상의 교훈은 아키텍처 다이어그램보다 테스트 과정에서 더 많이 나옵니다.

기억해둘 만한 것들을 정리해 보면 이렇습니다.

 

1. 시청 페이지가 완성되기 전에도 업로드는 성공할 수 있다

파일 업로드도 성공하고 메타데이터 저장도 성공했는데, 리다이렉트 시점에 에러가 날 수 있습니다. 시청 페이지가 아직 준비되지 않았기 때문입니다.

모순이 아닙니다. 단지 백엔드 흐름이 UI 흐름보다 앞서 있었던 것뿐입니다.

 

2. 무료 플랜의 파일 크기 제한은 생각보다 현실적이다

테스트 업로드가 실패하는 이유가 단순히 파일이 너무 커서일 수 있습니다. 이 구현에서는 체감상 20MB~40MB 정도가 무료 플랜의 실질적인 상한처럼 느껴졌습니다.

이건 개발자 불편을 넘어 제품 설계 포인트이기도 합니다.

 

3. 썸네일이 안 뜨면 의외로 설정 문제인 경우가 많다

썸네일이 보이지 않으면 먼저 확인할 것은:

  • 환경 변수 이름
  • 개발 서버 재시작 여부

 

정말 놀랄 만큼 자주 정답이 여기 있습니다.

 

4. 자동 생성 썸네일은 한 번만 느리고, 그 뒤로는 빠를 수 있다

버그가 아닙니다. 변환 생성과 캐시의 결과입니다.

 

5. 낮은 화질은 체감적으로 훨씬 빨라질 수 있다

화질을 10 정도로 낮추면 영상은 눈에 띄게 거칠어지지만, 그만큼 빨리 뜨게 만들 수 있습니다. 전달 최적화의 효과를 가장 직관적으로 확인하는 방법이기도 합니다.

이런 디테일을 이해하고 있으면, “기술적으로 동작한다” 수준과 “이 시스템이 어떻게 움직이는지 이해하고 있다” 수준 사이의 차이가 크게 벌어집니다.

 

 


 

Step 19: 이 아키텍처가 아직 해결하지 않은 것들

이 기반은 분명히 탄탄합니다. 하지만 어디까지나 기반일 뿐입니다.

아직 완전히 해결하지 않은 부분도 많습니다.

 

실제 프로덕션용 데이터베이스는 아니다

로컬 JSON 파일은 초기 구조 검증에는 훌륭합니다. 하지만 동시성, 내구성, 여러 인스턴스 환경에서의 안정성을 책임질 장기 해법은 아닙니다.

 

사용자 인증이 없다

업로드 인증 라우트가 업로드 credential을 발급하긴 하지만, 실제 사용자 신원이나 권한 규칙을 강제하지는 않습니다.

 

업로드 정책 검증이 없다

나중에는 아래 같은 검사를 넣을 수 있습니다.

  • 파일 크기
  • 파일 형식
  • 업로드 횟수 제한
  • 사용자별 quota

 

고급 인코딩 파이프라인이 없다

현재 구성은 풀 커스텀 transcoding 워크플로 대신, 전달 시점의 변환에 기대고 있습니다.

 

분석이나 재생 추적이 없다

아직 아래 데이터를 수집하고 있지 않습니다.

  • 조회수
  • 완료율
  • 시청 시간
  • 이탈 지점

 

관리자용 전체 워크플로가 없다

모더레이션 패널도 없고, 자산 교체 흐름도 없고, 메타데이터를 수정하는 UI도 아직 없습니다.

그래도 괜찮습니다. 기반 구조의 목적은 모든 것을 한 번에 해결하는 데 있지 않습니다. 다음 단계들을 더 쉽게 만드는 데 있습니다.

 

 


 

Step 20: 다음에는 어디까지 확장하면 좋을까

핵심 흐름이 작동하기 시작하면, 다음 수순은 꽤 명확합니다.

 

인증 추가하기

업로드 인증 credential을 발급하기 전에 로그인 상태를 요구합니다.

 

JSON 대신 실제 데이터베이스로 바꾸기

메타데이터를 PostgreSQL, MySQL, MongoDB 같은 내구성 있는 저장소로 옮깁니다.

 

업로드 전에 파일 검증하기

클라이언트와 서버 양쪽에서 파일 크기와 형식을 검사합니다.

 

자막 추가하기

자막은 접근성, 이해도, 검색 가능성을 모두 개선합니다.

 

여러 전달 포맷 지원하기

mp4, webm, 혹은 auto 중 무엇이 가장 적절한지 시스템이 판단하게 할 수 있습니다.

 

사용량 분석 붙이기

사용자가 실제로 무엇을 보고 있는지 추적하면, UX와 인프라를 함께 최적화할 수 있습니다.

 

관리자용 라이브러리 구축하기

크리에이터가 업로드 후에도 제목, 썸네일, 설명, 워터마크를 수정할 수 있도록 합니다.

 

비동기 워크플로 추가하기

오래 걸리는 작업을 큐에 넣고, 처리 상태를 추적하고, 자산 준비 상태를 사용자에게 보여줍니다.

바로 이런 지점에서, 지금의 아키텍처가 데모를 넘어 실제 제품으로 자라기 시작합니다.

 

 


 

마무리

Next.js 비디오 플레이어 앱을 만든다는 것은 화려한 플레이어 UI를 발명하는 일이 아닙니다. 그보다 훨씬 본질적인 문제입니다. 시스템 아래에서는 많은 일이 일어나더라도, 사용자 입장에서는 최대한 단순하게 느껴지는 미디어 워크플로를 설계하는 일에 가깝습니다.

이 접근이 잘 작동하는 이유는 분리가 명확하기 때문입니다.

  • 미디어 자산은 전달과 변환에 강한 곳에 둔다
  • 메타데이터는 애플리케이션 로직이 활용할 수 있는 곳에 둔다
  • 업로드는 안전하게 인증한다
  • 썸네일은 자연스러운 fallback을 가진다
  • 재생 구조는 워터마킹, 화질 제어, 이후 확장을 모두 감당할 수 있도록 유연하게 만든다

 

크리에이터 도구, 교육 플랫폼, 강의 사이트, 제품 데모 라이브러리, 사내 미디어 포털 같은 것을 만든다면, 이 정도 기반은 속도를 잃지 않으면서도 나중에 막다른 길로 들어가지 않게 해주는 아주 좋은 출발점이 됩니다.

 

좋은 기술 기반은 가장 복잡한 구조가 아닙니다. 이해하기 쉽고, 그 상태로도 충분히 확장 여지를 남겨두는 구조입니다.

 

 


FAQ: 다음 단계에서 개발자들이 가장 자주 묻는 질문

1. 처음에는 로컬 JSON 파일로 비디오 메타데이터를 저장해도 정말 괜찮을까요?

네. Prototype, 내부 도구, 초기 PoC 단계라면 충분히 괜찮은 선택입니다. 핵심 사용자 흐름을 빠르게 검증하는 데 아주 유용합니다. 다만 “버전 1에서 충분한 것”과 “영원히 충분한 것”을 혼동하면 안 됩니다.

 

2. 왜 프론트엔드에서 미디어 서비스 private key로 바로 업로드하면 안 되나요?

그렇게 하면 private credential이 브라우저에 노출됩니다. 누구나 들여다보고 악용할 수 있게 되죠. 서버가 발급하는 업로드 토큰을 쓰면 업로드 흐름을 통제된 일회성 권한 안으로 넣을 수 있습니다.

 

3. 사용자가 썸네일을 업로드하지 않으면 어떻게 되나요?

앱이 비디오 첫 프레임을 바탕으로 자동 썸네일을 생성할 수 있습니다. 덕분에 업로드 단계를 강제로 늘리지 않으면서도 라이브러리 화면을 깔끔하게 유지할 수 있습니다.

 

4. 자동 생성 썸네일이나 변환된 비디오가 처음에 느리게 뜨는 이유는 무엇인가요?

첫 요청 시점에는 시스템이 해당 자산을 생성하고 캐시에 올려야 할 수 있기 때문입니다. 이후에는 이미 준비된 변환 결과를 재사용하므로 훨씬 빨라지는 경우가 많습니다.

 

5. 수동 화질 제어만으로도 프로덕션에서 충분할까요?

제품에 따라 다릅니다. 수동 화질 제어는 테스트, 내부 도구, 통제된 워크플로에서는 충분히 유용합니다. 하지만 소비자 대상 대규모 스트리밍 서비스라면 적응형 비트레이트 전달이 장기적으로 더 나은 경험을 주는 경우가 많습니다.

 

6. 워터마크는 플레이어 밖에서도 재사용할 수 있나요?

네. 워터마크가 변환된 미디어 URL 자체를 통해 적용되기 때문에, 같은 변환 결과를 다른 앱, 임베드, 전달 환경에서도 그대로 재사용할 수 있습니다. 시청 페이지에만 묶인 기능이 아닙니다.

 

7. 코드상으로는 맞아 보이는데 썸네일이 왜 안 뜰 수 있나요?

가장 흔한 원인은 public URL endpoint 환경 변수 이름이 틀렸거나, 환경 변수를 수정한 뒤 개발 서버를 재시작하지 않은 경우입니다. 미디어 로직이 깨졌다고 단정하기 전에 설정부터 확인하는 것이 좋습니다.

 

8. 이 기반이 작동한 뒤에는 무엇을 가장 먼저 붙이는 게 좋을까요?

가장 가치가 큰 다음 단계는 인증, 실제 데이터베이스, 업로드 검증, 자막 지원, 분석 기능, 그리고 메타데이터와 자산을 관리할 수 있는 관리자 UI입니다.

반응형