오늘은 상세 페이지를 작업할 건데요.
처음 기획 당시 페이지 상단에 맛집 소개 Youtube 영상을 띄우기로 했었네요.. 어떤 방법이 있는지 찾아볼게요👀
✨ 영상 추가 방법
1️⃣ 기본 <iframe /> 태그 이용
유튜브 영상에 자체적으로 퍼가기 기능을 제공하고 있네요😀
iframe의 src를 보니 프로젝트에서 사용하는 데이터의 videoId를 embed/ 뒤에 추가하면 될 것 같아요.
2️⃣ react-youtube 라이브러리 및 Youtube API 사용
사용법을 보니 videoId 부분에 Youtube API에서 요청을 통해 받아온 videoId를 넣어주면 되는 것 같아요.
원래라면 Youtube API를 연동해서 아래와 같은 과정을 거치겠죠🤔
- 서버에서 Youtube로 API key를 포함한 데이터 요청
- 유튜브 서버에서 응답
- 서버에서 클라이언트로 응답 전달
저는 Yotube API를 통해 받은 데이터를 JSON 형태로 사용하고 있기 때문에 위 과정을 생략할 수 있어 로딩도 빠르고 API 제한에 걸릴 문제도 해결할 수 있겠네요😀
3️⃣ react-player 라이브러리 사용
react-player 라이브러리는 다양한 플레이어를 지원 라이브러리이며 제가 원하는 Youtube 영상 또한 지원하네요.
npm trends의 지난 1년간 다운로드 지표만 봐도 react-player가 react-youtube에 비해 압도적이네요.
뿐만 아니라 Next.js의 공식 문서에서도 react-player를 추천하는 third-party 플레이어 중 하나로 선정했어요.
🌈 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 코드는 유튜브 영상에서 공유 → 퍼가기를 통해 제공하고 있어서 따로 작성할 필요 없이 가져다 사용했어요😊
결과를 확인해 봐야겠죠
👀
👀
영상은 잘 나오는데 한 가지 불편한 점이 있네요...😅
위 사진처럼 영상이 로드되기 전까지 빈 화면이 노출되다가 영상이 나타나서 화면이 깜빡이고 있어요.
어떻게 처리해야 할지 고민할 필요 없겠네요❗
이전 프로젝트들에서 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을 보여주도록 작업했어요.
결과를 확인해 볼게요
👀
👀
영상이 로딩 상태인 경우 영상의 Thumbnail로 대체하니 훨씬 자연스러워졌네요👏👏
이렇게 마무리되는 줄 알았는데 문제가 생겼어요.
iframe에 등록한 onLoad 이벤트 핸들러가 동작하지 않는 경우가 종종 발생해서 무슨 문제인지 검색해 봐도 해결 방법이 나오지 않네요😭
검색어를 바꿔봐도 달라지는게 없어 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;
유튜브 영상은 잘 나오는데 한 가지 문제가 생겼어요.
👀
👀
🚨 문제 상황 - Hydration Mismatch 에러
이 에러는 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;
에러를 해결하기 위해 React Player 라이브러리를 선택하게 되었는데요.
어쩌다 보니 성능도 iframe을 사용해서 작업했을 때 보다 조금이나마 개선되었네요🎉🎉
📖 느낀 점
YouTube 영상 추가 작업을 하면서 단순히 기능을 구현하는 걸 넘어, "왜 이렇게 동작하는 걸까?" 하는 고민을 깊이 하게 되었어요.
에러를 해결하는 과정에서 SSR, Hydration, Lazy Loading 같은 프론트엔드 성능 최적화 개념들을 자연스럽게 익히게 됐는데요. 단순히 개념만 찾아보는 게 아니라, 실제 코드에서 적용하고 문제를 해결하는 과정이 앞으로 더 나은 방향을 고민하는 데 큰 도움이 될 것 같아요.
예전에는 "일단 동작하는 코드" 를 만드는 게 목표였다면, 이제는 "확장 가능하고 최적화된 코드" 를 고민하는 저를 보면서 개발자로서 한층 성장하고 있다는 느낌이 들어요.
앞으로도 작은 문제 하나하나 깊이 탐구하면서, 더 탄탄한 개발자로 성장해 나가고 싶어요.
해결해야 할 것들은 여전히 많지만, 그 과정 자체가 점점 더 재미있게 느껴지네요! 🚀
'프로젝트 > Next+TypeScript' 카테고리의 다른 글
[Coworkers] IOS 이미지 업로드 이슈 (0) | 2025.02.12 |
---|---|
[Coworkers] 리팩토링 (0) | 2025.02.07 |
[맛길] 동적 라우팅(Dynamic Routing) 적용 (1) | 2025.02.05 |
[Coworkers] 할 일 리스트 페이지 작업 (협업) (1) | 2025.02.04 |
[Coworkers] Axios interceptor 적용 (token 적용, refresh token을 이용한 토큰 재발급) (0) | 2025.02.03 |