Knowledge/React.js

[React.js] react-movie-app

winCow 2021. 6. 29. 18:13

1. 개요

react를 이용해 영화 정보를 표시해 주는 SPA를 만들었다. create-react-app과 react-router 라이브러리를 이용해 만들었으며, 아래 컴포넌트와 스타일을 적용했다.

 

 

2. index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('potato')
);

create-react-app의 index.js는, index.html과 App.js를 연결해 주는 역할을 한다. 따라서 App.js의 이름이 바뀐다면 index.js에서 render되는 App 컴포넌트의 이름도 바뀌어야 하고, index.html의 body 태그 안의 div 태그에 적용된 id를 바꾼다면 index.js에서 getElementById를 통해 가져오는 id도 바뀌어야 한다.

 

 

3. App.js

import React from "react";
import { HashRouter, Route } from "react-router-dom";
import Home from "./Routes/Home";
import About from "./Routes/About";
import Detail from "./Routes/Detail";
import Navigation from "./Components/Navigation";
import "./App.css";

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;

App.js는 다른 컴포넌트들을 렌더링할 root가 되는 부분이다. 여기서는 함수형 컴포넌트를 작성했으며, 네비게이션을 만들어주는 라이브러리인 react-router-dom을 이용했다. Route는 SPA(Single Page Application)에서 각각의 주소에 대해 다른 뷰를 보여주기 위한 컴포넌트를 말하는데, props로 path와 component를 설정하면, 해당 path와 component를 연결해 준다. HashRouter와 BrowserRouter는 라이브러리에서 제공하는 라우터의 종류이다. exact={true}를 설정하면 해당 경로와 정확히 일치하는 컴포넌트를 실행하지만, 이를 설정하지 않는다면 해당 경로가 포함된 모든 컴포넌트를 실행하게 되므로, 보통 경로를 공유하는 루트 컴포넌트에는 exact={true}를 설정하여 중복되어 실행되지 않도록 한다.

react-router-dom을 이용해, Home, About, Detail 세 개의 메인 컴포넌트로 이루어진 페이지를 만들었다.

 

 

4. Home.js

import React, { Component } from "react";
import axios from "axios";
import Movie from "../Components/Movie";
import "./Home.css";

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"
    );
    this.setState({ movies, isLoading: false });
  };

  componentDidMount() {
    this.getMovies();
  }
  render() {
    const { isLoading, movies } = this.state;
    return (
      <section className="container">
        {isLoading ? (
          <div className="loader">
            <span className="loader__text">Loading...</span>
          </div>
        ) : (
          <div className="movies">
            {movies.map((movie) => (
              <Movie
                key={movie.id}
                id={movie.id}
                year={movie.year}
                title={movie.title}
                summary={movie.summary}
                poster={movie.medium_cover_image}
                genres={movie.genres}
              />
            ))}
          </div>
        )}
      </section>
    );
  }
}

export default Home;

Home 컴포넌트는 루트 페이지로, 영화 데이터를 받아오고 각 자식 컴포넌트에 props를 전달한다. 가장 먼저 axios를 통해 데이터를 받아온다. axios는 JavaScript의 built-in 함수인 fetch와 같은 역할을 한다. 즉, 데이터를 받아올 때 사용하는 Promise API를 활용한 비동기 통신 라이브러리이다. 이를 사용하기 위해 axios를 import하고, getMovies 함수를 만들었다. 데이터를 받아오는데 시간이 걸릴 수 있으므로, async와 await를 이용해, await이 적용된 axios.get() 함수가 끝날 때까지 다음 작업을 대기 상태로 유지한다. axios.get()에 대한 응답은 JSON 형태로 넘어오기 때문에, 비구조화 할당을 사용해 각각 data와 movies에 할당했다. await이 적용된 axios.get()함수를 통해 데이터를 모두 받아온 후에는, this.setState()를 통해 Homt 컴포넌트의 state로, movies에는 movies를, isLoaing에는 false를 전달한다.

 

다음으로, render 함수를 작성한다. 가장 먼저 비구조화 할당을 통해 isLoading, movies를 this.state로 설정하고, 이후 각 컴포넌트를 return한다. isLoading이 true면, 현재 로딩 중인 상태이므로, Loading...이라는 글자를 반환한다. 기본 true로 설정되어 있으므로, 데이터를 모두 받아오기 전까지는 해당 글자가 반환될 것이다.

컴포넌트가 한 번 render된 이후, componentDidMount 메소드를 통해, 앱이 마운트된 이후 getMovies 메소드가 실행되도록 한다. getMovies 메소드가 실행되면, 해당 데이터를 받아올 것이고, 이를 받아온 뒤 loading이 false로 업데이트되면, 즉 state의 값이 변하면, render가 다시 업데이트된다. moives 배열에 저장된 데이터를 map API를 통해 각각의 데이터를 Movie 컴포넌트로 변환한다. 각 데이터에 key, id, year, title, summary, poster, genres를 props로 전달한다. 

 

 

5. Moive.js

import React from "react";
import { Link } from "react-router-dom";
import PropTypes from "prop-types";
import "./Movie.css";

const Movie = ({ id, year, title, summary, poster, genres }) => {
  return (
    <Link
      to={{
        pathname: `/movie/${id}`,
        state: {
          year,
          title,
          summary,
          poster,
          genres,
        },
      }}
    >
      <div className="movie">
        <img src={poster} alt={title} title={title} />
        <div className="movie__data">
          <h3 className="movie__title">{title}</h3>
          <h5 className="movie__year">{year}</h5>
          <ul className="movie__genres">
            {genres.map(
              (
                genre,
                index
              ) => (
                <li key={index} className="genres__genres">
                  {genre}
                </li>
              )
            )}
          </ul>
          <p className="movie__summary">{summary.slice(0, 140)}...</p>
        </div>
      </div>
    </Link>
  );
};

Movie.propTypes = {
  id: PropTypes.number.isRequired,
  year: PropTypes.number.isRequired,
  title: PropTypes.string.isRequired,
  summary: PropTypes.string.isRequired,
  poster: PropTypes.string.isRequired,
  genres: PropTypes.arrayOf(PropTypes.string).isRequired,
};

export default Movie;

Movie 컴포넌트는 각각의 영화 데이터를 표시해 줄 것이다. react-router-dom을 사용할 때는 Link 컴포넌트를 이용해야 하는데, Link 컴포넌트는 페이지를 이동할 때, 즉 다른 라우트로 이동할 때, 페이지를 새로고침하지 않고 원하는 라우트로 화면을 전환시킬 수 있도록 한다. Home에서 전달받은 props를 매개변수로 가지고, state의 year, title, summary, poster, genres 값을 각각 자기 자신으로 설정한다. (year: year, title: title, summary: summary, poster: poster, genres: genres) 또, Link 컴포넌트의 pathname을 각 movie의 id로 받는다. 이후 JSX에 따라, Link의 자식으로 전체 데이터를 담을 movie div를 만들고, 그 자식으로 포스터가 담길 img, movie__data가 담길 div를 다시 만든다. 다시 movie__data의 자식으로 title h3, year h5, genres ul, 스토리 요약이 들어갈 p를 만든다. ul의 자식 li로는, map API를 이용해 해당되는 모든 장르를 선택하여 li로 반환하게 한다. p에는 summary를 0에서 140까지만 슬라이싱하고 나머지는 ...으로 생략된 것처럼 보이도록 한다.

마지막으로 propTypes를 이용해 들어오는 데이터 형식을 체크한다.

 

 

6. About.js

import React from "react";
import "./About.css";

const About = (props) => {
  console.log(props);
  return <span>About this page: I build it because I love movies.</span>;
};

export default About;

About 컴포넌트는 연결되면 간단한 문구를 표시해 준다.

참고로 Routes를 통해 전달되는 props에는 history, location, match가 있는데, history 객체는 push, replace 메소드를 통해 다른 경로로 이동하거나 앞, 뒤 페이지로 전환할 수 있다. location 객체는 현재 경로에 대한 정보를 지니고 있고, URL 형식 정보를 가지고 있다. match 객체에는 어떤 라우트에 매칭되었는지에 대한 정보를 가지고 있다.

 

 

7. Detail.js

import React, { Component } from "react";

class Detail extends Component {
  componentDidMount() {
    const { location, history } = this.props;
    if (location.state === undefined) {
      history.push("/");
    }
  }
  render() {
    const { location } = this.props;
    if (location.state) {
      return <span>{location.state.title}</span>;
    } else {
      return null;
    }
  }
}

export default Detail;

 

 

Detail.js에는 각 영화의 세부적인 설명이 담기게 된다. 먼저, render() 함수가 실행되면 routes로 전달받은 props(history, location, match, staticContext)가 location에 담기게 된다. location.state가 true일 때, 즉 존재할 때 location.state.title을 반환하도록 했다. 존재하지 않는다면 null을 반환하여 아무것도 표시되지 않게 한다. 콘솔로 확인해 보면, location.state에는 Movie.js의 Link 컴포넌트에서 state로 설정했던 영화의 정보가 저장되어 있다.

render가 끝나면 componentDidMount가 실행되는데, location과 history에 전달받은 props를 할당하고, location.state가 undefined 상태라면 history에 push 메소드를 이용하여 루트(/) 경로로 이동시킨다.