
[React] ReactJS로 영화 웹 서비스 만들기 (5)
Lpla
·2021. 2. 13. 22:11
반응형
#4 Making the Movie App
이번 포스팅이 영화 웹 서비스 만들기의 핵심이 되는 파트이다.
fetch
- 자바스크립트는 필요할 때 서버에 네트워크 요청을 보내고 새로운 정보를 받아올 수 있다.
- 일반적으로 fetch()를 사용하지만 여기선 Axios를 사용한다.
- Axios는
npm install axios
로 설치한다.
YTS API
- 영화 데이터는 YTS에서 만든 API를 사용한다.
- 구글에 YTS를 검색하여 공식 사이트로 이동한다.
- 사이트 하단에 API를 클릭하여 List Moives 탭으로 간다.
- 포스팅 시점 기준으로 JSON의 경로는 https://yts.mx/api/v2/list_movies.json 이고 URL을 열어보면 영화 정보가 가득하다.
- 크롬 확장프로그램으로 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; }
반응형