깃허브 주소
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="이전"
>
<
</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="다음"
>
>
</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 문서를 참고해서 다양한 카테고리에 따른 영상 렌더링을 구현 해보세요 ~ ☺️
'카카오x구름 풀스택 > 프로젝트' 카테고리의 다른 글
[Youtube-clone 팀 프로젝트] React 활용한 유튜브 "메인화면" Redux 적용 방법 및 회고 - 4 (0) | 2025.01.10 |
---|---|
[Youtube-clone 팀 프로젝트] React 활용한 유튜브 "메인 화면-드롭다운메뉴" 제작 및 회고 - 2 (0) | 2025.01.09 |
[Youtube-clone 팀 프로젝트] React 활용한 유튜브 "메인 화면" 제작 및 회고 - 1 (1) | 2025.01.09 |
[Youtube clone 개인 프로젝트] 코드 리뷰 (1) | 2024.11.20 |