본문 바로가기
Tutorial/youtube

21. 나만의 유튜브 사이트 만들기 : 채널 상세 페이지 만들기

by @webstoryboy 2023. 10. 17.
Tutorial/Portfolio

나만의 유튜브 사이트 만들기

by @webs 2023. 09. 01.
21
나만의 유튜브 사이트 만들기 : 채널 상세 페이지 만들기
난이도 중간

소개

안녕하세요! 웹스토리보이입니다. 이 강의는 React 프레임워크와 YouTube API를 이용하여 자신만의 간단한 영상 사이트를 만들어보겠습니다. React의 기본 개념을 이해하고, 컴포넌트를 구조화하고 상태를 관리하는 방법을 학습하게 될 것입니다. 또한 YouTube Data API를 활용하여 외부 데이터를 가져오는 방법을 익히고, API 응답을 처리하여 사용자에게 의미 있는 정보를 제공하는 방법을 이해하게 됩니다. 이로써 자신만의 유튜브 사이트를 만들고, 활용해보는 것을 목표로 합니다. 그럼 한번 시작해볼까요? 🥳

인덱스

  • 1. 셋팅하기
    • 1_1. Node.js 설치
    • 1_2. Vscode 설치
    • 1_3. React.js 설치
  • 2. 라이브러리 설치하기
    • 2_1. 폴더 정리하기
    • 2_2. 라이브러리 설치하기
  • 3. Git 연동하기
    • 3_1. 저장소 만들기
    • 3_2. 모든 파일 올리기
    • 3_3. 깃 상태 확인하기
  • 4. SCSS 셋팅하기
    • 4_1. SCSS 설정하기
    • 4_2. style.scss 설정하기
    • 4_3. fonts.scss 설정하기
    • 4_4. vars.scss 설정하기
    • 4_5. reset.scss 설정하기
    • 4_6. mixin.scss 설정하기
    • 4_7. common.scss 설정하기
  • 5. 페이지 만들기
    • 5_1. 페이지 만들기
    • 5_2. 페이지 컴퍼넌트 만들기
  • 6. 섹션 컴퍼넌트 구조화하기
    • 6_1. 전체 레이아웃 만들기
    • 6_2. 섹션 컴퍼넌트 만들기
  • 7. 헤더 영역 완성하기
    • 7_1. 헤더 영역 구조 잡기
    • 7_2. 헤더 영역 디자인 작업
  • 8. 헤더 영역 데이터 작업
    • 8_1. 헤더 영역 데이터 작업
    • 8_2. 반복문과 map()
    • 8_3. 메뉴 활성화하기
    • 8_4. 컴퍼넌트 세부화 시키기
  • 9. 컴퍼넌트 비동기 작업
    • 8_1. 컴퍼넌트 props 사용하기
    • 8_2. React.Suspense 사용하기
  • 10. 페이지 SEO 작업
    • 10_1. 메인 페이지 SEO 설정하기
    • 10_2. 모든 페이지 SEO 설정하기
  • 11. 메인 콘텐츠 작업
    • 11_1. 검색 컴퍼넌트 작업하기
    • 11_2. 메인 컴퍼넌트 작업하기
  • 12. 추천 영상 작업
    • 12_1. 메인 추천 영상 작업
    • 12_2. 추천 영상 반응형 작업
    • 12_3. 추천 영상 페이지 작업
  • 13. 추천 개발자 작업
    • 13_1. 메인 추천 개발자 작업
    • 13_2. 추천 개발자 페이지 작업
  • 14. 메인 섹션 나머지 콘텐츠 작업
    • 14_1. 웹디자인 기능사 컴퍼넌트 작업
    • 14_2. 웹표준 사이트 컴퍼넌트 작업
    • 14_3. GSAP 사이트 컴퍼넌트 작업
    • 14_4. 포트폴리오 사이트 컴퍼넌트 작업
    • 14_5. 유튜브 사이트 컴퍼넌트 작업
  • 15. 비디오 컴퍼넌트 통합 작업
    • 15_1. 공통 요소 컴퍼넌트 만들기
    • 15_2. Swiper 슬라이드 만들기
    • 15_3. 로딩 효과 넣기
  • 16. Swiper 이미지 슬라이드 작업
    • 16_1. 추천 개발자 이미지 슬라이드 작업
    • 16_2. 추천 개발자 로딩 효과 넣기
    • 16_3. 추천 영상 로딩 및 데이터 작업
  • 17. 나머지 페이지 작업
    • 17_1. 웹디자인 기능사 페이지 작업
    • 17_2. 웹표준 사이트 페이지 작업
    • 17_3. GSAP 페이지 작업
    • 17_4. 포트폴리오 페이지 작업
    • 17_5. 유튜브 페이지 작업
    • 17_6. 추천 개발자 페이지 작업
    • 17_7. 추천 영상 페이지 작업
  • 18. 검색 및 유튜브 API 설정하기
    • 18_1. 검색 영역 설정하기
    • 18_2. 유튜브 API 설정하기
    • 18_3. 유튜브 API 데이터 가져오기
  • 19. Rapid API 및 더보기 설정하기
    • 19_1. Rapid API 설정하기
    • 19_2. Rapid API 데이터 가져오기
    • 19_3. 검색 데이터 더보기 기능 설정하기
    • 19_4. Loading 소스 설정하기
  • 20. 비디오 상세 페이지 만들기
    • 20_1. 비디오 데이터 불러오기
    • 20_2. 비디오 페이지 반응형 작업
  • 21. 채널 상세 페이지 만들기
    • 21_1. 채널 데이터 불러오기
    • 21_2. 채널 페이지 반응형 작업
    • 21_3. 로딩 기능 추가하기
    • 21_4. 채널 영상 추가하기
    • 21_5. 채널 영상 더보기 구현하기

21. 채널 상세 페이지 만들기

21_1. 채널 데이터 불러오기

이번에는 채널 페이지를 작업하겠습니다. 해당 작가의 채널 id 값을 클릭하면 channel 페이지로 channelId 값을 넘겨주게 되어 있습니다. 그 채널 아이디 값을 API를 통해 데이터를 불러오면 됩니다.

pages 폴더 channel.jsx 파일을 수정하겠습니다.

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { fetchFromAPI } from '../utils/api'

import Main from '../components/section/Main';

import { CiBadgeDollar } from "react-icons/ci";
import { CiMedal } from "react-icons/ci";
import { CiRead } from "react-icons/ci";

const Channel = () => {
    const { channelId } = useParams();
    const [ channelDetail, setChannelDetail ] = useState();

    useEffect(() => {
        const fetchResults = async () => {
            const data = await fetchFromAPI(`channels?part=snippet&id=${channelId}`);
            setChannelDetail(data.items[0]);
        };
        fetchResults();
    }, [channelId]);

    return (
        <Main 
            title = "유튜브 채널"
            description="유튜브 채널페이지입니다.">
            
            {channelDetail && (
                <section id='channel'>
                    <div className='channel__header' style={{ backgroundImage: `url(${channelDetail.brandingSettings.image.bannerExternalUrl})` }}>
                        <div className='circle'>
                            <img src={channelDetail.snippet.thumbnails.high.url} alt={channelDetail.snippet.title} />
                        </div>
                    </div>
                    <div className='channel__info'>
                        <h3 className='title'>{channelDetail.snippet.title}</h3>
                        <p className='desc'>{channelDetail.snippet.description}</p>
                        <div className='info'>
                            <span><CiBadgeDollar />{channelDetail.statistics.subscriberCount}</span>
                            <span><CiMedal />{channelDetail.statistics.videoCount}</span>
                            <span><CiRead />{channelDetail.statistics.viewCount}</span>
                        </div>
                    </div>
                </section>
            )}
        </Main>
    )
}

export default Channel

21_2. 채널 페이지 반응형 작업

scss > section > _channel.scss 파일을 만들고 소스를 추가하겠습니다.

#channel {
    padding: 0;
}
.channel__header {
    width: 100%;
    height: 300px;
    background-color: #969696;
    padding-top: 100px;
    background-position: center center;
    position: relative;

    &::after {
        content: '';
        width: 100%;
        height: 100%;
        background-color: #00000064;
        position: absolute;
        left: 0; 
        top: 0;
    }
    .circle {
        width: 200px;
        height: 200px;
        border-radius: 50%;
        overflow: hidden;
        position: absolute;
        left: 50%;
        top: 150px;
        transform: translateX(-50%);
        z-index: 100;
    }
}

.channel__info {
    margin-top: 100px;
    color: #fff;
    text-align: center;

    .title {
        margin-bottom: 10px;
    }
    .desc {
        color: #adadad;
        padding: 0 10vw;
        margin-bottom: 30px;
        overflow-wrap: anywhere;
    }
    .info span {
        background-color: #a6a6a631;
        padding: 10px 20px;
        border-radius: 40px;
        margin: 4px;

        svg {
            width: 20px;
            height: 20px;
            vertical-align: -5px;
            margin-right: 5px;
        }
    }
}

.channel__video {
    padding: 40px;
    margin-top: 50px;
}
.channel__more {
    padding: 40px;

    button {
        width: 100%;
        border: 0;
        padding: 20px;
        background-color: #1a1a1a;
        color: #fff;
        transition: all 0.3s;
        margin-bottom: 20px;
        cursor: pointer;
        border-radius: 40px;

        &:hover {
            background-color: #111010;
        }
    }
}

21_3. 로딩 기능 추가하기

기존에 로딩 소스는 임의적으로 settimeout을 설정하였지만, 이번에는 API 소스를 다 다운 받으면, 데이터가 출력하도록 설정했습니다.

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { fetchFromAPI } from '../utils/api'

import Main from '../components/section/Main';

import { CiBadgeDollar } from "react-icons/ci";
import { CiMedal } from "react-icons/ci";
import { CiRead } from "react-icons/ci";

const Channel = () => {
    const { channelId } = useParams();
    const [ channelDetail, setChannelDetail ] = useState();
    const [ loading, setLoading ] = useState(true);

    useEffect(() => {
        const fetchResults = async () => {
            try {
                const data = await fetchFromAPI(`channels?part=snippet&id=${channelId}`);
                setChannelDetail(data.items[0]);
            } catch (error) {
                console.error('Error fetching data:', error);
            } finally {
                setLoading(false);
            }
        };
        fetchResults();
    }, [channelId]);

    const channelPageClass = loading ? 'isLoading' : 'isLoaded';

    return (
        <Main 
            title = "유튜브 채널"
            description="유튜브 채널페이지입니다.">
            
            <section id='channel' className={channelPageClass};
                {channelDetail && (
                    <div className='channel__inner'>
                        <div className='channel__header' style={{ backgroundImage: `url(${channelDetail.brandingSettings.image.bannerExternalUrl})` }}>
                            <div className='circle'>
                                <img src={channelDetail.snippet.thumbnails.high.url} alt={channelDetail.snippet.title} />
                            </div>
                        </div>
                        <div className='channel__info'>
                            <h3 className='title'>{channelDetail.snippet.title}</h3>
                            <p className='desc'>{channelDetail.snippet.description}</p>
                            <div className='info'>
                                <span><CiBadgeDollar />{channelDetail.statistics.subscriberCount}</span>
                                <span><CiMedal />{channelDetail.statistics.videoCount}</span>
                                <span><CiRead />{channelDetail.statistics.viewCount}</span>
                            </div>
                        </div>
                    </div>
                )}
            </section>
        </Main>
    )
}

export default Channel

21_4. 채널 영상 추가하기

채널장이 올린 영상을 볼 수 있도록 설정하겠습니다.

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { fetchFromAPI } from '../utils/api'

import Main from '../components/section/Main';
import VideoSearch from '../components/videos/VideoSearch';

import { CiBadgeDollar } from "react-icons/ci";
import { CiMedal } from "react-icons/ci";
import { CiRead } from "react-icons/ci";

const Channel = () => {
    const { channelId } = useParams();
    const [ channelDetail, setChannelDetail ] = useState();
    const [ channelVideo, setChannelVideo ] = useState([]);
    const [ loading, setLoading ] = useState(true);

    useEffect(() => {
        const fetchResults = async () => {
            try {
                const data = await fetchFromAPI(`channels?part=snippet&id=${channelId}`);
                setChannelDetail(data.items[0]);

                const videosData = await fetchFromAPI(`search?channelId=${channelId}&part=snippet%2Cid&order=date`);
                setChannelVideo(videosData?.items);
            } catch (error) {
                console.error('Error fetching data:', error);
            } finally {
                setLoading(false);
            }
        };
        fetchResults();
    }, [channelId]);

    const channelPageClass = loading ? 'isLoading' : 'isLoaded';

    return (
        <Main 
            title = "유튜브 채널"
            description="유튜브 채널페이지입니다.">
            
            <section id='channel' className={channelPageClass};
                {channelDetail && (
                    <div className='channel__inner'>
                        <div className='channel__header' style={{ backgroundImage: `url(${channelDetail.brandingSettings.image.bannerExternalUrl})` }}>
                            <div className='circle'>
                                <img src={channelDetail.snippet.thumbnails.high.url} alt={channelDetail.snippet.title} />
                            </div>
                        </div>
                        <div className='channel__info'>
                            <h3 className='title'>{channelDetail.snippet.title}</h3>
                            <p className='desc'>{channelDetail.snippet.description}</p>
                            <div className='info'>
                                <span><CiBadgeDollar />{channelDetail.statistics.subscriberCount}</span>
                                <span><CiMedal />{channelDetail.statistics.videoCount}</span>
                                <span><CiRead />{channelDetail.statistics.viewCount}</span>
                            </div>
                        </div>
                        <div className='channel__video video__inner search'>
                            <VideoSearch videos={channelVideo} />
                        </div>
                    </div>
                )}
            </section>
        </Main>
    )
}

export default Channel

21_5. 채널 영상 더보기 구현하기

더 보기 기능을 구현하겠습니다.

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { fetchFromAPI } from '../utils/api'

import Main from '../components/section/Main';
import VideoSearch from '../components/videos/VideoSearch';

import { CiBadgeDollar } from "react-icons/ci";
import { CiMedal } from "react-icons/ci";
import { CiRead } from "react-icons/ci";

const Channel = () => {
    const { channelId } = useParams();
    const [ channelDetail, setChannelDetail ] = useState();
    const [ channelVideo, setChannelVideo ] = useState([]);
    const [ loading, setLoading ] = useState(true);
    const [ nextPageToken, setNextPageToken ] = useState(null); 

    useEffect(() => {
        const fetchResults = async () => {
            try {
                const data = await fetchFromAPI(`channels?part=snippet&id=${channelId}`);
                setChannelDetail(data.items[0]);

                const videosData = await fetchFromAPI(`search?channelId=${channelId}&part=snippet%2Cid&order=date`);
                setChannelVideo(videosData?.items);
                setNextPageToken(videosData?.nextPageToken);
            } catch (error) {
                console.error('Error fetching data:', error);
            } finally {
                setLoading(false);
            }
        };
        fetchResults();
    }, [channelId]);

    const loadMoreVideos = async () => {
        if (nextPageToken) {
            const videosData = await fetchFromAPI(`search?channelId=${channelId}&part=snippet%2Cid&order=date&pageToken=${nextPageToken}`);
            setChannelVideo(prevVideos => [...prevVideos, ...videosData.items]);
            setNextPageToken(videosData?.nextPageToken);
        }
    };

    const channelPageClass = loading ? 'isLoading' : 'isLoaded';

    return (
        <Main 
            title = "유튜브 채널"
            description="유튜브 채널페이지입니다.">
            
            <section id='channel' className={channelPageClass};
                {channelDetail && (
                    <div className='channel__inner'>
                        <div className='channel__header' style={{ backgroundImage: `url(${channelDetail.brandingSettings.image.bannerExternalUrl})` }}>
                            <div className='circle'>
                                <img src={channelDetail.snippet.thumbnails.high.url} alt={channelDetail.snippet.title} />
                            </div>
                        </div>
                        <div className='channel__info'>
                            <h3 className='title'>{channelDetail.snippet.title}</h3>
                            <p className='desc'>{channelDetail.snippet.description}</p>
                            <div className='info'>
                                <span><CiBadgeDollar />{channelDetail.statistics.subscriberCount}</span>
                                <span><CiMedal />{channelDetail.statistics.videoCount}</span>
                                <span><CiRead />{channelDetail.statistics.viewCount}</span>
                            </div>
                        </div>
                        <div className='channel__video video__inner search'>
                            <VideoSearch videos={channelVideo} />
                        </div>
                        <div className='channel__more'>
                            {nextPageToken && <button onClick={loadMoreVideos}>더 보기</button>}
                        </div>
                    </div>
                )}
            </section>
        </Main>
    )
}

export default Channel

마무리

git 올리기

터미널에서 다음과 같이 작성하겠습니다. 새로운 페이지가 올라오는 것을 확인 할 수 있습니다.

webstoryboyhwang@Webstoryboyui-MacBookPro webs-youtube % git add .
webstoryboyhwang@Webstoryboyui-MacBookPro webs-youtube % git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
    (use "git restore --staged ..." to unstage)
        modified:   src/assets/scss/section/_video.scss
        modified:   src/pages/Video.jsx

webstoryboyhwang@Webstoryboyui-MacBookPro webs-youtube % git commit -m "비디오 상세 페이지 만들기"
[main 0c967e9] 비디오 상세 페이지 만들기
    2 files changed, 108 insertions(+), 3 deletions(-)
webstoryboyhwang@Webstoryboyui-MacBookPro webs-youtube % git push -u origin main
Enumerating objects: 17, done.
Counting objects: 100% (17/17), done.
Delta compression using up to 10 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 1.90 KiB | 1.90 MiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 6 local objects.
To https://github.com/webstoryboy/webs-youtube.git
    f818a8a..0c967e9  main -> main
branch 'main' set up to track 'origin/main'.

비디오 상세 페이지에는 이 데이터 뿐만 아니라 댓글이나 연관 영상들도 만들 수 있습니다. 같이 해보지는 않겠지만 혼자서 도전하는 습관도 중요합니다. 한번 도전해보세요! 오늘도 수고하셨습니다. 😘


예제 목록

댓글