비디오를 선택하게 되면 선택되는 비디오를 기억했다가, 선택된 비디오에 관련된 정보를 보여주는 것을 구현한다.
선택된 비디오가 없는 null인 상태이고 이제 detail 컴포넌트를 만들어서 선택된 정보를 넘기는 걸 해본다.
app.jsx 코드에서 아래와 같이 추가한다. 선택된 것이 있으면 video 옆에다가 보여주게 해준다.
function App({ youtube }) {
const [videos, setVideos] = useState([]);
const [selectedVideo, setSelectedVideo] = useState(null);
const search = query => {
youtube
.search(query) //
.then(videos => setVideos(videos));
};
useEffect(() => {
youtube
.mostPopular() //
.then(videos => setVideos(videos));
}, []);
return (
<div className={styles.app}>
<SearchHeader onSearch={search} />
{selectedVideo && <VideoDetail video={selectedVideo}/>}
<VideoList videos={videos} />
</div>
);
}
export default App;
컴포넌트 폴더안에 video_detail 폴더를 하나 만들고 video_detail.jsx파일과 video_detail.module.css파일을 각각 만든다.
video_detail.jsx코드는 아래와 같이한다.
rsi를 이용해서 VideoDetail 컴포넌트를 만들어주고 import도 해준다. 선택된 video를 받아와주고 h1태그를 이용해서 간단하게 video에 title을 지정해준다.
import React from 'react';
import styles from './video_detail.module.css'
const VideoDetail = ({video}) => <h1>{video.snippet.title}</h1>
export default VideoDetail;
app에서 확인을 해주고 selected된 video가 있으면 video_detail을 보여줘야 하니까 제일 위에 코드에서
{selectedVideo && <VideoDetail video={selectedVideo} /}이 부분을 추가해준다.
즉 VideoDetail이라는 컴포넌트를 이용해서 video에 전달을 해줄껀데 selected된 Video를 전달해준다는 것이다.
그리고 비디오 리스트에서 클릭이 되었을 때 선택이 될 것이다. 핸들링하는 콜백함수를 만들어주고 추가해준다.
video_list.jsx에서 videoitem이 클릭이 되면 선택이 되면 여기서 처리가 된다. onVideoClick={onVideoClick}을 추가해주고 코드를 아래와 같이한다.
import React from 'react';
import VideoItem from '../video_item/video_item';
import styles from './video_list.module.css';
const VideoList = ({ videos, onVideoClick, display }) => (
<ul className={styles.videos}>
{videos.map(video => (
<VideoItem
key={video.id}
video={video}
onVideoClick={onVideoClick}
display={display}
/>
))}
</ul>
);
export default VideoList;
video_item.jsx에서는 코드를 아래와 같이 한다. li에서 onClick이 되면 onVideoClick에서 onClick이 되면 비디오를 전달하면 된다. const VideoItem에다가 video를 받아오도록 한다.
import React from 'react';
import styles from './video_item.module.css';
const VideoItem = ({ video, video: { snippet }, onVideoClick, display }) => {
const displayType = display === 'list' ? styles.list : styles.grid;
return (
<li
className={`${styles.container} ${displayType}`}
onClick={() => onVideoClick(video)}
>
<div className={styles.video}>
<img
className={styles.thumbnail}
src={snippet.thumbnails.medium.url}
alt="video thumbnail"
/>
<div className={styles.metadata}>
<p className={styles.title}>{snippet.title}</p>
<p className={styles.channel}>{snippet.channelTitle}</p>
</div>
</div>
</li>
);
};
export default VideoItem;
app.jsx에서 비디오 디테일과 리스트를 두 가지를 묶어서 스타일링을 해보면 section을 이용해본다.
class 이름은 styles에 있는 content라고 하고 두 가지 아이템이 있을 것이다. content 안에 selectedVideo&& <VideoDetail video={selectedVideo} />를 묶어서 div를 이용해서 넣어주고 클래스 이름은 styles.detail이라고 해준다.
ㅍideoList는 컴포넌트라면 className을 해주지 못한다. 해줄경우 className이라는 props자체가 여기 안으로 전달되기 때문에 스타일링이 되지 않아서 추가적으로 부모에서 자식 노드에 구조적인 스타일링을 해야 한다면 감싸서 해야한다.
<VideoList classvideos={videos} onVideoClick={selectVideo }/>
=>
<div className={styles.list}>
<VideoList classvideos={videos} onVideoClick={selectVideo }/>
</ div>
app.module.css에서 content는 display를 flex로 써서 글자와 그림이 왼쪽과 오른쪽으로 나오게 한다.
flex를 이용해서 1 1 grow와 shrink를 1 1로 지정하고 detail은 70퍼센트 list는 flex를 이용해서 동일하게 1 1 해서 공간이 늘어나게 하고 grow를 해준다.
.app {
max-width: 80rem;
}
.content {
display: flex;
}
.detail {
flex: 1 1 70%;
}
.list {
flex: 1 1 30%;
}
app.jsx에서 display라는 새로운 props을 만들어서 selectedVideo가 있으면 list를 보여주고 없으면 grid로 보여주게 코드를 작성한다.(1줄에 두 개씩 나오도록 props을 전달해준다.)
<div className={styles.list}>
<VideoList
videos={videos}
onVideoClick={selectVideo}
display={selectedVideo ? 'list' : 'grid'}
/>
</div>
video_list.jsx에서는 VideoList에서 해당하는 props에 맞게 CSS를 적용해준다.
const VideoList = ({ videos, onVideoClick, display }) => (
video_item.module.css에 오면 width가 50%로 정해져 있다. 즉 VideoList에서 display를 VideoItem으로 전달해 줘야 되는 것이 확실해진다. 그래서 display를 전달해 준다. VideoItem에 display가 전달이 되면 VideoItem에서 display를 받아준다.
const VideoList = ({ videos, onVideoClick, display }) => (
<ul className={styles.videos}>
{videos.map(video => (
<VideoItem
key={video.id}
video={video}
onVideoClick={onVideoClick}
display={display}
/>
))}
</ul>
);
video_item.jsx에서 className에 display를 넣어서 CSS에서 해주면 되는데 바로 리턴하는 것이 아니여서 코드블럭을 이용해준다.( {}을 이용) const에 displayType을 display가 list이면 styles에 있는 list를 쓰고 아니면 styles에 있는 grid를 쓰게 해준다.
그리고 displayType은 displayType에 따라서 styles.list를 쓰던지 grid를 쓰게된다. 이렇게 나눠서 스타일링을 해주고
const VideoItem = ({ video, video: { snippet }, onVideoClick, display }) => {
const displayType = display === 'list' ? styles.list : styles.grid;
return (
<li
className={`${styles.container} ${displayType}`}
onClick={() => onVideoClick(video)}
>
video_item CSS에서는 container에 container가 grid이면 width를 50% 쓰고 container가 list이면 100%쓰도록 한다.
선택이 되면 display가 list로 되고 선택이 된 게 없으면 grid로 된다.
.container {
padding: 0.2em;
}
.container.grid {
width: 50%;
}
.container.list {
width: 100%;
}
video_detail.jsx에서는 video에 detail이 들어가 있는 section이 있는데 클래스 이름은 styles에 있는 detail이라고 작성해준다. 디테일 안에는 유튜브의 iframe이 들어가면 된다. 받는 법은 일단 youtube data api사이트로 들어온다.(https://developers.google.com/youtube/v3/getting-started?hl=ko) 그리고 아래그림을 클릭한다.
그리고 iframe를 클릭한다. 그리고 sample에 가서 플레이어 매개변수 표시를 클릭해주고 iframe을 받아온다.
video_detail.jsx에서 코드는 아래와 같다. 클래스 이름은 styles에 있는 video로 해준다. width는 100%로 해주고 height는 500px 정도로 고정해 놓는다. 소스는 iframe에 나와있는 형식대로 url을 쓰고 받아온 비디오에 있는 id를 이용해서 소스를 만들었다.
디테일은 h2를 쓰고 video에 있는 snippet 안에 있는 title을 쓰고 h3에는 channelTitle을 쓴다.
그리고 설명을 넣어주면 pre태그를 이용해서 video에 있는 snippet안에 있는 description을 이용해준다.
여기서 계속 video가 반복되어지니까 video: {snippet}로 정의해준다.
import React from 'react';
import styles from './video_detail.module.css';
const VideoDetail = ({ video }) => (
<section className={styles.detail}>
<iframe
className={styles.video}
type="text/html"
width="100%"
height="500px"
src={`https://www.youtube.com/embed/${video.id}`}
frameborder="0"
allowfullscreen
></iframe>
<h2>{video.snippet.title}</h2>
<h3>{video.snippet.channelTitle}</h3>
<pre className={video.styles.description}>{snippet.description}</pre>
</section>
);
export default VideoDetail;
// =>
import React from 'react';
import styles from './video_detail.module.css';
const VideoDetail = ({ video, video: { snippet } }) => (
<section className={styles.detail}>
<iframe
className={styles.video}
type="text/html"
width="100%"
height="500px"
src={`https://www.youtube.com/embed/${video.id}`}
frameborder="0"
allowfullscreen
></iframe>
<h2>{snippet.title}</h2>
<h3>{snippet.channelTitle}</h3>
<pre className={styles.description}>{snippet.description}</pre>
</section>
);
export default VideoDetail;
전체적으로 패딩을 주면 video_detail.module.css에서 detail에 와서 padding을 0.2em 정도로 준다.
.detail {
padding: 0.2em;
}
.description {
white-space: pre-wrap;
}
video_list.module.css에서는 margin을 0으로 설정해준다.
.videos {
display: flex;
flex-wrap: wrap;
list-style: none;
padding-left: 0;
margin: 0;
}
다른 걸 누르면 pre태그의 문제가 생기는데 텍스트가 계속 연결되어 있으면 원래 공간을 넘어가서 사이즈가 계속해서 길어진다. pre태그에 와서 CSS로 해결이 된다. className에서 styles에 description이라고 해준다.
<pre className={styles.description}>{snippet.description}</pre>
video_detail.module.css에서는 detail에서 description을 한 다음에 여기서 white-space를 pre-wrap 랩핑 할 수 있도록 해준다.
.detail {
padding: 0.2em;
}
.description {
white-space: pre-wrap;
}
app.jsx에서 videos가 받아 줬다면 검색이 됐다면 다시 이거를 videos를 선택하고 나서 selectedVideo라는 함수를 이용해서 null로 지정하면 된다. 그러면 화면에서 검색창에 검색을 하면 검색목록으로 나가게 된다.
만약 새로운 결과를 받기전에 즉 비디오를 검색할 때 결과에 상관없이 항상 먼저 초기화를 하고 나서 비디오가 받아지면 비디오를 보여 주고 로딩스피너를 보여주고 싶다하면 목록 대신에 스피너가 나오도록 만들어서 비디오가 다 받아지면 setVideos를 선택하고 실패하면 에러 state를 만들어서 에러 state만 업데이트 하면 에러에 맞게 보여줄 수 있는 화면만 바꿔가면 된다.
이렇게 감을 잡는 작업도 해보자. 그리고 창이 줄어들면 리스트가 밑으로 내려오게 만들 수 있도록 하는 기능도 구현해볼 수 있는데 앱 컨텐트를 이용해서 flex-direction을 미디어쿼리를 이용해서 바꿔 주면 된다.
const search = query => {
youtube
.search(query) //
.then(videos => {
setVideos(videos);
setSelectedVideo(null);
});
};
// =>
const search = query => {
setSelectedVideo(null);
youtube
.search(query) //
.then(videos => setVideos(videos));
};
그리고 youtube.js에서 fetch를 이용해서 쓰는거 보다는 좀더 자주 사용하는 라이브러리가 있다. 어떤 것을 대체할 수 있는지도 알아보자.
// ex
async mostPopular() {
const response = await fetch(
`https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&maxResults=25&key=${this.key}`,
this.getRequestOptions
);
const result = await response.json();
return result.items;
}
<출처 : DreamCoding 리액트 개념 정리+유튜브 클론코딩: ellie>
참고: https://academy.dream-coding.com/courses/react-basic
리액트 강의 (유튜브 클론 코딩 + 실시간 전송 명함 카드 만들기 웹앱 만들기)
리액트 전반적인 개념 설명과 (클래스 컴포넌트와 함수 컴포넌트 그리고 리액트 훅까지) 실전 유튜브 클론 코딩 프로젝트. Firebas의 실시간 데이터베이스를 이용해 멋진 명함 카드 만들기 웹 어
academy.dream-coding.com
'React > youtube_clone_coding' 카테고리의 다른 글
fetch web APIs & Axios library 차이점 (0) | 2021.08.16 |
---|---|
마무리 단계(Error잡기) (0) | 2021.08.16 |
검색 기능 Refactoring (0) | 2021.08.10 |
검색 기능 구현하기 (0) | 2021.08.10 |
Youtube 검색 기능 UI 구현 (0) | 2021.08.10 |