import React from 'react';
import Nav from './Nav';
import VideoPlayer from './VideoPlayer';
import VideoList from './VideoList';
import { YOUTUBE_API_KEY } from "../../config/youtube";
import { searchYouTube } from '../searchYouTube';
class App extends React.Component {
constructor(props) {
super(props);
// class component의 state를 videos, currentVideo 2개를 지정하였다.
// 2개로 충분한가요? state는 왜 App에 있고, 왜 2개를 만들었는지 고민하셨으면 합니다.
// 정답은 없습니다. 다만, 어떻게, 왜 state를 표현하는지가 중요한것 같습니다.
this.state = {
videos: [],
currentVideo: null
};
}
componentDidMount() {
// 초기 state 결과가 확정되어 있지 않고, 비동기적으로 서버요청을 하는 자원이므로,
// this.state에 할당하지 않습니다.
// constructor에서 setState를 수행하는 것은 anti-pattern입니다.
this.getYouTubeVideos('react tutorials');
}
// query를 인자로 받는 서버요청 함수를 만들어 줍니다.
getYouTubeVideos(query) {
var options = {
key: YOUTUBE_API_KEY,
query: query
};
// helper함수를 이용해서, 결과를 setState하여, state를 update해줍니다.
searchYouTube(options, videos =>
this.setState({
videos: videos,
currentVideo: videos[0]
})
);
}
// lifting state up 함수입니다. 자식 components가 setState를 실행할 수 있게합니다.
// 그러기 위해서는 props로 내려줘야겠죠?
// reactjs.org를 다시 한번 확인하세요!
handleVideoListEntryTitleClick(video) {
// 함수의 이름을 지을 때는 그 기능을 예상가능하게 지어야합니다.
// 본인이 compiler 혹은 컴퓨터가 되려하지마세요. 저희는 개발자이고, 사람입니다.
// ex) clickEventHandler() { ... } : 어떤 것을 하는지, 모릅니다. 클릭 이벤트 핸들러..
this.setState({
currentVideo: video
});
}
render() {
return (
{/* lifting state up 함수를 props로 내려줍니다. this를 명시할 수 있도록 bind 해줍니다. */}
<div>
<Nav handleSearchInputChange={this.getYouTubeVideos.bind(this)} />
<div className="col-md-7"> { {/* bootstrap를 참고하세요! 7의 의미는 무엇인가요? */}
<VideoPlayer video={this.state.currentVideo} />
</div>
<div className="col-md-7">
<VideoList
// lifting state up 함수를 props로 내려줍니다. this를 명시할 수 있도록 bind 해줍니다.
handleVideoListEntryTitleClick={this.handleVideoListEntryTitleClick.bind(
this
)}
videos={this.state.videos}
/>
</div>
</div>
);
}
}
export default App;
위의 코드는 레퍼런스 전문이다
코드를 분석하며 내가 궁금했던 부분에 대해 알아본다
VideoListEntry 컴포넌트
안에 있는 .video-list-entry-title
를 클릭했을때 어떻게하면 app 컴포넌트
의 state
를 변경할 수 있을까?
// VideoListEntry.js
import React from 'react';
const VideoListEntry = ({video, handleVideoListEntryTitleClick}) => (
<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={() => handleVideoListEntryTitleClick(video)}
>
{video.snippet.title}
</div>
<div className="video-list-entry-detail">{video.snippet.description}</div>
</div>
</div>
);
export default VideoListEntry;
답은 아래의 코드에 있었다
// state
constructor(props) {
super(props);
this.state = {
videos: [],
currentVideo: null
};
}
// handleVideoListEntryTitleClick 메서드
handleVideoListEntryTitleClick(video) {
this.setState({
currentVideo: video
});
}
<VideoList
{handleVideoListEntryTitleClick={this.handleVideoListEntryTitleClick.bind(this)}
videos={this.state.videos}/>
handleVideoListEntryTitleClick 메서드에 현재 this를 바인딩하여 VideoList 컴포넌트에 props로 내려보내준다.
그리고 VideoList 컴포넌트에서는 아래와같이 사용할 수 있다
// VideoList.js
import React from 'react';
import VideoListEntry from './VideoListEntry';
const VideoList = ({videos, handleVideoListEntryTitleClick}) => (
<div className="video-list media">
{videos.map((video) =>
<VideoListEntry
key={video.etag}
video={video}
handleVideoListEntryTitleClick={handleVideoListEntryTitleClick}
/>
)}
</div>
);
export default VideoList;
위의 코드에서 props를 전달하는데 구조 분해 할당이 사용되었다.
VideoListEntry에 이벤트처리를 해줘야되는 .video-list-entry-title
이 있으므로 app 컴포넌트에서 VideoList로 props를 내려보내주었던 것처럼 한번더 내려보내준다.
map
메소드를 통해 비디오배열에서 추출한 하나의 비디오도 VideoListEntry 컴포넌트의 props로 내려보내준다
{videos.map((video) =>
<VideoListEntry
key={video.etag}
video={video}
handleVideoListEntryTitleClick={handleVideoListEntryTitleClick}
/>
)}
그럼 이제 VideoListEntry 컴포넌트에서는,
// VideoListEntry.js
import React from 'react';
const VideoListEntry = ({video, handleVideoListEntryTitleClick}) => (
<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={() => handleVideoListEntryTitleClick(video)}
>
{video.snippet.title}
</div>
<div className="video-list-entry-detail">{video.snippet.description}</div>
</div>
</div>
);
export default VideoListEntry;
<div
className="video-list-entry-title"
onClick={() => handleVideoListEntryTitleClick(video)}
>
.video-list-entry-title
의 onClick 이벤트에 VideoList 컴포넌트에서 내려받은 handleVideoListEntryTitleClick
함수를 넣어주고 그 함수의 인자로 VideoList 컴포넌트에서 내려받은 video
를 넣어준다.
이제 .video-list-entry-title
을 클릭하면 handleVideoListEntryTitleClick(video)
함수가 실행되어
handleVideoListEntryTitleClick(video) {
this.setState({
currentVideo: video
});
}
app 컴포넌트의 this가 바인딩되어 전해졌으므로 app 컴포넌트의 state인 currentVideo가 handleVideoListEntryTitleClick(video)
함수가 실행될때 같이 전달된 인자인 video
로 변경된다.
state가 변경되면,
{video : this.state.currentVideo}
를 props로 VideoPlayer 컴포넌트를 호출한다.<VideoPlayer video={this.state.currentVideo} />
import React from 'react';
const VideoPlayer = ({video}) => ( // (A)
!video ? <div className="video-player">Please wait...</div> :
<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>
);
export default VideoPlayer;
(A)에서 video
는 app 컴포넌트의 this.state.currentVideo
video.id.videoId
를 동영상 소스 URL과 함께 'src' 어트리뷰트에 속성으로 할당한다
<iframe className="embed-responsive-item" src={`https://www.youtube.com/embed/${video.id.videoId}`} allowFullScreen></iframe>
이렇게 해주면 현재 클릭한 타이틀의 영상이 .video-player
안에 생성된다