[React] React Project 2 - Movie App
2020. 4.13
React Project 2 - Movie App
React를 이용하여 API에서 받아온 영화 데이터를 보여주고,
React Router를 이용하여 다른 페이지로 이동할 수 있는 웹사이트를 만들어 보았다.
Github Pages로 로컬에서 개발한 React Code를 Deploy하는 작업도 수행하였다.
결과 화면은 다음과 같다.

개발환경 세팅
create-react-app [project_name]
cd [project_name]
yarn start
필요한 컴포넌트
- Home : 모든 영화 리스트를 보여주는 컴포넌트
- Movie : 각 영화 정보를 관리하는 컴포넌트
- Detail : 영화 아이템 클릭 시 상세 정보를 보여주는 컴포넌트
- About : About 페이지를 관리하는 컴포넌트
- Navigation : 여러 페이지로 이동하도록 하는 상단 Navigation 컴포넌트
컴포넌트 전체 구조

React Router 사용 (App.js)
url을 확인하며, 명령에 따라 다른 컴포넌트를 불러온다.
npm install react-router-domimport {HashRouter, Route} from 'react-router-dom';- HashRouter : # 다음으로 link가 있다.
- BrowserRouter : # 가 없지만 Github Pages에 deploy하기 까다롭다.
반드시 Router 안에 Link 또는 Routes를 구현해야 한다.
<HashRouter>
<Route path="/" exact={true} component={Home} />
{/* exact={true} : url이 path와 정확히 일치할 때만 component를 렌더링함 */}
<Route path="/about" component={About} />
<Route path="/movie/:id" component={Detail} />
{/* path로 이동하면 component를 수행한다.
<Route>의 props =>
path : 이동할 스크린, component : 수행할 작업 */}
</HashRouter>
<Link>vs<a>(Navigation.js)
<a href="/"> : <a>를 사용하면 클릭 시 매번 페이지를 새로고침한다. => <Link>를 사용하여 해결
<Link
to={{
pathname: "/about",
state: {
fromNavigation: true
}
// About 페이지로 object를 전송
}}
>
About
</Link>⇒ pathname 페이지로 state object를 전송한다.
Movie API의 JSON data Fetching (Home.js)
- 사용한 API : https://yts-proxy.now.sh/list_movies.json
- Axios를 이용하여 url의 data를 받아오는 작업을 수행하였다.
(axios는 HTTP 클라이언트 라이브러리로써, 비동기 방식으로 HTTP 데이터 요청을 실행한다.)
- Axios 설치 :
npm install axios import axios from 'axios';- 컴포넌트가 생성될 때 (DOM에 Mount될 때) 호출되는
componentDidMount()함수에서axios.get()을 수행한다. - 이 때,
axios.get()을 통해 data를 받아오는 데에 시간이 걸리므로 비동기로 처리해야 한다. (async await 이용)
비동기 처리 : async await (Home.js)
-
async() : 함수가 실행되는 데 걸리는 시간을 비동기로 처리한다.
⇒ 끝날 때까지 기다렸다가 완료되면 실행.
- await {something} : something이 실행 완료될 때까지 기다린다.
// 방법 1
getMovies = async () => {
const movies = await axios.get("https://yts-proxy.now.sh/list_movies.json");
/* axios.get() : Network에서 데이터를 받아옴
=> movies 객체에 저장 */
console.log(movies.data.data.movies);
this.setState({movies.data.data.movies, isLoading:false})
}// 방법 2 => ES6을 이용하여 간결하게 표현.
getMovies = async () => {
const {
data: {
data: { movies }
}
} = await axios.get("https://yts-proxy.now.sh/list_movies.json");
console.log(movies);
this.setState({ movies, isLoading: false });
/* => {movies: movies}로 표현하지 않고 단축해서 사용하는 것도 가능함. JavaScript가 movies를 자동적으로 axios의 movies로 인식한다.
axios.get()으로 데이터가 fetch되면, state의 isLoading을 false로 변경한다. */
};Class Component vs Function Component (Movie.js)
state를 필요로 하지 않는 Component, 즉 객체의 상태 변경이 필요 없으며 부모로부터 받은 props를 그대로 사용하는 Component는 Class Component로 작성할 필요가 없다 ⇒ Function Component로 작성한다.
PropTypes (Movie.js)
객체의 type과 필요 여부를 미리 설정하고, 설정한 값과 다른 값이 들어올 경우 에러를 표시하기 위해 PropTypes를 이용한다.
import PropTypes from 'prop-types';-
ex)
[컴포넌트].propTypes = { id: PropTypes.number.isRequired, ... }
Each child in a list should have a unique "key" prop.
map()을 통해 List를 객체로 표현할 때 반드시 key값을 지정해 주어야 한다.
⇒ map의 각 요소들은 unique한 key값을 가지고 있어야 한다.
(map에서 기본적으로 제공하는 index 사용 가능.)
ex)
genres.map((genre, index) => (
<li key={index} className="genres_genre">
{genre}
</li>
));React의 location, history를 이용한 Redirection
- Parent Component에서
Link to의stateproperty를 이용하여, Link를 클릭해 Child Component 이동할 때 Object를 전송한다. - Child Component에서는, 컴포넌트가 생성될 때 호출되는
componentDidMount()에서 state에 값이 들어왔는지 확인한다. (location.state) -
state에 아무 값도 들어오지 않은 경우 /로 돌아가도록 설정한다. => Redirect
즉, Link를 클릭하여 이동했을 경우에만 state값이 전송되며 해당 페이지로 이동할 수 있다.
componentDidMount() { const {location, history} = this.props; if(location.state === undefined) { history.push("/"); } }
Github Pages에 Deploy하기
- Github Repository에 React Code를 Push한다.
git remote -vnpm i gh-pages: 웹사이트를 github page 도메인에 띄워주기 위한 gh-pages 라이브러리를 설치한다.-
package.json 파일의 하단에 다음과 같이 추가
"homepage": "https://[username].github.io/[projectname]""scripts": {} 안에 다음과 같이 추가
"deploy": "gh-pages " npm run build⇒ Project에 build 폴더가 생성된 것을 확인 (이 build 폴더를 gh pages에 업로드하는 것이다.)-
이전에 작성한 script 내용을 다음과 같이 변경
"deploy": "gh-pages -d build","predeploy": "npm run build"⇒ npm이 predeploy를 호출한 다음 deploy를 호출하도록 설정.
npm run deploy⇒ 완료!
React Redux의 필요성..
- Home 컴포넌트가 렌더링될 때 state는 초기화된다.
- componentDidMount()에서 getMovies()를 호출하므로, Router를 통해 Home Link로 들어오면 리렌더링되고, axios를 다시 불러와서 새로 로딩하는 불필요한 작업이 반복된다.
- 이를 해결하기 위해 Redux를 사용한다.
- Redux는 state를 스크린 밖에 저장해두고 사용하는 방법이다.
- Redux를 사용하면 외부에 저장된 state를 불러오기 때문에, Home이 리렌더링되더라도 데이터를 다시 로드하지 않는다.
전체 Code
App.js
import React from "react";
import Home from "./routes/Home";
import About from "./routes/About";
import Detail from "./routes/Detail";
import Navigation from "./components/Navigation";
import { HashRouter, Route } from "react-router-dom";
// npm install react-router-dom
function App() {
return (
<HashRouter>
<Navigation />
<Route path="/" exact={true} component={Home} />
<Route path="/about" component={About} />
<Route path="/movie/:id" component={Detail} />
</HashRouter>
);
}
export default App;Navigation.js
import React from "react";
import { Link } from "react-router-dom";
function Navigation() {
return (
<div>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</div>
);
}
export default Navigation;Home.js
import React, { Component } from "react";
import Movie from "../components/Movie";
import axios from "axios";
// npm install axios
class Home extends Component {
state = {
isLoading: true,
movies: []
};
getMovies = async () => {
const {
data: {
data: { movies }
}
} = await axios.get(
"https://yts-proxy.now.sh/list_movies.json?sort_by=rating"
);
console.log(movies);
this.setState({ movies: movies, isLoading: false });
};
componentDidMount() {
this.getMovies();
}
render() {
const { isLoading, movies } = this.state;
const movieList = movies.map(movie => (
<Movie
key={movie.id}
id={movie.id}
title={movie.title}
year={movie.year}
genres={movie.genres}
summary={movie.summary}
poster={movie.medium_cover_image}
rating={movie.rating}
/>
));
return (
<section className="container">
{isLoading ? (
<div className="loading">Loading...</div>
) : (
<div className="movies">{movieList}</div>
)}
</section>
);
}
}
export default Home;Movie.js
import React from "react";
import { Link } from "react-router-dom";
import PropTypes from "prop-types";
function Movie({ id, title, year, genres, summary, poster, rating }) {
return (
<Link
to={{
pathname: `/movie/:${id}`,
state: {
title,
year,
genres,
summary,
poster,
rating
}
}}
>
<div className="movie_container">
<img src={poster} alt={title} title={title} />
<h1 className="movie_title">{title}</h1>
<h3 className="movie_year">{year}</h3>
<div className="movie_rating">Rating : {rating}/10.0</div>
<ul className="movie_genres">
{genres.map((genre, index) => (
<li key={index} className="genres_genre">
{genre}
</li>
))}
</ul>
</div>
</Link>
);
}
Movie.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
genre: PropTypes.arrayOf(PropTypes.string),
rating: PropTypes.number,
summary: PropTypes.string,
poster: PropTypes.string.isRequired
};
export default Movie;Detail.js
import React from "react";
class Detail extends React.Component {
componentDidMount() {
const { location, history } = this.props;
if (location.state === undefined) {
history.push("/");
// Redirection
}
}
render() {
const { location } = this.props;
if (location.state) {
return (
<div>
<h1 className="movie_title">{location.state.title}</h1>
<img
src={location.state.poster}
alt={location.state.title}
title={location.state.title}
/>
<h3 className="movie_year">{location.state.year}</h3>
<ul className="movie_genres">
{location.state.genres.map((genre, index) => (
<li key={index} className="genres_genre">
{genre}
</li>
))}
</ul>
<div className="movie_rating">
Rating : {location.state.rating}/10.0
</div>
<div className="movie_summary">
{location.state.summary}
</div>
</div>
);
} else {
return null;
}
}
}
export default Detail;About.js ⇒ 자유롭게 작성.
React Fundamentals 2019 - Nomad Coder 참고
