처음 이머시브 스프린트를 시작할 때는 TIL을 매일매일 쓰자...! 라고 다짐했지만 5주가 지난 지금을 보면 매주 1개? 2개 쓰고 있다. 변명을 하면 바쁘다 ㅠㅠ 매일 하루 쓰는 것을 우습게 생각했던 내 자신이 우습다. 그래도
블로깅 끊기지 않게 계속 합시다~
위 다이어그램은 이번 스프린트의 흐름을 정리해 놓은 그림이다. App이라는 source of truth에서 state안에 있는 내용과 또 안에 작성되어 있는 setState가 가능한 함수들을 이제 필요한 component들에게 내려준다. 자신만의 state를 설정해주어야 하는 component들은 class 로 짜고, 아닌 것들은 functional로 짠다. 사실 이 순서만 잘 기억하고 어떤 함수가 필요해 state를 바꾸어줘야 하는지, 무엇을 props로 내려줘야 하는지 잘 정리만 하면 그렇게 막 어렵진 않다. 하지만 처음은 막막했다. 지금은 다 했으니까 ㅋㅋㅋㅋㅋ
자 이제 본격적으로 시작하기 전, npm 을 설치해 주고, 또 React Developer Tools 크롬 확장기능을 설치한다.
일단 sprint 순서는 =>
1. VideoList 와 VideoListEntry
2. VideoPlayer
3. Nav 와 Search (Use YouTube API Key)
4. 실시간 검색과 debounce 구현
5. WatchList 와 WatchListEntry
6. Use Local Storage
기억해야 할건 각 component들을 만들면서 끊임없이 App.js 에 내용을 추가해주고 확인해줘야 한다. 아래 component들에 필요한 내용은 App에서 만들어서 내려줘야 하기 때문이다!
먼저 이 컴포넌트들은 처음 딱 페이지가 렌더 됐을 때 옆에 나오는 동영상 목록을 보여주는 component들이다. VideoListEntry 가 영상 하나, 타이틀과 description을 담당하고 VideoList는 그것을 다 묶어서 정리한다.
import React from "react";
import VideoListEntry from "./VideoListEntry";
// 실제 API를 쓰게 되면 이 fakeData는 더이상 import 하지 않아야 합니다.
const VideoList = props => (
<div className="video-list media">
{props.videos.map(data => (
<VideoListEntry
handleVideo={props.handleVideo}
key={data.id.videoId}
video={data}
handleWatch={props.handleWatch}
/>
))}
</div>
);
export default VideoList;
먼저 App.js 에서 첫단계로 내려오는 VideoList.js를 보면 일단 props를 받아와 VideoListEntry에 필요한 부분들을 넘겨주는 것을 볼 수 있다. 여기서 사용되는 videos는 App.js에서 내려다주는, state안에 있는 videos 이다.
videos는 5개의 객체를 가진 하나의 배열로 VideoList에서는 map을 통해 각 객체를 VideoListEntry안으로 넣어줘서 각 영상마다 VideoListEntry 컴포넌트를 실행시킬 수 있도록 한다.
handleVideo는 App.js에서 만든 영상 타이틀을 클릭하면 App.js의 state에 있는 video를 변경시켜주어 videoPlayer가 사용할 state의 video 값을 변경시켜 주는 함수이다.
handleWatch는 버튼을 클릭시, state의 videoWatch 배열에 이 데이터를 추가시켜주는 함수이다.
Video라는 넘겨주는 항목은 배열의 data를 넘겨주기 위해 만든 것이다. Key는 무슨 요소에 변화가 있었는지 더 쉽게 알기 위해 넣어주는 값이다. 전체적인 함수와는 무관하다.
요약하자면, VideoList는 배열 속 5 개 객체에 각각 VideoListEntry를 실행시켜 각 영상에 필요한 부분들을
조립한다.
import React from "react";
const VideoListEntry = (props) => {
const { video, handleVideo, handleWatch} = props;
const snippet = video.snippet;
return (
<div className="video-list-entry">
<div className="media-left media-middle">
<img
className="media-object"
src={snippet.thumbnails.high.url}
alt=""
/>
</div>
<div className="media-body">
<div onClick={() => handleVideo(video)} className="video-list-entry-title">
{snippet.title}
</div>
<div className="video-list-entry-detail">{snippet.description}</div>
<button onClick={() =>handleWatch(video)} className="watchButton">Watch Later</button>
</div>
</div>
);
};
export default VideoListEntry;
import React from 'react';
import Search from './Search';
const Nav = ({ searchHandler }) => (
<nav className="navbar">
<div className="col-md-6 col-md-offset-3">
<Search searchHandler={searchHandler}/>
</div>
</nav>
);
export default Nav;
import React from "react";
import searchYouTube from "../searchYouTube";
class Search extends React.Component {
constructor(props) {
super(props)
this.state = {
value: '',
count: 0 //검색 남용 방지
}
this.handleClick = this.handleClick.bind(this);
}
componentDidMount() {
setInterval(
() => this.setState({count: 0}),
5000
);
}
handleClick() {
const { value, count } = this.state;
if (count > 3) {
alert('적당히 하세요');
return ;
}
if (value.length === 0) {
alert('입력을 해야지');
return ;
}
const { searchHandler } = this.props;
this.setState({count: count + 1});
searchYouTube({query: value}, searchHandler);
}
handleChange(e) {
const value = e.target.value;
const { searchHandler } = this.props;
if (value.length > 3){
this.setState({value});
searchYouTube({query: value}, searchHandler)
}
}
render() {
return (
<div className="search-bar form-inline">
<input className="form-control" type="text" onChange={(e) => this.handleChange(e)}/>
<button className="btn hidden-sm-down" onClick={this.handleClick}>
<span className="glyphicon glyphicon-search"></span>
</button>
</div>
);
}
}
export default Search;
import YOUTUBE_API_KEY from "../config/youtube";
const searchYouTube = ({ query, max = 5, key = YOUTUBE_API_KEY }, callback) => {
return fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&q=
${query}&maxResults=${max}&key=${key}&type=video`)
.then(response => response.json())
.then(json => callback(json.items))
};
이 부분을 이용한다!
this.setState({value});
searchYouTube({query: value}, searchHandler)
}
import React from "react";
import WatchListEntry from "./WatchListEntry";
// 실제 API를 쓰게 되면 이 fakeData는 더이상 import 하지 않아야 합니다.
const WatchList = props => (
<div className="watch-list watchmedia">
{props.videoWatch.map(data => (
<WatchListEntry
handleVideo={props.handleVideo}
key={data.id.videoId}
video={data}
removeWatch={props.removeWatch}
/>
))}
</div>
);
export default WatchList;
import React from "react";
const WatchListEntry = ({video, handleVideo, removeWatch}) => {
const snippet = video.snippet;
return (
<div className="watch-list-entry">
<div className="media-left media-middle">
<img
className="media-object"
src={snippet.thumbnails.high.url}
alt=""
/>
</div>
<div className="media-body">
<div onClick={() => handleVideo(video)} className="video-list-entry-title">
{snippet.title}
</div>
<div className="video-list-entry-detail">{snippet.description}</div>
<button onClick={() => removeWatch(video)} className="watchButton">Remove</button>
</div>
</div>
);
};
export default WatchListEntry;
import React from "react";
import Nav from "./Nav";
import VideoPlayer from "./VideoPlayer";
import VideoList from "./VideoList";
import WatchList from "./WatchList";
import searchYouTube from "../searchYouTube";
class App extends React.Component {
constructor(props) {
super(props);
this.handleVideo = this.handleVideo.bind(this);
this.searchHandler = this.searchHandler.bind(this);
this.handleWatch = this.handleWatch.bind(this);
this.removeWatch = this.removeWatch.bind(this);
this.saveData = this.saveData.bind(this);
this.state = {
video: "",
videos: {},
videoWatch: []
};
}
componentDidMount(){
searchYouTube({query: "izone"}, this.setInitial.bind(this))
if (JSON.parse(localStorage.getItem("list") !== null)) {
this.setState({
videoWatch: JSON.parse(localStorage.getItem("list"))
})
}
}
saveData(){
if (this.state.videoWatch.length === 0){
localStorage.clear()
return;
}
else{
localStorage.setItem('list', JSON.stringify(this.state.videoWatch))
}
}
setInitial(item){
this.setState({
video: item[0],
videos: item
})
}
handleVideo(item) {
this.setState({
video: item
});
}
searchHandler(items) {
this.setState({
videos: items
})
}
handleWatch(items) {
if (this.state.videoWatch.includes(items)){
return;
}
else{
this.setState({
videoWatch: this.state.videoWatch.concat(items)
}, () => this.saveData())
}
}
removeWatch(item) {
this.setState({
videoWatch: this.state.videoWatch.filter(element => element !== item)
}, () => this.saveData())
}
render() {
if (this.state.video.length === 0){
return (<h1>loading</h1>)
}
return (
<div>
<Nav searchHandler={this.searchHandler}/>
<div className="col-md-7">
<VideoPlayer
video={this.state.video}
handleWatch={this.handleWatch}
/>
</div>
<div className="col-md-5">
<VideoList
handleVideo={this.handleVideo}
videos={this.state.videos}
videoWatch={this.state.videoWatch}
handleWatch={this.handleWatch}
/>
</div>
<div className="watchlist-title">Watch List</div>
<div className="watchlist">
<WatchList
handleVideo={this.handleVideo}
videoWatch={this.state.videoWatch}
removeWatch={this.removeWatch}
/>
</div>
</div>
);
}
}
export default App;
여러가지 종합 후 나오는 결과 값은 =>