React 스프린트의 시작입니다!🤔

기본적으로 알고 있는 내용이다 보니 수월한 진행을 예상한....

역시나, 코드스테이츠는 제가 모르는 부분만 콕콕 집어서 진행합니다! 대단하다,정말!

물론, 좋은 의미에서~ 배울게 끊이지 않기 때문입니다...


📺 Recast.ly

YouTubeAPI를 불러와서 간단한 웹 페이지를 구성하는 스프린트입니다.

image.png
코드스테이츠에서 기본적으로 주는 가이드라인입니다.

각 컴포넌트를 어떻게 구성해야 할지 미리 알려주는데요,

컴포넌트 구성
App > Nav > Search
App > VideoPlayer
App > VideoList > VideoListEntry

부모 자식 관계를 위와 같이 정리할 수 있습니다. 이제 코드를 알아봅시다.


fakeData

실제 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입니다.

모든 자식들이 불려오는 곳이라서 먼저 보면 이해하기 힘들 수 있습니다.

하지만 자식들을 먼저 보고, 다시 올라와서 참고하시면 좋을 것 같습니다!

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.js

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

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

가장 하위 자식인 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

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

끝으로, 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)
  });
};

간단한 정리가 끝났습니다.

부분 부분 리팩토링으로 코드를 수정할 예정이며, 설명이 매우 짧은 부분은 양해부탁드립니다.

테스트 케이스 통과를 위해서 다소 어색한 부분이 많으니 간단히 참고하시는게 좋습니다!