React 스프린트의 시작입니다!🤔
~기본적으로 알고 있는 내용이다 보니 수월한 진행을 예상한....~
역시나, 코드스테이츠는 제가 모르는 부분만 콕콕 집어서 진행합니다! 대단하다,정말!
물론, 좋은 의미에서~ 배울게 끊이지 않기 때문입니다...
YouTubeAPI를 불러와서 간단한 웹 페이지를 구성하는 스프린트입니다.
코드스테이츠에서 기본적으로 주는 가이드라인입니다.
각 컴포넌트를 어떻게 구성해야 할지 미리 알려주는데요,
컴포넌트 구성
App > Nav > Search
App > VideoPlayer
App > VideoList > VideoListEntry
부모 자식 관계를 위와 같이 정리할 수 있습니다. 이제 코드를 알아봅시다.
실제 API를 적용하기 전에 동일한 구조인 fakeData로 테스트해보아야 합니다.
다음은 fakeData의 구조입니다.
const fakeData = [{
etag: 'L332gQTY',
id: {
videoId: '000001'
},
snippet: {
title: 'Cute cat video',
description: 'The best cat video on the internet!',
thumbnails: {
default: {
url: 'https://i.redd.it/oa78q1ng7lc01.jpg',
}
}
}
},
{...}
]
먼저 부모인 App.js입니다.
모든 자식들이 불려오는 곳이라서 먼저 보면 이해하기 힘들 수 있습니다.
하지만 자식들을 먼저 보고, 다시 올라와서 참고하시면 좋을 것 같습니다!
App.js에서 가장 중요한 부분은 화면을 렌더링 하는 부분입니다.
LifeCycle 순서로, constructor가 먼저 실행되고, render()가 실행됩니다.
그런데 fetching이 다 되지 않은 상태에서 각 API에서 받은 state를 적용하게 되면
렌더할 대상이 없어서 오류가 나게 됩니다.
그 부분을 componentDidMount와 render의 Loading부분으로 해결합니다.
handleChange와 handleClick메서드는 자식들에게서 받은 값으로 setState합니다.
class App extends Component {
constructor(props) {
super(props);
// 기본 state(상태)값을 fakeData로 설정합니다.
this.state = {
videos: fakeData,
currentVideo: null,
};
this.handleClick = this.handleClick.bind(this);
this.handleChange = this.handleChange.bind(this)
}
// componentDidMount 시점에 searchYouTubeAPI를 받아와서 상태값을 변경합니다.
componentDidMount() {
searchYouTube({ query: "codestates", max: 5, key: YOUTUBE_API_KEY }, items => {
this.setState({
videos: items,
currentVideo: items[0]
});
});
}
// Search에서 받은 input값을 searchYouTubeAPI를 받아와서 상태값을 변경합니다.
handleChange(textInput) {
searchYouTube({ query: textInput, max: 5, key: YOUTUBE_API_KEY }, items => {
this.setState({
videos: items,
currentVideo: items[0],
});
});
}
// VideoListEntry에서 받은 click값을 state로 변경합니다. { video : video }
handleClick(currentVideo) {
this.setState({ currentVideo });
}
render() {
// ! 상태의 videos가 null이라면 로딩 페이지를 띄웁니다.
if(!this.state.videos) return <div>Loading...</div>;
const { handleChange, handleClick } = this;
const { videos, currentVideo } = this.state;
return (
<div>
<Nav handleChange={handleChange}/>
<div className="col-md-7">
<VideoPlayer video={currentVideo || fakeData[0]} />
</div>
<div className="col-md-5">
<VideoList
handleClick={handleClick}
videos={videos}
/>
</div>
</div>
);
}
}
VideoPlayer입니다. 화면에서 가장 큰 부분을 차지하는 ~자식~이죠!
실제로 수정한 부분은 각 props를 전달한 부분입니다.
부모에서 props로 video를 받아와서 각 값에 전달하는 것이죠!
이런 패턴은 모든 파일에 적용됩니다.
이를 fakeData의 구조에 맞추어서 각 태그에 적용해줍니다.
// VideoPlayer.js
const VideoPlayer = ({ video }) => {
return (
<div className="video-player">
<div className="embed-responsive embed-responsive-16by9">
<iframe className="embed-responsive-item"
src={`https://www.youtube.com/embed/${video.id.videoId}`} allowFullScreen></iframe>
</div>
<div className="video-player-details">
<h3>{video.snippet.title}</h3>
<div>{video.snippet.description}</div>
</div>
</div>
)};
VideoList는 App.js와 VideoListEntry를 연결하는 부분입니다.
그저 전달만 해주면 되기 때문에 정말 전달만 해줍니다.
// VideoList.jsx
const VideoList = ({ videos, handleClick }) => (
<div className="video-list media">
{videos.map(video =>
<VideoListEntry video={video} key={video.etag} handleClick={handleClick}/>
)}
</div>
);
가장 하위 자식인 VideoListEntry입니다.
이제 상위에서 전달받은 props로 Entry의 상태를 전달해주어야 합니다.
하지만 stateless, 즉 상태를 위로 전달할 수 없으므로 props로 전달하는 것이죠!
// VideoListEntry.jsx
const VideoListEntry = ({ video, handleClick }) => {
return (
<div className="video-list-entry">
<div className="media-left media-middle">
<img className="media-object" src={video.snippet.thumbnails.default.url} alt="" />
</div>
<div className="media-body">
<div className="video-list-entry-title" onClick={() => handleClick(video)}>{video.snippet.title}</div>
<div className="video-list-entry-detail">{video.snippet.description}</div>
</div>
</div>
)
};
Nav.js는 App.js에서 handleChange를 받아와서 Search로 전달하는 역할을 합니다.
전달하는 역할을 제외하곤 HTML 적인 동작 뿐입니다.
// Nav.jsx
const Nav = ({ handleChange }) => (
<nav className="navbar">
<div className="col-md-6 col-md-offset-3">
<Search handleChange={handleChange}/>
</div>
</nav>
);
Search.js는 Nav.js에서 받은 props를 이용하여 value를 전달해줍니다.
const Search = ({ handleChange }) => {
return (
<div className="search-bar form-inline">
<input className="form-control" type="text" onChange={e => handleChange(e.target.value)}/>
<button className="btn hidden-sm-down">
<span className="glyphicon glyphicon-search"></span>
</button>
</div>
);
}
끝으로, searchYouTube.js에서는 fetch동작으로 youtubeAPI를 호출합니다.
반환값으로 data.item을 콜백에 담아 반환합니다.
이 값을 App.js에서 각 메서드에 사용하게 됩니다.
처음의 App.js로 돌아가서 살펴보시면 됩니다.
export const searchYouTube = ({ query, max, key }, callback) => {
let option = {
q: query,
part: "snippet",
key: key,
maxResults: max,
type: "video",
}
let url = "https://www.googleapis.com/youtube/v3/search?";
for(let key in option) {
url = url + `${key}=${option[key]}&`;
}
url = url.substr(0, url.length - 1);
// ! fetching
fetch(url)
.then(res => res.json())
.then(data => {
console.log(data)
return callback(data.items)
});
};
간단한 정리가 끝났습니다.
부분 부분 리팩토링으로 코드를 수정할 예정이며, 설명이 매우 짧은 부분은 양해부탁드립니다.
테스트 케이스 통과를 위해서 다소 어색한 부분이 많으니 간단히 참고하시는게 좋습니다!