분류 Reactjs

자신의 YouTube 복제 웹 앱을 구축하는 방법

컨텐츠 정보

  • 조회 276 (작성일 )

본문

나만의 YouTube 클론 웹 앱을 구축하는 방법 : 심층적인 React 자습서


소개 


이 기사는 실제 프로젝트를 처음부터 만들어서 React에서 배우고 발전시키려는 사람을 위한 것입니다. 이 프로젝트를 구축하면 React의 모든 주요 개념에 대한 지식이 강화됩니다. 이 프로젝트에는 React 컴포넌트, 프로젝트 구조, 라이프 사이클 방법, 상태 관리, 부모에서 자식 컴포넌트로 소품 전달, API 요청 등이 포함됩니다.


이 기사는 React로 시작한 모든 사람들과 이미 알고 있지만 지식을 향상 시키고 자하는 사람들에게 적합합니다.


특정 단계에서 앱이 작동하지 않으면 작은 오타가 발생할 수 있습니다. 이 GitHub 리포지토리를 참조하고 코드를 복사하면 몇 시간 동안 오류를 해결하지 않고도 프로젝트 작업을 즉시 계속할 수 있습니다. 또한, 별표를 줘도 됩니다! ⭐ :)


그냥 메모 


이 기사를 쓰는 동시에 YouTube 동영상도 만들었습니다!


비디오에 모든 코드 줄에 대한 자세한 설명이 있다는 사실을 충분히 강조 할 수 없습니다. 먼저 비디오를 보면서 시청하는 동안 / 코드를 따르는 것이 좋습니다. 비디오는 이 기사보다 훨씬 더 깊이 있게 설명되어 있습니다. 그런 다음이 기사에서 간결한 코드 블록을 참조로 사용할 수 있습니다.


비디오는 또한 React의 모범 사례 주제 (구조화, 가져 오기 / 내보내기, 클래스 기반 및 기능적 구성 요소 등)에 대해 다루며 중요한 것은 비디오를 통해 반복되므로 초보자에게 더 친숙합니다.


https://youtu.be/VPVzx1ZOVuw에 대한 전체 비디오 링크는 다음과 같습니다.


프로젝트 개요 


비디오 시작 부분에서 완제품의 기능을 확인할 수 있습니다.


Screen-Shot-2019-08-04-at-08.50.18.png 


프로젝트 설정 


필요한 모든 디렉토리와 파일을 만드는 프로세스 속도를 높이기 위해 명령 행을 사용할 것입니다. Visual Studio Code를 사용하는 경우 매우 편리한 내장 터미널이 있지만 VSC를 사용하지 않는 경우 Windows의 Windows 또는 터미널 Mac과 함께 제공되는 명령 프롬프트를 사용할 수 있습니다.


1. 새 디렉토리를 만듭니다 


mkdir youtube-clone-app && cd youtube-clone-app 


2. 새로운 React 앱 만들기 


create-react-app ./ 


그런 다음 새로 만든 디렉토리로 이동하여 종속성을 설치하십시오.


npm i -S axios @material-ui/core 


3. 프로젝트를 실행하십시오.


npm start 


4. Open another terminal window   


앱이 실행되는 동안 동시에 추가 명령을 실행할 수 있습니다. (VSC의 플러스 아이콘).


Dive into coding 


코드 작성을 시작하기 전에 불필요한 상용구 코드 create-react-app을 삭제합니다.

rm -rf src

이제 다음을 입력하여 새로운 src 디렉토리를 만들 수 있습니다 :

mkdir src

그리고 프로젝트에서 가장 중요한 두 파일을 추가 할 수 있습니다.

touch src/App.js src/index.js

index.js 파일 내부에서 React 애플리케이션을 시작합니다.

import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';

ReactDOM.render(<App />, document.querySelector('#root'));

App.js 내부에서 초기 프로젝트 구조를 설정합니다.

import React from 'react';

import { Grid } from '@material-ui/core';

class App extends React.Component {
	render() {
      return (
        <Grid style={{ justifyContent: 'center' }} container spacing={10}>
          <Grid item xs={11}>
            <Grid container spacing={10}>
              <Grid item xs={12}>
              	{/* This is where SearchBar component will go */}
              </Grid>
              <Grid item xs={8}>
              	{/* This is where VideoDetail component will go */}
              </Grid>
              <Grid item xs={4}>
              	{/* This is where VideoList component will go */}
              </Grid>
            </Grid>
          </Grid>
        </Grid>
      );
    }
}

export default App;

다음으로 목록에 있는 것은 API 호출을 설정하는 것입니다. https://console.developers.google.com/에서 Google 계정에 로그인 해야 합니다. 그런 다음 YouTube Data API v3 용 API를 검색하여 자격 증명을 얻을 수 있습니다. 문제가 발생하면 위에서 언급 한 동영상을 참조하십시오.


그런 다음 /src 디렉토리 안에 새 폴더와 파일을 만들 수 있습니다.

mkdir src/api
touch src/api/youtube.js

새로 생성 된 파일 내에서 axios를 가져 와서 axios 인스턴스를 시작할 수 있습니다.

import axios from 'axios';

export default axios.create({
    baseURL: 'https://www.googleapis.com/youtube/v3',
});

이제 YouTube 인스턴스를 App.js로 가져올 수 있습니다


'./api/youtube'에서 가져 오기 YouTube를 추가하기 만하면 됩니다. App.js 파일 상단에 있습니다.


우리는 물건을 가져 오는 동안 전체 응용 프로그램에 필요한 모든 구성 요소를 가져옵니다. 그러나 먼저, 그것들을 만들어야 합니다 :

mkdir src/components

디렉토리를 만듭니다

cd src/components

해당 디렉토리로 이동

touch VideoDetail.js VideoList.js SearchBar.js index.js

필요한 모든 파일을 작성합니다

cd ../../

루트 디렉토리로 다시 이동


클래스 기반 및 기능 구성 요소 


React는 두 가지 유형의 구성 요소를 제공합니다. 기능 및 클래스 기반 구성 요소라고 합니다. 바로 아래에는 두 가지 장점과 단점이 있으며 각각을 사용할 때의 제안이 있습니다. 


기능적 컴포넌트는 props를 인수로 받아들이고 일부 JSX를 리턴하는 일반 순수 JavaScript 함수입니다.

const Hello = ({name}) => <div>Hello {name}</div>

다음은 클래스 기반 구성 요소와 동일한 구성 요소입니다.

class Hello extends Component{
   render(){
      return <div>Hello {this.props.name}</div>
   }
}

기능적 구성 요소는 벙어리 구성 요소라고도 합니다. 상태를 관리하거나, 일부 로직을 실행하거나, 라이프 사이클 방법에 대해 걱정할 필요가 없습니다. 그들은 단순히 프리젠 테이션이며 JSX를 반환합니다.


반면에 클래스 기반 구성 요소는 상태 개체, 수명주기 메서드 등에 액세스 할 수 있습니다. 더 복잡해져서 더 많은 일을 할 수 있습니다.


경험적으로 볼 때마다 기능 구성 요소를 사용하는 것이 좋습니다. 모든 구성 요소는 바보 같은 것으로 시작할 수 있습니다. 그러나 상태가 필요할 수 있음을 알게 되면 즉시 스마트 클래스 기반 구성 요소로 변환하십시오.


우리의 응용 프로그램에서 App.js와 SearchBar.js는 스마트 클래스 기반 구성 요소가 될 것입니다. 그들은 상태를 관리하고 논리를 실행할 것입니다. 다른 모든 구성 요소는 기능적 구성 요소가 됩니다.


세 가지 컴포넌트에 대한 스켈레톤을 만들어 App.js로 가져 와서 브라우저에서 시각화 할 수 있습니다.


SearchBar.js 


import React from 'react';

class SearchBar extends React.Component {
    render() {
      return (<h1>SearchBar Component</h1>)
    }
}

export default SearchBar;

VideoDetail.js 

import React from 'react';

const VideoDetail = () => {
  return (<h1>VideoDetail Component</h1>)
}

export default VideoDetail;

VideoList.js 

import React from 'react';

const VideoList = () => {
  return (<h1>VideoList Component</h1>)
}

export default VideoList;

그리고 이제 src 디렉토리 안에 있는 index.js 내부에서 컴포넌트에서 모두 내보낼 수 있습니다 :


index.js 

export { default as SearchBar } from './SearchBar';
export { default as VideoDetail } from './VideoDetail';
export { default as VideoList } from './VideoList';

이 내보내기 방법을 사용하면 App.js에서 모든 구성 요소를 한 줄로 가져올 수 있습니다.

import { SearchBar, VideoList, VideoDetail } from './components';

그리고 지금 우리는 우리의 의견이 전에 있던 곳에 그것들을 둘 수 있습니다 :

import React from 'react';

import { Grid } from '@material-ui/core';

import { SearchBar, VideoList, VideoDetail } from './components';

import youtube from './api/youtube';

class App extends React.Component {
	render() {
      return (
        <Grid style={{ justifyContent: 'center' }} container spacing={10}>
          <Grid item xs={11}>
            <Grid container spacing={10}>
              <Grid item xs={12}>
              	<SearchBar />
              </Grid>
              <Grid item xs={8}>
                <VideoDetail />
              </Grid>
              <Grid item xs={4}>
                <VideoList />
              </Grid>
            </Grid>
          </Grid>
        </Grid>
      );
    }
}

export default App;

모든 것이 올바르게 완료되면 브라우저에 표시되어야 합니다.

Screen-Shot-2019-08-04-at-08.47.29.png 

SearchBar 구성 요소 설정 


SearchBar는 스마트 구성 요소 중 하나이며 여기에서 입력 필드의 상태를 관리해야 합니다. 설정해 봅시다.


먼저 MaterialUI에서 일부 요소를 가져와야 합니다.

import { Paper, TextField } from '@material-ui/core';

둘째, 상태를 초기화하고 handleChange와 handleSubmit이라는 두 가지 메소드를 설정합니다.

state = { searchTerm: '' };

	handleChange = event => this.setState({ searchTerm: event.target.value });

    handleSubmit = (event) => {
      const { searchTerm } = this.state;
      const { onFormSubmit } = this.props;

      onFormSubmit(searchTerm);

      event.preventDefault();
    }

this.props에서 온 onFormSubmit 함수는 사용할 수 없을 때 까지입니다. App.js 파일에서 만들 것입니다.


이제 검색 창 모양을 만들고 다음 방법을 사용합니다.

render() {
      const { searchTerm } = this.state;

      return (
        <Paper elevation={6} style={{ padding: '25px' }}>
          <form onSubmit={this.handleSubmit}>
            <TextField fullWidth label="Search..." value={searchTerm} onChange={this.handleChange} />
          </form>
        </Paper>
      );
    }

양식 요소의 onSubmit 리스너에서 handleSubmit을 사용하고 있습니다. TextField 요소의 onChange 리스너에서 변경을 처리하십시오


이것은 전체 구성 요소의 코드입니다.

import React from 'react';

import { Paper, TextField } from '@material-ui/core';

class SearchBar extends React.Component {
    state = { searchTerm: '' };

    handleChange = event => this.setState({ searchTerm: event.target.value });

    handleSubmit = (event) => {
      const { searchTerm } = this.state;
      const { onFormSubmit } = this.props;

      onFormSubmit(searchTerm);

      event.preventDefault();
    }

    render() {
      const { searchTerm } = this.state;

      return (
        <Paper elevation={6} style={{ padding: '25px' }}>
          <form onSubmit={this.handleSubmit}>
            <TextField fullWidth label="Search..." value={searchTerm} onChange={this.handleChange} />
          </form>
        </Paper>
      );
    }
}

export default SearchBar;

이제 App.js 내부에서 초기 상태 및 handleSubmit 함수를 만들 수 있습니다. 페칭 로직이 발생하는 곳입니다.

state = {
    videos: [],
    selectedVideo: null,
  }
  
handleSubmit = async (searchTerm) => {
    const response = await youtube.get('search', {
      params: {
        part: 'snippet',
        maxResults: 5,
        key: '[YOUR_API_KEY]',
        q: searchTerm,
      }
    });

    this.setState({ videos: response.data.items, selectedVideo: response.data.items[0] });
  }

여기서 우리는 비동기 함수를 사용할 수 있습니다.이 함수는 이전에 생성 한 axios 인스턴스를 사용하고 get 요청을 합니다. API가 올바르게 작동하려면 일부 매개 변수를 전달해야 합니다. 스니펫은 각 동영상에 대한 추가 데이터가 필요하다는 것을 의미합니다. 최대 결과는 설명이 필요하며 키는 직접 붙여 넣어야 하는 API 키입니다 (링크를 기사 상단에 위치). 그리고 q (평균 쿼리)는 SearchBar 구성 요소에서 전달하는 searchTerm과 같습니다. 그런 다음 비디오 상태를 새로 가져온 데이터로 설정하고 selectedVideo 상태를 비디오 배열의 첫 번째 항목으로 설정합니다.


API에서 데이터를 가져 오는 데 문제가있는 경우 이 비디오를 참조하십시오.


가져 오는 데이터의 모양은 다음과 같습니다.

Screen-Shot-2019-08-04-at-09.07.27.png 


필요한 모든 것은 response.data.items 안에 있으며 각 항목 아래에는 channelId, title, description 등에 대한 데이터가 포함 된 스니펫이 있습니다.


렌더 방법의 상단에서 상태를 구조화하십시오.

const { selectedVideo, videos } = this.state;

이제 해당 handleSubmit을 onFormSubmit으로 SearchBar에 전달할 수 있습니다.

<SearchBar onFormSubmit={this.handleSubmit} />

우리가 여기 있는 동안 비디오를 VideoDetail로 전달하십시오.

<VideoDetail video={selectedVideo}/>

VideoDetail 구성 요소 설정 


마지막으로 데이터를 볼 수 있습니다!


먼저 MaterialUI에서 일부 요소를 가져 와야 합니다.

import { Paper, TextField } from '@material-ui/core';

두 번째로, 구조화를 사용하여 전달한 비디오 속성을 다음과 같이 얻을 수 있습니다.

const VideoDetail = ({ video }) => {
  return (
  	...
  )
}

비디오 세부 사항 내에서 먼저 비디오가 있는지 확인하고 없는 경우 로드 메시지를 표시합니다.

if(!video) return <div>Loading...</div>

이제 비디오의 src를 구성 할 수 있습니다. 우리는 API 호출에서 오는 videoId를 사용하여 그렇게 할 것입니다.

const videoSrc = `https://www.youtube.com/embed/${video.id.videoId}`

마지막으로, 우리의 반환 진술은 다음과 같습니다.

return (
    <React.Fragment>
      <Paper elevation={6} style={{ height: '70%' }}>
        <iframe frameBorder="0" height="100%" width="100%" title="Video Player" src={videoSrc}/>
      </Paper>
      <Paper elevation={6} style={{ padding: '15px' }}>
        <Typography variant="h4">{video.snippet.title} - {video.snippet.channelTitle}</Typography>
        <Typography variant="subtitle1">{video.snippet.channelTitle}</Typography>
        <Typography variant="subtitle2">{video.snippet.description}</Typography>
      </Paper>
    </React.Fragment>
  )

여기서는 모든 의미 있는 데이터를 사용하여 비디오에 대한 일부 정보를 표시하고 iframe을 사용하여 브라우저에서 재생합니다.


다음은 완성 된 VideoDetail.js의 코드입니다.

import React from 'react';

import { Paper, Typography } from '@material-ui/core';

const VideoDetail = ({ video }) => {
  if(!video) return <div>Loading...</div>

  const videoSrc = `https://www.youtube.com/embed/${video.id.videoId}`

  return (
    <React.Fragment>
      <Paper elevation={6} style={{ height: '70%' }}>
        <iframe frameBorder="0" height="100%" width="100%" title="Video Player" src={videoSrc}/>
      </Paper>
      <Paper elevation={6} style={{ padding: '15px' }}>
        <Typography variant="h4">{video.snippet.title} - {video.snippet.channelTitle}</Typography>
        <Typography variant="subtitle1">{video.snippet.channelTitle}</Typography>
        <Typography variant="subtitle2">{video.snippet.description}</Typography>
      </Paper>
    </React.Fragment>
  )
}

export default VideoDetail;

VideoList 및 VideoItem 구성 요소 설정 


먼저 App.js의 비디오를 VideoList로 전달하십시오.

<VideoList videos={videos} />

VideoList 컴포넌트는 우리가 만들 다른 컴포넌트를 단순히 루프하는 상위 컴포넌트입니다. 먼저 VideoItem.js를 만들어 봅시다 :

touch src/components/VideoItem.js

VideoItem.js: 

import React from 'react';

import { Grid, Paper, Typography } from '@material-ui/core';

const VideoItem = ({ video, onVideoSelect }) => {
  return (
    <Grid item xs={12}>
      <Paper style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
        <img style={{ marginRight: '20px' }} alt="thumbnail" src={video.snippet.thumbnails.medium.url}/>
        <Typography variant="subtitle1"><b>{video.snippet.title}</b></Typography>
      </Paper>
    </Grid>
  )
}

export default VideoItem;

여기서는 의미 있는 데이터를 추가로 가져 오며 이 구성 요소를 사용하여 측면에 5 개의 서로 다른 비디오를 만듭니다.


VideoList.js: 

import React from 'react';

import { Grid } from '@material-ui/core';

import VideoItem from './VideoItem';

const VideoList = ({ videos }) => {
  const listOfVideos = videos.map((video, id) => <VideoItem key={id} video={video} />)

  return (
    <Grid container spacing={10}>
      {listOfVideos}
    </Grid>
  )
}

export default VideoList;

데이터가 지금 올바르게 표시되지만 비디오 항목을 클릭 할 수 없습니다. App.js 구성 요소에있는 selectedVideo 상태를 기억하십니까? 우리는 지금 그것을 사용할 것입니다.


App.js 내부에서 다른 메소드를 작성하십시오.

onVideoSelect = (video) => {
   this.setState({ selectedVideo: video });
 }

이제 해당 메소드를 VideoList 컴포넌트에 전달하십시오.

<VideoList videos={videos} onVideoSelect={this.onVideoSelect} />

이제 VideoList 구성 요소 내에서 다음과 같이 해당 메소드를 한 번 더 전달하십시오.

import React from 'react';

import { Grid } from '@material-ui/core';

import VideoItem from './VideoItem';

const VideoList = ({ videos, onVideoSelect }) => {
  const listOfVideos = videos.map((video, id) => <VideoItem onVideoSelect={onVideoSelect} key={id} video={video} />)

  return (
    <Grid container spacing={10}>
      {listOfVideos}
    </Grid>
  )
}

export default VideoList;

props으로 가져 와서 VideoItem 구성 요소에 전달합니다. 이제 여기에서 사용할 수 있습니다. Paper 구성 요소의 onClick 리스너를 살펴보십시오.

import React from 'react';

import { Grid, Paper, Typography } from '@material-ui/core';

const VideoItem = ({ video, onVideoSelect }) => {
  return (
    <Grid item xs={12}>
      <Paper style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={() => onVideoSelect(video)}>
        <img style={{ marginRight: '20px' }} alt="thumbnail" src={video.snippet.thumbnails.medium.url}/>
        <Typography variant="subtitle1"><b>{video.snippet.title}</b></Typography>
      </Paper>
    </Grid>
  )
}

export default VideoItem;