프로젝트/Next+TypeScript

[맛길] 상세 페이지 - Youtube 영상 추가

dev-hpk 2025. 2. 6. 22:34

오늘은 상세 페이지를 작업할 건데요.

처음 기획 당시 페이지 상단에 맛집 소개 Youtube 영상을 띄우기로 했었네요.. 어떤 방법이 있는지 찾아볼게요👀

✨ 영상 추가 방법

1️⃣ 기본 <iframe /> 태그 이용 

youtube 동영상 퍼가기 iframe 제공

 

유튜브 영상에 자체적으로 퍼가기 기능을 제공하고 있네요😀

iframe의 src를 보니 프로젝트에서 사용하는 데이터의 videoId를 embed/ 뒤에 추가하면 될 것 같아요.

 

2️⃣ react-youtube 라이브러리 및 Youtube API 사용

npm - react-youtue 사용법

 

사용법을 보니 videoId 부분에 Youtube API에서 요청을 통해 받아온 videoId를 넣어주면 되는 것 같아요.

원래라면 Youtube API를 연동해서 아래와 같은 과정을 거치겠죠🤔 

  1. 서버에서 Youtube로 API key를 포함한 데이터 요청
  2. 유튜브 서버에서 응답
  3. 서버에서 클라이언트로 응답 전달

저는 Yotube API를 통해 받은 데이터를 JSON 형태로 사용하고 있기 때문에 위 과정을 생략할 수 있어 로딩도 빠르고 API 제한에 걸릴 문제도 해결할 수 있겠네요😀

 

3️⃣ react-player 라이브러리 사용

ReactPlayer 소개

 

react-player 라이브러리는 다양한 플레이어를 지원 라이브러리이며 제가 원하는 Youtube 영상 또한 지원하네요.

npm trends 다운로드 지표

 

npm trends의 지난 1년간 다운로드 지표만 봐도 react-player가 react-youtube에 비해 압도적이네요.

뿐만 아니라 Next.js의 공식 문서에서도 react-player를 추천하는 third-party 플레이어 중 하나로 선정했어요.  

 

Next.js 공식 문서 - Video

 

🌈 Youtube 영상 추가

저는 라이브러리 의존성을 줄이고 빠른 작업을 위해 기본 <iframe /> 태그를 이용해 작업을 진행하기로 결정했어요.

우선 유튜브 영상을 보여주는 iframe을 컴포넌트로 분리해서 작업해 볼게요.

 

VideoPlayer.tsx - 유튜브 영상 플레이어

function VideoPlayer({ videoId }: { videoId: string; }) {
  return (
    <iframe
      width="100%"
      height="100%"
      src={`https://www.youtube.com/embed/${videoId}`}
      title="YouTube video player"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
      allowFullScreen
    />
  );
}

export default VideoPlayer;

 

VideoPlayer 컴포넌트는 부모 컴포넌트로부터 videoId를 props로 받아서 src에 추가하는 형식으로 작업했어요.

iframe embed 코드 확인 경로

 

iframe 코드는 유튜브 영상에서 공유 → 퍼가기를 통해 제공하고 있어서 따로 작성할 필요 없이 가져다 사용했어요😊

결과를 확인해 봐야겠죠

👀

👀

유튜브 영상 적용 화면

 

영상은 잘 나오는데 한 가지  불편한 점이 있네요...😅 

영상 로딩 중 빈 화면

 

위 사진처럼 영상이 로드되기 전까지 빈 화면이 노출되다가 영상이 나타나서 화면이 깜빡이고 있어요.

 

어떻게 처리해야 할지 고민할 필요 없겠네요❗

이전 프로젝트들에서 API 요청을 할 때 이미 수 차례 isLoading 상태를 관리해 봤잖아요😊

 

Loading 상태 추가 - thumbnail 데이터 이용

'use client';

import { useState } from 'react';
import Image from 'next/image';

function VideoPlayer({ videoId, lazy }: { videoId: string; lazy: string }) {
  const [isLoaded, setIsLoaded] = useState(false);

  const handleLoad = () => {
    setIsLoaded(true);
  };
  
  return (
    <>
      {!isLoaded && (
        <Image fill src={lazy} className="object-cover" alt="Video Thumbnail" />
      )}
      <iframe
        width="100%"
        height="100%"
        src={`https://www.youtube.com/embed/${videoId}`}
        title="YouTube video player"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        allowFullScreen
        onLoad={handleLoad}
        loading="lazy"
      />
    </>
  );
}

export default VideoPlayer;

 

iframe의 onLoad 이벤트 핸들러를 추가해 로드가 되기 전까지 thumbnail을 보여주도록 작업했어요.

결과를 확인해 볼게요

👀

👀

유튜브 영상 loading 처리

 

영상이 로딩 상태인 경우 영상의 Thumbnail로 대체하니 훨씬 자연스러워졌네요👏👏

 

이렇게 마무리되는 줄 알았는데 문제가 생겼어요.

iframe에 등록한 onLoad 이벤트 핸들러가 동작하지 않는 경우가 종종 발생해서 무슨 문제인지 검색해 봐도 해결 방법이 나오지 않네요😭

onLoad 이슈 구글링

 

검색어를 바꿔봐도 달라지는게 없어 GPT에게 질문했더니 아래와 같은 해결책을 줬어요.

GPT가 도출한 해결책

 

흠... 제가 원하는 해결책이 아니네요😭

 

혹시나 하는 마음으로 react-player의 github 문서를 확인해 봤더니 onReady Prop을  제공하고 있네요❗❗

 

설명에 media가 로드되고 재생할 준비가 되면 호출된다고 하니 iframe 대신 react-player를 사용해 볼게요🤣

⭐ React-Player 라이브러리 사용

'use client';

import { useEffect, useState } from 'react';
import Image from 'next/image';
import ReactPlayer from 'react-player/lazy';

function VideoPlayer({ videoId, lazy }: { videoId: string; lazy: string }) {
  const [isLoaded, setIsLoaded] = useState(false);

  return (
    <>
      {!isLoaded && (
        <Image fill src={lazy} className="object-cover" alt="Video Thumbnail" />
      )}
      <ReactPlayer
        url={`https://www.youtube.com/watch?v=${videoId}`}
        controls
        width="100%"
        height="100%"
        onReady={() => setIsLoaded(true)}
      />
    </>
  );
}

export default VideoPlayer;

react-player 결과

 

유튜브 영상은 잘 나오는데 한 가지 문제가 생겼어요.

👀

👀

 

🚨 문제 상황 - Hydration Mismatch 에러

react-player hydration 에러

 

이 에러는 Next.js에서 서버 사이드 렌더링(SSR)된 HTML과 클라이언트에서 실행된 React 컴포넌트의 결과가 다를 때 발생해요❗

 

에러를 해결하려면 Hydration에 대한 이해가 필요해 보이네요.

 

Hydration은 수분 보충이라는 뜻을 가지고 있어요.

간단하게 설명하면 정적인 HTML(SSR)을 Hydration(수분보충)을 통해 동적(CSR)으로 만드는 과정이에요.

더 간단하게 설명하자면 서버에서 받은 HTML에 이벤트 리스너를 등록하는 작업이라고 할 수 있겠네요👀

 

다시 프로젝트로 돌아와서 에러를 살펴볼게요.

React Player 라이브러리는 렌더링 시에 일부 DOM 엘리먼트를 동적으로 생성해요.

따라서 서버에서 미리 렌더링 된 HTML과 클라이언트에서 실행된 결과가 다를 수 있는 거죠.

이런 차이로 인해 Next.js의 Hydration 과정에서 오류가 발생하게 되는 거예요❗❗❗

 

이를 방지하려면, React Player를 클라이언트에서만 렌더링하도록 설정해야겠죠😊

 

CSR 적용 - React Player 클라이언트에서만 렌더링

'use client';

import { useEffect, useState } from 'react';
import Image from 'next/image';
import ReactPlayer from 'react-player/lazy';

function VideoPlayer({ videoId, lazy }: { videoId: string; lazy: string }) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    if (typeof window !== 'undefined') {
      setIsClient(true);
    }
  }, []);

  return (
    <>
      {!isLoaded && (
        <Image fill src={lazy} className="object-cover" alt="Video Thumbnail" />
      )}
      {isClient && (
        <ReactPlayer
          url={`https://www.youtube.com/watch?v=${videoId}`}
          controls
          width="100%"
          height="100%"
          onReady={() => setIsLoaded(true)}
        />
      )}
    </>
  );
}

export default VideoPlayer;

 

hydration mismatch 에러 해결

 

 

 

에러를 해결하기 위해 React Player 라이브러리를 선택하게 되었는데요.

어쩌다 보니 성능도 iframe을 사용해서 작업했을 때 보다 조금이나마 개선되었네요🎉🎉

 

성능 측정 - iframe 사용

 

성능 측정 - react player 라이브러리 사용

 

 

📖 느낀 점

YouTube 영상 추가 작업을 하면서 단순히 기능을 구현하는 걸 넘어, "왜 이렇게 동작하는 걸까?" 하는 고민을 깊이 하게 되었어요.

 

에러를 해결하는 과정에서 SSR, Hydration, Lazy Loading 같은 프론트엔드 성능 최적화 개념들을 자연스럽게 익히게 됐는데요. 단순히 개념만 찾아보는 게 아니라, 실제 코드에서 적용하고 문제를 해결하는 과정이 앞으로 더 나은 방향을 고민하는 데 큰 도움이 될 것 같아요.

 

예전에는 "일단 동작하는 코드" 를 만드는 게 목표였다면, 이제는 "확장 가능하고 최적화된 코드" 를 고민하는 저를 보면서 개발자로서 한층 성장하고 있다는 느낌이 들어요.

앞으로도 작은 문제 하나하나 깊이 탐구하면서, 더 탄탄한 개발자로 성장해 나가고 싶어요.

해결해야 할 것들은 여전히 많지만, 그 과정 자체가 점점 더 재미있게 느껴지네요! 🚀