본문 바로가기
카카오x구름 풀스택/프로젝트

[Youtube-clone 팀 프로젝트] React 활용한 유튜브 "메인화면 - 카테고리바" 제작 및 회고 - 3

by 꾸주니=^= 2025. 1. 9.

깃허브 주소

https://github.com/PrograMemo-Groom/youtube-clone

 

GitHub - PrograMemo-Groom/youtube-clone: [Team 1] prograMemo youtube-clone site

[Team 1] prograMemo youtube-clone site. Contribute to PrograMemo-Groom/youtube-clone development by creating an account on GitHub.

github.com

 

배포사이트 주소

https://programemo-youtube.netlify.app/

 

PrograMemo Youtube

 

programemo-youtube.netlify.app

 


이번에는 카테고리바에 대해서 알아보겠습니다.
카테고리바는 메인화면 검색창 밑에 위치하며, 카테고리 버튼을 클릭하면 관련된 영상들이 나타나도록 구현했습니다.

 

작동 영상을 확인해보겠습니다.

원래, 유튜브처럼 사용자기반으로 카테고리가 다르게 나타내고 싶었으나,
유튜브 토큰 연결을 하지 않고 구현을 했어서 유튜브 api 문서를 참고하여 카테고리 번호를 입력해서 화면에 렌더링했습니다.

 

다음은 카테고리별 번호들을 정리한 표를 보여드리겠습니다

번호 카테고리 이름(영문) 카테고리 이름(한글)
1 Film & Animation 영화 및 애니메이션
2 Autos & Vehicles 자동차 및 차량
10 Music 음악
15 Pets & Animals 애완동물 및 동물
17 Sports 스포츠
18 Short Movies 단편 영화
19 Travel & Events 여행 및 이벤트
20 Gaming 게임
21 Videoblogging 비디오 블로깅
22 People & Blogs 사람 및 블로그
23 Comedy 코미디
24 Entertainment 엔터테인먼트
25 News & Politics 뉴스 및 정치
26 Howto & Style 노하우 및 스타일
27 Education 교육
28 Science & Technology 과학 및 기술
29 Nonprofits & Activism 비영리 및 활동
30 Movies 영화
31 Anime/Animation 애니메이션
32 Action/Adventure 액션/어드벤처
33 Classics 클래식
34 Comedy 코미디
35 Documentary 다큐멘터리
36 Drama 드라마
37 Family 가족
38 Foreign 외국 영화
39 Horror 공포 영화
40 Sci-Fi/Fantasy 공상 과학/판타지
41 Thriller 스릴러
42 Shorts 쇼츠
43 Shows 쇼 프로그램
44 Trailers 예고편

저는 위 중에서 작동되는 몇 가지 카테고리만 사용해서 메인화면에 나타냈습니다.

 


카테고리바 코드

Main.js

import React, {useState} from 'react';
import CategoryBar from "./category/CategoryBar";
import MainVideos from "./videos/MainVideos";


const Main = () => {
    const [fetchFunction, setFetchFunction] = useState(null);

    const handleCategoryChange = (fetchFunctionName) => {
        setFetchFunction(fetchFunctionName);
    };

    return (
        <div>
            <CategoryBar onCategoryChange={handleCategoryChange} />
            <MainVideos fetchFunction={fetchFunction} />
        </div>
    );
};

export default Main;

Main.js에 CategoryBar를 연결한 모습입니다.

 

CategoryBar코드는 다음과 같습니다.

CategoryBar.js

import React, { useRef, useEffect, useState } from "react";
import styles from "./CategoryBar.module.css";

const categories = [
    { name: "전체", fetchFunction: null },
    { name: "음악", fetchFunction: "10" },
    { name: "뉴스", fetchFunction: "25" },
    { name: "게임", fetchFunction: "20" },
    { name: "애니메이션", fetchFunction: "1" },
    { name: "스케치 코미디", fetchFunction: "23" },
    { name: "자동차", fetchFunction: "2" },
    { name: "과학", fetchFunction: "28" },
    { name: "스포츠", fetchFunction: "17" },
    { name: "블로그", fetchFunction: "22" },
];


const CategoryBar = ({ onCategoryChange }) => {
    const categoryBarRef = useRef(null);
    const [activeCategory, setActiveCategory] = useState("전체");
    const [isPrevVisible, setPrevVisible] = useState(false);
    const [isNextVisible, setNextVisible] = useState(false);

    const scrollLeft = () => {
        categoryBarRef.current.scrollBy({
            left: -200,
            behavior: "smooth",
        });
    };

    const scrollRight = () => {
        categoryBarRef.current.scrollBy({
            left: 200,
            behavior: "smooth",
        });
    };

    const handleScroll = () => {
        const { scrollLeft, scrollWidth, clientWidth } = categoryBarRef.current;
        setPrevVisible(scrollLeft > 0);
        setNextVisible(scrollLeft + clientWidth < scrollWidth - 1);
    };

    const updateButtonVisibility = () => {
        const { scrollWidth, clientWidth } = categoryBarRef.current;
        setNextVisible(scrollWidth > clientWidth);
        setPrevVisible(false);
    };

    useEffect(() => {
        if (categoryBarRef.current) {
            updateButtonVisibility();
        }
        window.addEventListener("resize", updateButtonVisibility);

        return () => {
            window.removeEventListener("resize", updateButtonVisibility);
        };
    }, []);

    const handleCategoryClick = (category) => {
        console.log("Category clicked:", category.name);
        console.log("Category fetchFunction:", category.fetchFunction);

        setActiveCategory(category.name);

        if (onCategoryChange && typeof onCategoryChange === "function") {
            if (typeof category.fetchFunction === "function") {
                category
                    .fetchFunction()
                    .then((result) => {
                        onCategoryChange(result);
                    })
                    .catch((error) => {
                        console.error("Error fetching data:", error.message);
                    });
            } else {
                onCategoryChange(category.fetchFunction);
            }
        }
    };

    return (
        <div className={styles.container}>
            <div className={styles.categoryWrapper}>
                {isPrevVisible && (
                    <div className={styles.buttonWrapper} style={{ left: "10px" }}>
                        <button
                            onClick={scrollLeft}
                            className={styles.prevButton}
                            aria-label="이전"
                        >
                            &lt;
                        </button>
                        <span className={styles.buttonText}>이전</span>
                    </div>
                )}
                <div
                    className={styles.categoryBar}
                    ref={categoryBarRef}
                    onScroll={handleScroll}
                >
                    {categories.map((category, index) => (
                        <button
                            key={index}
                            onClick={() => handleCategoryClick(category)}
                            className={`${styles.categoryButton} ${
                                activeCategory === category.name
                                    ? styles.activeCategoryButton
                                    : ""
                            }`}
                        >
                            {category.name}
                        </button>
                    ))}
                </div>
                {isNextVisible && (
                    <div className={styles.buttonWrapper} style={{ right: "10px" }}>
                        <button
                            onClick={scrollRight}
                            className={styles.nextButton}
                            aria-label="다음"
                        >
                            &gt;
                        </button>
                        <span className={styles.buttonText}>다음</span>
                    </div>
                )}
            </div>
        </div>
    );
};

export default CategoryBar;

 

CategoryBar.module.css

.container {
    display: flex;
    flex-direction: column;
}

.categoryBar {
    display: flex;
    overflow-x: auto;
    padding: 10px 20px;
    margin-left: 10px;
    scroll-behavior: smooth;
    background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}

.categoryButton {
    margin-right: 10px;
    padding: 8px 12px;
    border: none;
    border-radius: 10px;
    background-color: #F2F2F2;
    color: #000000;
    font-size: 14px;
    cursor: pointer;
    white-space: nowrap;
    text-decoration: none;
}

.categoryButton:hover {
    background-color: #e4e4e4;
}

.activeCategoryButton {
    background-color: #000000;
    color: #ffffff;
    font-weight: bold;
}

.activeCategoryButton:hover{
    background-color: #000000;
    color: #ffffff;
}

.categoryWrapper {
    display: flex;
    align-items: center;
    position: relative;
    margin-bottom: 10px;
}

.categoryBar::-webkit-scrollbar {
    display: none;
}

.buttonWrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;
}

.prevButton {
    position: absolute;
}

.nextButton {
    position: absolute;
}

.prevButton,
.nextButton {
    border: none;
    border-radius: 50%;
    cursor: pointer;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    color: #000000;
    background-color: rgba(255, 255, 255, 255);
}

.prevButton:hover,
.nextButton:hover {
    background-color: #f0f0f0;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.buttonWrapper:hover .buttonText {
    width: 35px;
    height: 35px;
    text-align: center;
    align-content: center;
    border-radius: 10%;
    margin-top: 100px;
    font-size: 12px;
    color: #ffffff;
    background-color: #222222;
    opacity: 70%;
    transition: opacity 0.3s ease;
}

 

주요 기능

1) 카테고리 선택 및 데이터 연동

  • 카테고리 데이터 구조: categories 배열에 카테고리 이름과 API 요청에 필요한 fetchFunction을 매핑.
  • 클릭 이벤트 처리: 사용자가 버튼을 클릭하면, 선택된 카테고리를 기반으로 데이터를 가져옵니다.
const handleCategoryClick = (category) => {
    setActiveCategory(category.name);
    if (onCategoryChange) {
        onCategoryChange(category.fetchFunction);
    }
};

활성화 상태 표시: 선택된 카테고리는 activeCategory 상태로 관리되며, 스타일링으로 시각적으로 강조됩니다.

 

2) 스크롤 인터랙션

  • 좌우 스크롤 버튼 제공:
    • 카테고리가 화면에 넘칠 경우, 좌우로 스크롤할 수 있는 버튼을 제공합니다.
    • 버튼 클릭 시 스크롤이 부드럽게 이동하도록 구현
const scrollLeft = () => {
    categoryBarRef.current.scrollBy({ left: -200, behavior: "smooth" });
};
const scrollRight = () => {
    categoryBarRef.current.scrollBy({ left: 200, behavior: "smooth" });
};

 

  • 스크롤 이벤트 감지:
    • 스크롤 위치를 실시간으로 감지하여 "이전" 및 "다음" 버튼의 가시성을 조정.
const handleScroll = () => {
    const { scrollLeft, scrollWidth, clientWidth } = categoryBarRef.current;
    setPrevVisible(scrollLeft > 0);
    setNextVisible(scrollLeft + clientWidth < scrollWidth - 1);
};

 

3) 반응형 디자인

  • CSS로 구현한 스크롤 숨김 및 레이아웃:
    • overflow-x: auto로 스크롤 가능 영역을 만들고, 사용자 경험을 해치지 않도록 스크롤바를 숨김
.categoryBar::-webkit-scrollbar {
    display: none;
}
  • 모바일/데스크톱 대응:
    • CSS와 JavaScript를 조합해 다양한 화면 크기에서도 안정적인 사용자 경험 제공.

 


이상으로 CategoryBar 컴포넌트에 대해 알아봤습니다.
CategoryBar 컴포넌트는 직관적이고 유연한 디자인을 통해 동영상 플랫폼에서 카테고리를 탐색하는 최적의 사용자 경험을 제공합니다.
유튜브 API 문서를 참고해서 다양한 카테고리에 따른 영상 렌더링을 구현 해보세요 ~ ☺️