[React] React Project 2 - Movie App

React Project 2 - Movie App

React를 이용하여 API에서 받아온 영화 데이터를 보여주고,

React Router를 이용하여 다른 페이지로 이동할 수 있는 웹사이트를 만들어 보았다.

Github Pages로 로컬에서 개발한 React Code를 Deploy하는 작업도 수행하였다.

결과 화면은 다음과 같다.

react1

개발환경 세팅

create-react-app [project_name]

cd [project_name]

yarn start

필요한 컴포넌트

  1. Home : 모든 영화 리스트를 보여주는 컴포넌트
  2. Movie : 각 영화 정보를 관리하는 컴포넌트
  3. Detail : 영화 아이템 클릭 시 상세 정보를 보여주는 컴포넌트
  4. About : About 페이지를 관리하는 컴포넌트
  5. Navigation : 여러 페이지로 이동하도록 하는 상단 Navigation 컴포넌트

컴포넌트 전체 구조

react2

React Router 사용 (App.js)

url을 확인하며, 명령에 따라 다른 컴포넌트를 불러온다.

  1. npm install react-router-dom
  2. import {HashRouter, Route} from 'react-router-dom';
  3. HashRouter : # 다음으로 link가 있다.
  4. 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)

(axios는 HTTP 클라이언트 라이브러리로써, 비동기 방식으로 HTTP 데이터 요청을 실행한다.)

  1. Axios 설치 : npm install axios
  2. import axios from 'axios';
  3. 컴포넌트가 생성될 때 (DOM에 Mount될 때) 호출되는 componentDidMount()함수에서 axios.get()을 수행한다.
  4. 이 때, 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를 이용한다.

  1. import PropTypes from 'prop-types';
  2. 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

  1. Parent Component에서 Link tostate property를 이용하여, Link를 클릭해 Child Component 이동할 때 Object를 전송한다.
  2. Child Component에서는, 컴포넌트가 생성될 때 호출되는 componentDidMount()에서 state에 값이 들어왔는지 확인한다. (location.state)
  3. state에 아무 값도 들어오지 않은 경우 /로 돌아가도록 설정한다. => Redirect

    즉, Link를 클릭하여 이동했을 경우에만 state값이 전송되며 해당 페이지로 이동할 수 있다.

    componentDidMount() {
            const {location, history} = this.props;
            if(location.state === undefined) {
                history.push("/");
            }
        }

Github Pages에 Deploy하기

  1. Github Repository에 React Code를 Push한다.
  2. git remote -v
  3. npm i gh-pages : 웹사이트를 github page 도메인에 띄워주기 위한 gh-pages 라이브러리를 설치한다.
  4. package.json 파일의 하단에 다음과 같이 추가

    "homepage": "https://[username].github.io/[projectname]"

    "scripts": {} 안에 다음과 같이 추가

    "deploy": "gh-pages "

  5. npm run build ⇒ Project에 build 폴더가 생성된 것을 확인 (이 build 폴더를 gh pages에 업로드하는 것이다.)
  6. 이전에 작성한 script 내용을 다음과 같이 변경

    "deploy": "gh-pages -d build",

    "predeploy": "npm run build"

    ⇒ npm이 predeploy를 호출한 다음 deploy를 호출하도록 설정.

  7. npm run deploy ⇒ 완료!

Reference Link

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 참고