[React] ReactJS로 영화 웹 서비스 만들기 (5)

Lpla

·

2021. 2. 13. 22:11

반응형

ReactJS로 영화 웹 서비스 만들기 (1)

ReactJS로 영화 웹 서비스 만들기 (2)

ReactJS로 영화 웹 서비스 만들기 (3)

ReactJS로 영화 웹 서비스 만들기 (4)

ReactJS로 영화 웹 서비스 만들기 (6)

ReactJS로 영화 웹 서비스 만들기 (7)

 


#4 Making the Movie App

이번 포스팅이 영화 웹 서비스 만들기의 핵심이 되는 파트이다.

 

fetch

  • 자바스크립트는 필요할 때 서버에 네트워크 요청을 보내고 새로운 정보를 받아올 수 있다.
  • 일반적으로 fetch()를 사용하지만 여기선 Axios를 사용한다.
  • Axios는 npm install axios로 설치한다.

 

YTS API

  • 영화 데이터는 YTS에서 만든 API를 사용한다.
  • 구글에 YTS를 검색하여 공식 사이트로 이동한다.

  • 사이트 하단에 API를 클릭하여 List Moives 탭으로 간다.

  • 크롬 확장프로그램으로 JSONView 를 설치하면 보기 편하게 바꿔주니 설치하면 좋다.
 

JSONView

Validate and view JSON documents

chrome.google.com

  • 이제 우리는 영화 정보까지 얻었다. 하지만 한 가지 문제가 있다. YTS는 불법 토렌트 사이트다. 따라서 도메인을 수시로 바꾸기 때문에 API URL도 계속 바뀐다. (강의에서 불법 사이트를 이용한다니 신기하다.)
  • 그래서 니콜라스(강사)는 YTS 사이트를 추적하는 코드를 만들었고 그 주소는 이렇다.

https://yts-proxy.now.sh/list_movies.json

  • 실제로 접속해면 위 사이트 주소와 똑같은 곳으로 이동하니 실질적으로 다른 점은 없다.
  • URL이 바뀜으로 나타나는 에러를 방지하기 위한 것일 뿐이다.

 

Import Axios

  • 이제 코드를 작성해보자. 먼저 설치한 axios를 import 한다.
import axios from "axios";
  • 그리고 componentDidMount로 json을 불러온다.
componentDidMount() {
  const movies = axios.get("https://yts-proxy.now.sh/list_movies.json");
}
  • 개발자도구로 네트워크를 확인해보면 list_movies.json이 보인다.

  • 하지만 axios는 느리다. 따라서 완료될 때까지 기다리기 위한 비동기 처리 문법 async/await 를 함께 써야 한다.
  • async : 싱크할 때까지 기다려달라. await : 무엇을 기다릴까? 로 생각하면 쉽다.

 

async function 함수명() {
  await 비동기 처리 메서드명();
}
getMovies = async() => {
  const movies = await axios.get("https://yts-proxy.now.sh/list_movies.json");
}
componentDidMount() {
  this.getMovies();
}

 

Rendering the Moives

  • 이제 console.log(movies)로 데이터를 확인해보자.

  • 우리가 필요한 데이터는 data > data > movies이다.
  • console.log(Movies.data.data.movies) 로 수정하고 결과를 확인해보자.

  • Movies.data.data.movies는 아래 코드로 바꿔 사용할 수 있다.
getMovies = async() => {
  const {data : {data : {movies}}} = await axios.get("https://yts-proxy.now.sh/list_movies.json");
  console.log(movies);
}
  • 지금까지 코드를 모두 작으면 아래와 같다.
import React from "react";
import axios from "axios";

class App extends React.Component {
  state = {
    isLoading: true,
    movies: [],
  };
  getMovies = async() => {
    const {data : {data : {movies}}} = await axios.get("https://yts-proxy.now.sh/list_movies.json");
    console.log(movies);
  }
  componentDidMount() {
    this.getMovies();
  }
  render() {
    const { isLoading } = this.state;
    return <div>{isLoading ? "로딩중..." : "준비완료"}</div>;
  }
}

export default App;
  • 이제 가져온 데이터를 state의 movies에 넣어야 한다.
getMovies = async() => {
  const {data : {data : {movies}}} = await axios.get("https://yts-proxy.now.sh/list_movies.json");
  console.log(this.state.movies);
  this.setState({movies : movies});
  console.log(this.state.movies);
}

// []
// (20) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
  • console.log로 setState 이후에 movies 값이 채워진 걸 확인할 수 있다.
  • setState의 처음 movies는 json에 있는 movies 데이터를 말하고 두번째 movies는 state.movies 를 말한다.
  • 이것은 this.setState({movies}); 로 짧게 만들 수 있다.
  • console.log는 지우고 이전에 만든 "로딩중..."을 "준비완료"로 바꾸도록 하자.
getMovies = async() => {
  const {data : {data : {movies}}} = await axios.get("https://yts-proxy.now.sh/list_movies.json");
  this.setState({movies, isLoading: false});
}
  • setState 안에 movies와 isLoading 2개가 들어가 있고 이렇게 해도 정상적으로 작동한다.

 

  • 이제 새로운 파일로 Movie.js 를 만든다.
  • Movie.js는 state가 필요하지 않으므로 클래스 컴포넌트를 만들지 않는다.
import React from "react"
import propTypes from "prop-types"

function Movie() {

}

Movie.propTypes = {
  
}

export default Movie;
  • Movie.propTypes는 json 파일을 참고하여 작성한다.
import React from "react"
import propTypes from "prop-types"

function Movie({id, year, title, summary, poster}) {

}

Movie.propTypes = {
  id: propTypes.number.isRequired,
  year: propTypes.number.isRequired,
  title: propTypes.string.isRequired,
  summary: propTypes.string.isRequired,
  poster: propTypes.string.isRequired,
}

export default Movie;
  • Movie() 안에 중괄호를 넣어야 한다. 나는 중괄호를 빼먹어서 30분 넘게 오류 찾아 다녔다.
  • 이제 가져온 데이터를 어떻게 html로 나타낼지 작성해야 한다.
  • 우선 간단하게 제목과 연도만 가져오겠다.
function Movie({id, year, title, summary, poster}) {
  return (
    <div>
      <h3>{title}</h3>
      <span>{year}</span>
    </div>
  );
}
  • 이제 이것을 render하기 위해 App.js 를 수정한다.
render() {
  const { isLoading } = this.state;
  return <div>{isLoading ? "로딩중..." : this.state.movies.map(movie => {console.log(movie);})}</div>;
}
  • this.state.movies는 다음으로 짧게 바꿀 수 있다.
render() {
  const { isLoading, movies } = this.state;
  return <div>{isLoading ? "로딩중..." : movies.map(movie => {console.log(movie);})}</div>;
}
  • 콘솔에 영화 데이터가 불러와진다면 문제없이 진행되고 있는 것이다.

  • 이제 movie.js 에서 생성한 movie 컴포넌트를 불러온다.
  • <Movie /> 에 Props로 key, id, year, title, summary, poster를 만들었다.
import Movie from "./Movie";

render() {
  const { isLoading, movies } = this.state;
  return (
    <div>
      {isLoading
        ? "로딩중..."
        : movies.map((movie) => {
            return (
              <Movie
                key={movie.id}
                id={movie.id}
                year={movie.year}
                title={movie.title}
                summary={movie.summary}
                poster={movie.medium_cover_image}
              />
            );
          })}
    </div>
  );
}

 

  • 원하는 내용이 출력된다.

 

HTML 수정

  • 이제 거의 다 왔다. 데이터를 불러오고 출력까지 할 수 있으니 HTML과 CSS만 잡으면 된다.
  • 먼저 로딩중 HTML은 아래로 만들었다.
<section className="container">
  {isLoading ? (
    <div className="loader">
      <span className="loader_text">"로딩중..."</span>
    </div>
  ) : (
  • 로딩이 끝난 후 나타나는 영화 목록은 아래로 만들었다.
<div className="movies_container">
  {movies.map((movie) => {
    return (
      <Movie
        key={movie.id}
        id={movie.id}
        year={movie.year}
        title={movie.title}
        summary={movie.summary}
        poster={movie.medium_cover_image}
      />
    );
  })};
</div>
  • 그리고 Movie.js 도 수정했다.
import React from "react";
import propTypes from "prop-types";

function Movie({ id, year, title, summary, poster }) {
  return (
    <div className="movie_list">
      <div className="movie_poster">
        <img src="{poster}" alt="{title}"/>
      </div>
      <h3 className="movie_title">{title}</h3>
      <h5 className="movie_year">{year}</h5>
      <p className="movie_summary">{summary}</p>
    </div>
  );
}

Movie.propTypes = {
  id: propTypes.number.isRequired,
  year: propTypes.number.isRequired,
  title: propTypes.string.isRequired,
  summary: propTypes.string.isRequired,
  poster: propTypes.string.isRequired,
};

export default Movie;
  • 리액트에서 class라는 속성이 있기 때문에 html의 class를 classname으로 쓰는 것이 좋다.

 

  • 브라우저를 새로고침하고 확인해보면 이미지가 깨져 있다.
  • 개발자도구로 확인해보니 자바스크립트를 읽지 못하고 있다. 따옴표를 없애니 이미지가 잘 보인다.

 

CSS 수정

  • 리액트 자바스크립트에서 바로 css를 추가할 수 있다.
<h3 className="movie_title" style={{color: 'red'}}>{title}</h3>

  • 중괄호 두개를 사용해서 color를 red로 바꿨다.
  • 하지만 이 방법은 정말 좋지 않다.
  • html에 style을 바로 추가하게 되면 매우 지저분해진다. 애초에 css가 분리된 이유가 그것 때문이 아닌가.
  • 이런 방식이 있다는 것만 알고 넘어가자.

 

  • 우리는 2개의 css파일이 필요하다.
  • App.js와 Movie.js 에 각각 필요한 css로 나는 App.css와 Movie.css를 만들었다.

App.js

import React from "react";
import axios from "axios";
import Movie from "./Movie";
import "./App.css";

Movie.js

import React from "react";
import propTypes from "prop-types";
import "./Movie.css";
  • 그리고 css를 import 시켰다.
  • 이제 css를 작성하면 즉시 반영되는 것을 확인할 수 있다.

Movie.css

body { background: #111; }
h3, h5, p { color: #fff; margin: 0; padding: 0; }
.movie_list { margin-bottom: 6rem; display: flex; }
.movie_poster { margin-right: 1rem; }
.movie_summary { margin-top: 2rem; }

 

Array 추가하기

  • 우리는 json에서 id, year, title, summary, poster 이렇게 데이터를 가져왔다.
  • 그리고 이것들의 타입은 number 혹은 string이다.
  • 만약 array를 똑같이 가져오면 적용될까?
  • yts.mx/api/v2/list_movies.json%EF%BB%BF 주소로 들어가서 genres를 가져와보자.

Movie.js

import React from "react";
import propTypes from "prop-types";
import "./Movie.css";

function Movie({ id, year, title, summary, poster, genres }) {
  return (
    <div className="movie_list">
      <div className="movie_poster">
        <img src={poster} alt={title} />
      </div>
      <div className="movie_contents">
        <h3 className="movie_title">{title}</h3>
        <h5 className="movie_year">{year}</h5>
        <h5 className="movie_genres">{genres}</h5>
        <p className="movie_summary">{summary}</p>
      </div>
    </div>
  );
}

Movie.propTypes = {
  id: propTypes.number.isRequired,
  year: propTypes.number.isRequired,
  title: propTypes.string.isRequired,
  summary: propTypes.string.isRequired,
  poster: propTypes.string.isRequired,
  genres: propTypes.array.isRequired
};

export default Movie;
  • Movie 함수에 genres를 추가하고 h5태그로 genres를 불러왔다.
  • 타입 확인을 위해 genres: propTypes.array.isrequired 도 추가했다.

App.js

<div className="movies_container">
  {movies.map((movie) => {
    return (
      <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>;
  • App.js에도 마찬가지로 genres를 추가했다.
  • 그리고 새로고침하면?

  • 장르까지 잘 추가 되었다!
  • 그런데 이상한 부분이 하나 있다.

  • 장르가 여러개일 경우 이것을 어떻게 조작할 수 있을까?
  • 바로 이전에 사용했던 map을 사용해야 한다.
<div className="movie_contents">
  <h3 className="movie_title">{title}</h3>
  <h5 className="movie_year">{year}</h5>
  <ul className="movie_genres">{genres.map(genres => <li className="genres_list">- {genres}</li>)}</ul>
  <p className="movie_summary">{summary}</p>
</div>

  • 원하는 결과를 얻었지만 개발자도구를 보면 key값이 없다고 경고가 뜬다.
  • 이전의 경우에는 각각의 영화마다 id값이 있어 그것을 사용했지만 장르에는 없다.
  • 다행히 map 함수에는 해당 배열의 index값을 나타내주는 파라미터가 있고 그것을 사용하면 된다.
<ul className="movie_genres">{genres.map((genres, index) => <li key={index} className="genres_list">- {genres} </li>)}</ul>
  • li에 key값을 index로 주었고 콘솔에 더 이상 에러가 보이지 않는다.
  • 이제 css로 사이트를 꾸미면 된다. 강의에서는 이 사이트 UI를 카피했다.
 

Movie Application

Posted on Jan 7, 2016

dribbble.com

  • 하지만 나는 실무에서 매일 하는 일이다 보니 굳이 이 자리에서까지 시간을 들여서 하고 싶지 않다.
  • 후에 Grid 레이아웃을 공부하고 시간나면 이 부분은 완성시켜 보겠다.
  • 레이아웃 안 하려고 했는데 다음 진도를 나가려면 해야 한다. 귀찮아도 해야지.
  • 이 파트를 따라 가며 궁금한 점이 몇 가지 있었다. 하지만 남은 수업을 모두 듣고 다음에 공부하도록 하겠다.

 

추가

  • 시간을 내서 레이아웃을 완성했다.
  • 강의에서는 <p className="movie_summary">{summary.slice(0, 180)}...<p>를 이용해서 180글자 이상은 자르고, 마지막에 ...을 붙였다.
  • 하지만 이렇게 하면 180글자 이하도 뒤에 ...이 붙어 버리기 때문에 마음에 들지 않았고 조건문을 이용해서 글자 이하일 경우에는 ...을 뺐다.

App.js

import React from "react";
import axios from "axios";
import Movie from "./Movie";
import "./App.css";

class App extends React.Component {
  state = {
    isLoading: true,
    movies: [],
  };
  getMovies = async () => {
    const {
      data: {
        data: { movies },
      },
    } = await axios.get("https://yts-proxy.now.sh/list_movies.json?sort_by=like_count");
    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">로딩중...</span>
          </div>
        ) : (
          <div className="movies_container">
            {movies.map((movie) => {
              return (
                <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 App;

Movie.js

import React from "react";
import propTypes from "prop-types";
import "./Movie.css";

function Component(e) {
  if ( e.length > 280 ) {
    return <span>{e.slice(0, 280)}...</span>;
  } else {
    return <span>{e}</span>;
  }
} 

function Movie({ id, year, title, summary, poster, genres }) {
  return (
    <div className="movie_list">
      <div className="movie_poster">
        <img src={poster} alt={title} />
      </div>
      <div className="movie_contents">
        <h3 className="movie_title">{title}</h3>
        <h5 className="movie_year">{year}</h5>
        <ul className="movie_genres">{genres.map((genres, index) => <li key={index} className="genres_list"> {genres} </li>)}</ul>
        <p className="movie_summary">{Component(summary)}</p>
      </div>
    </div>
  );
}

Movie.propTypes = {
  id: propTypes.number.isRequired,
  year: propTypes.number.isRequired,
  title: propTypes.string.isRequired,
  summary: propTypes.string.isRequired,
  poster: propTypes.string.isRequired,
  genres: propTypes.array.isRequired
};

export default Movie;

Movie.css

/* 초기화 */
*,
*::before,
*::after { box-sizing: border-box; } 

:root { -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; } 

html { line-height: 1.15;
 -webkit-text-size-adjust: 100%; } 

body { margin: 0; } 

hr { height: 0; /* 1 */
 color: inherit; /* 2 */ } 

abbr[title] { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; } 

code,
kbd,
samp,
pre { font-size: 1em; /* 2 */ } 

small { font-size: 80%; } 

sub,
sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } 

sub { bottom: -0.25em; } 

sup { top: -0.5em; } 

table { text-indent: 0; /* 1 */
 border-color: inherit; /* 2 */ } 

button,
input,
optgroup,
select,
textarea { font-family: inherit; /* 1 */
 font-size: 100%; /* 1 */
 line-height: 1.15; /* 1 */
 margin: 0; /* 2 */ } 

button,
select { /* 1 */
 text-transform: none; } 

button,
[type='button'],
[type='reset'],
[type='submit'] { -webkit-appearance: button; } 

::-moz-focus-inner { border-style: none; padding: 0; } 

:-moz-focusring { outline: 1px dotted ButtonText; } 

:-moz-ui-invalid { box-shadow: none; } 

legend { padding: 0; } 

progress { vertical-align: baseline; } 

::-webkit-inner-spin-button,
::-webkit-outer-spin-button { height: auto; } 

[type='search'] { -webkit-appearance: textfield; /* 1 */
 outline-offset: -2px; /* 2 */ } 

::-webkit-search-decoration { -webkit-appearance: none; } 

::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */
 font: inherit; /* 2 */ } 

summary { display: list-item; } 

blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre { margin: 0; } 

button { background-color: transparent; background-image: none; } 

button:focus { outline: 1px dotted; outline: 5px auto -webkit-focus-ring-color; } 

fieldset { margin: 0; padding: 0; } 

ol,
ul { list-style: none; margin: 0; padding: 0; } 

body { font-family: inherit; line-height: inherit; } 

*,
::before,
::after { box-sizing: border-box; /* 1 */
 border-width: 0; /* 2 */
 border-style: solid; /* 2 */
 border-color: #e5e7eb; /* 2 */ } 

hr { border-top-width: 1px; } 

img { border-style: solid; } 

textarea { resize: vertical; } 

input::-moz-placeholder,
textarea::-moz-placeholder { color: #9ca3af; } 

input:-ms-input-placeholder,
textarea:-ms-input-placeholder { color: #9ca3af; } 

input::placeholder,
textarea::placeholder { color: #9ca3af; } 

button,
[role="button"] { cursor: pointer; } 

table { border-collapse: collapse; } 

h1,
h2,
h3,
h4,
h5,
h6 { font-size: inherit; font-weight: inherit; } 

a { color: inherit; text-decoration: inherit; } 

button,
input,
optgroup,
select,
textarea { padding: 0; line-height: inherit; color: inherit; } 

img,
svg,
video,
canvas,
audio,
iframe,
embed,
object { display: block; vertical-align: middle; } 
/* 초기화 끝 */

html { font-size: 62.5%; } 
body { background: #EAEEF4; } 
h3, h5, p, ul, li { color: #000; margin: 0; padding: 0; } 
ul, li { list-style: none; }

.loader { width: 100%; display: flex; justify-content: center; margin-top: 50%; }
.loader_text { font-size: 4rem; }
.loader_text { color: #000; } 
.movies_container { display: flex; flex-wrap: wrap; width: 90%; margin: 10rem auto 0; justify-content: space-between; } 
.movie_list { margin-bottom: 10rem; display: flex; flex-basis: 48%; background: #fff; box-shadow: 4px 4px 14px 0px #cacaca; } 
.movie_poster { margin-right: 1rem; position: relative; left: 2rem; top: -5rem; box-shadow: 0 7px 14px 0px #909090; align-self: flex-start; }
.movie_poster img { width: 150px; }
.movie_contents { margin: 2rem 2rem 2rem 4rem; }
.movie_title { font-size: 2rem; font-weight: 500; margin-bottom: 1rem; display: inline-block; }
.movie_year { font-size: 1.4rem; display: inline-block; margin-left: 1rem; color: #b1b1b1; }
.movie_genres { display: table; }
.movie_genres .genres_list { display: table-cell; font-size: 1.4rem; color: #b1b1b1; }
.movie_genres .genres_list:not(:last-child) { border-right-width: .5rem; border-color: transparent; }
.movie_summary { font-size: 1.4rem; color: #b1b1b1; margin-top: 2rem; line-height: 2rem; } 
반응형