프로젝트/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

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로 대체하니 훨씬 자연스러워졌네요!

이렇게 마무리되는 줄 알았는데 문제가 생겼어요. 느린 4G, 3G 등 네트워크 throttling 환경에서 iframe에 등록한 onLoad 이벤트 핸들러가 동작하지 않는 경우가 종종 발생해서 무슨 문제인지 검색해 봐도 해결 방법이 나오지 않네요😭

onLoad 이슈 구글링

깃헙의 이슈에 따르면, iframe의 로드 이벤트는 React가 리스너를 추가하기 전에 발생하는 경우가 있다고 하네요. 이로 인해 iframe의 onLoad 이벤트가 호출되지 않는 문제가 발생하는 것 같습니다. 이러한 현상은 React의 hydration 과정과 관련이 있는 것으로 보입니다.

 

Hydration은 서버에서 전달받은 정적인 HTML에 이벤트 리스너를 연결하여 동적으로 만드는 과정입니다. 이 과정을 살펴보면 서버에서 렌더링 된 html에 onLoad 이벤트를 추가하여 클라이언트로 전달합니다. 이때 onLoad 이벤트가 이미 실행된 상태일 수 있습니다.

 

만약 느린 네트워크 환경에서 비동기로 로드되는 유튜브 콘텐츠가 지연된다면, onLoad 이벤트가 종료된 후 iframe이 마운트 되어 onLoad 이벤트가 호출되지 않는 것처럼 보이게 되는 것입니다.

 

문제 원인이 hydration 과정에 있는지 확인하기 위해, iframe 요소를 동적으로 추가하는 코드를 통해 검증해 보겠습니다.

 

iframe 동적으로 추가

'use client';

import { useEffect, useState, useRef } from 'react';

function Iframe() {
  const [isLoading, setIsLoading] = useState(true);
  const iframeRef = useRef(null);

  useEffect(() => {
    const iframe = document.createElement('iframe');
    iframe.src = 'https://www.youtube.com/embed/he0KEFFhvvA';
    iframe.width = '500';
    iframe.height = '300';
    iframe.onload = () => {
      console.log('iFrame Loaded');
      setIsLoading(false);
    };

    if (iframeRef.current) {
      iframeRef.current.appendChild(iframe);
    }

    return () => {
      if (iframeRef.current) {
        iframeRef.current.removeChild(iframe);
      }
    };
  }, []);

  return (
    <>
      {isLoading && (
        <div className="h-[300px] w-[500px] bg-white text-black">
          Loading...
        </div>
      )}
      <div ref={iframeRef} />
    </>
  );
}

export default Iframe;

iframe 동적 추가 - 느린 네트워크 환경 onLoad 호출 결과

Network Throttling을 통해 느린 네트워크 환경에서 테스트해 본 결과, onLoad 이벤트가 정상적으로 동작하는 것을 확인했습니다. 이는 hydration 과정이 끝난 후 클라이언트에서 iframe을 추가했기 때문입니다.

 

따라서 문제의 원인은 iframe의 콘텐츠가 비동기로 로드되기 때문에, 느린 네트워크 환경에서는 마운트 되는 시점이 미뤄져 onLoad 이벤트가 이미 실행이 끝난 후에 iframe이 마운트 되므로 onLoad가 호출되지 않는 것입니다.

 

위 방법을 채택하려고 했지만 몇 가지 문제가 있습니다.

  • 페이지 로드 시점에 iframe을 동적으로 생성하는 추가적인 JavaScript 실행 필요 (초기 로드 성능 저하)
  • iframe을 동적으로 생성하기 때문에 검색 엔진이 콘텐츠를 인식하지 못함 (접근성, SEO 문제)

iframe을 동적으로 추가한 Lighthouse 결과
동적 iframe 성능 진단 결과
React-Player를 사용한 Lighthouse 결과
React-Player 성능 진단 결과

React Player 라이브러리를 사용한 결과 다음과 같은 성과를 얻었습니다.

  • 성능(performance) 점수 : 76에서 85로 9점 향상
  • TBT(Total Blocking Time) : 600ms에서 340ms로 개선
  • 접근성 점수 : 95에서 100으로 5점 향상

라이브러리 의존성을 줄이는 것도 좋지만, 효율적인 개발과 안정적인 서비스 제공을 위해 React-Player 라이브러리를 사용해 이슈를 해결하기로 판단했습니다.

⭐ React-Player 라이브러리 사용

react-player의 github 문서를 확인해 봤더니 onReady Prop을  제공하고 있네요!

react-player 문서

 

설명에 media가 로드되고 재생할 준비가 되면 호출된다고 하니 iframe 대신 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 에러 해결

 

📖 느낀 점

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

 

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

 

예전에는 "일단 동작하는 코드"를 만드는 게 목표였다면, 이제는 "확장 가능하고 최적화된 코드"를 고민하는 저를 보면서 개발자로서 한층 성장하고 있다는 느낌이 들어요.  앞으로도 작은 문제 하나하나 깊이 탐구하면서, 더 탄탄한 개발자로 성장해 나가고 싶어요.  해결해야 할 것들은 여전히 많지만, 그 과정 자체가 점점 더 재미있게 느껴지네요!