*이 게시글은 Do it! 클론 코딩 영화 평점 웹서비스를 보고 정리한 글입니다.


목차

1. HTML구조 만들어주고 장르 추가하기

2. CSS로 스타일링하기

3. 영화앱 여러 기능 추가하기-react-router-dom 설치하고 프로젝트 폴더 정리하기

4.  영화앱 여러 기능 추가하기-라우터 만들기

5. 영화앱 여러 기능 추가하기-내비게이션 만들어보기

6. 영화앱 여러 기능 추가하기-영화 상세 정보 기능 만들어보기

7. 영화앱 여러 기능 추가하기-리다이렉트 기능 만들어 보기

8. 영화 앱 깃허브에 배포하기


1. HTML 구조 만들어주고 장르 추가하기

그 다음 기본적인 스타일링을 해보도록 하자.

스타일링의 핵심은 CSS지만 CSS를 적용할 HTML도 필요하므로 HTML구조를 넣어준다.

 

HTML에서 class속성을 사용하려면 'class'를 그대로 쓰지만 리액트에서는 class 속성을 사용하기 위해서는 'class'가 아니라 'className'을 써야한다.

리액트는 JSX를 HTML로 변환하면서 className을 class로 다시 바꿔준다.

 

App.js

import React from 'react';
import axios from 'axios';
import Movie from './Movie';


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=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) => {
              return (
                <Movie
                  key={movie.id}
                  id={movie.id}
                  year={movie.year}
                  title={movie.title}
                  summary={movie.summary}
                  poster={movie.medium_cover_image}
                />
              );
            })}
          </div>
        )}
      </section>
    );
  }
}

export default App;

 

Movie.js

코드를 완성해보니 id props가 필요없어서 지운다.

import React from 'react';
import PropTypes from 'prop-types';

function Movie({ title, year, summary, poster }) {
  return (
    <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>
        <p className="movie__summary">{summary}</p>
      </div>
    </div>
  );
}

Movie.propTypes = {

  year: PropTypes.number.isRequired,
  title: PropTypes.string.isRequired,
  summary: PropTypes.string.isRequired,
  poster: PropTypes.string.isRequired,
};

export default Movie;

 

여기까지 만들고보니 영화에서 나름 중요한 장르 데이터가 안들어가있다.

추가해준다.

 

arrayOf(PropTypes.string)은 문자열을 원소로 하는 배열을 의미한다.

import React from 'react';
import PropTypes from 'prop-types';


function Movie({ title, year, summary, poster, genres }) {
  return (
    <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>
        <p className="movie__summary">{summary}</p>
      </div>
    </div>
  );
}

Movie.propTypes = {
  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;

아래 App 컴포넌트에는

genres={movie.genres}를 추가한다.

import React from 'react';
import axios from 'axios';
import Movie from './Movie';


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=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) => {
              return (
                <Movie
                  key={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 컴포넌트에 장르가 출력될 수 있도록 코드를 수정한다

<ul className="movie__genres">
            {genres.map((genre)=>{
                return <li className="movie__genre">{genre}</li>;

            })}
        </ul>
import React from 'react';
import PropTypes from 'prop-types';


function Movie({ title, year, summary, poster, genres }) {
  return (
    <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)=>{
                return <li className="movie__genre">{genre}</li>;

            })}
        </ul>
        <p className="movie__summary">{summary}</p>
      </div>
    </div>
  );
}

Movie.propTypes = {
  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;

 

위와 같은 코드를 짜면 오류가 발생된다.

앞에서 잠깐 얘기했지만 컴포넌트를 여러개 출력할 때는 유일한 값을 이용하여 key props를 추가해야한다.

그런데 API에서 movies는 id라는 식별가능한 유일한 키가 있는데 genres에는 그런게 없다.

이런 경우에는 어떻게 해야할까?

map함수의 두번째 인자로 index를 주면된다.

자세한 사항은 map함수에 대해 찾아보기 바란다.

 

<ul className="movie__genres">
            {genres.map((genre,index)=>{
                return <li key={index}className="movie__genre">{genre}</li>;

            })}
        </ul>
import React from 'react';
import PropTypes from 'prop-types';


function Movie({ title, year, summary, poster, genres }) {
  return (
    <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)=>{
                return <li key={index} className="movie__genre">{genre}</li>;

            })}
        </ul>
        <p className="movie__summary">{summary}</p>
      </div>
    </div>
  );
}

Movie.propTypes = {
  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;

2. CSS로 스타일링하기

우선 Movies.css와 App.css를 만든다.

그 후 Movie.js와 App.js에 import 시킨다.

 

그 다음 아래 코드를 그냥 복사한다.

css수업이 아니므로 자세한 사항은 생략한다.

 

App.css

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
    'Open Sans', 'Helvetica Neue', sans-serif;
  background-color: #eff3f7;
  height: 100%;
}

 

Movie.css

.movies .movie {
    background-color: white;
    margin-bottom: 70px;
    font-weight: 300;
    padding: 20px;
    border-radius: 5px;
    color: #adaeb9;
    box-shadow: 0 13px 27px -5px rgba(50, 50, 93, 0.25), 0 8px 16px -8px rgba(0, 0, 0, 0.3),
      0 -6px 16px -6px rgba(0, 0, 0, 0.025);
  }
  
  .movies .movie a {
    display: grid;
    grid-template-columns: minmax(150px, 1fr) 2fr;
    grid-gap: 20px;
    text-decoration: none;
    color: inherit;
  }
  
  .movie img {
    position: relative;
    top: -50px;
    max-width: 150px;
    width: 100%;
    margin-right: 30px;
    box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3),
      0 -12px 36px -8px rgba(0, 0, 0, 0.025);
  }
  
  .movie .movie__title,
  .movie .movie__year {
    margin: 0;
    font-weight: 300;
  }
  
  .movie .movie__title {
    margin-bottom: 5px;
    font-size: 24px;
    color: #2c2c2c;
  }
  
  .movie .movie__genres {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    margin: 5px 0px;
  }
  
  .movie__genres li,
  .movie .movie__year {
    margin-right: 10px;
    font-size: 14px;
  }

 

영화 요약을 180자로 제한하기 위해 아래 코드를 수정한다.

<p className="movie__summary">{summary.slice(0,180)}</p>
import React from 'react';
import PropTypes from 'prop-types';
import './Movie.css';

function Movie({ title, year, summary, poster, genres }) {
  return (
    <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)=>{
                return <li key={index} className="movie__genre">{genre}</li>;

            })}
        </ul>
        <p className="movie__summary">{summary.slice(0,180)}</p>
      </div>
    </div>
  );
}

Movie.propTypes = {
  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;

 

뜨는 제목을 바꾸기 위해 index에서 Movie App으로 바꿔준다.


3. 영화앱 여러 기능 추가하기-react-router-dom 설치하고 프로젝트 폴더 정리하기

◆네비게이션 기능

Home과 About을 만들어서 각가을 클릭하면 해당 화면으로 이동시키는 메뉴이다.

이렇게 화면을 이동시키기 위해서는 장치가 필요한데 그것을 라우터라고 하며

라우터는 react-router-dom 패키지를 이용하면 쉽게 도입 가능하다.

 

cmd나 터미널에 명령어 입력

npm install react-router-dom

 

 

우선 src폴더 아래에 components폴더랑 routes폴더를 만든다.

그 다음 components에 Movie.css, Movie.js 파일 이동

routes폴더에는 About.js, Home.js를 만들어준다.

그런다음 App.js 코드를 Home.js에 복사하고 이름변경에 맞게 수정해준다.

 

Home.js

import React from 'react';
import axios from 'axios';
import Movie from '../components/Movie';
import './Home.css';

class Home 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=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) => {
              return (
                <Movie
                  key={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.css

.container {
  height: 100%;
  display: flex;
  justify-content: center;
}

.loader {
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  font-weight: 300;
}
.movies {
  display: grid;
  grid-template-columns: repeat(2, minmax(400px, 1fr));
  grid-gap: 100px;
  padding: 50px;
  width: 80%;
  padding-top: 70px;
}

@media screen and (max-width: 1090px) {
  .movies {
    grid-template-columns: 1fr;
    width: 100%;
  }
}

 

App.js도 수정

import React from 'react';
import Home from './routes/Home';
import './App.css';


function App(){
  return <Home/>;
}

export default App;

4.  영화앱 여러 기능 추가하기-라우터 만들기

라우터는 사용자가 입력한 URL을 통해 특정 컴포넌트를 불러준다.

예를 들어 사용자가 localhost:3000/about이라고 입력하면 About 컴포넌트를 불러주는 것이다.

react-router-dom은 여러 종류의 라우터를 제공하는데 우리는 HashRouter과 Route 컴포넌트를 사용할 것이다.

 

App.js 코드

 

<Route path="/about" element={<About/>}/>

이렇게 작성해주면 localhost:3000/#/about 주소일 경우

About 컴포넌트 화면을 보여준다.

home도 마찬가지

 

 

import React from 'react';
import './App.css';
import {HashRouter,Route, Routes} from 'react-router-dom';
import About from './routes/About';
import Home from './routes/Home';


function App(){
  return(
    <HashRouter>
      <Routes>
      <Route path="/" element={<Home/>}/>
      <Route path="/about" element={<About/>}/>
      </Routes>
    </HashRouter>
  )
}

export default App;

 

About컴포넌트에도 내용이 있어야하므로 간단하게 만들어준다.

 

About.js

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

function About() {
  return (
    <div className="about__container">
      <span>
        “Freedom is the freedom to say that two plus two make four. If that is granted, all else
        follows.”
      </span>
      <span>- George Orwell, 1984</span>
    </div>
  );
}

export default About;

About.css

.about__container {
  box-shadow: 0 13px 27px -5px rgba(50, 50, 93, 0.25), 0 8px 16px -8px rgba(0, 0, 0, 0.3),
    0 -6px 16px -6px rgba(0, 0, 0, 0.025);
  padding: 20px;
  border-radius: 5px;
  background-color: white;
  margin: 0 auto;
  margin-top: 100px;
  width: 100%;
  max-width: 400px;
  font-weight: 300;
}

.about__container span:first-child {
  font-size: 20px;
}
.about__container span:last-child {
  display: block;
  margin-top: 10px;
}


5. 영화앱 여러 기능 추가하기-내비게이션 만들어보기

라우터가 준비되었으므로 내비게이션을 통해 다른 화면으로 이동하면 된다.

 

먼저 components폴더에 Navigation.js를 만든다.

그리고 Navigation.css 파일도 만든다.

App 컴포넌트에 Navigation 컴포넌트를 import 시킨다

 

페이지 이동시키는 거니까 html a태그 쓰면 되겠지라고 생각하겠지만

a태그는 링크를 누를 때마다 리액트가 죽고 새 페이지가 열리는 문제점이 있어서

react-router-dom의 Link 컴포넌트를 사용한다.

 

Navigation.js

import React from 'react';
import { Link } from 'react-router-dom';
import './Navigation.css';

function Navigation() {
  return (
    <div className="nav">
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
    </div>
  );
}

export default Navigation;

 

App.js

import React from 'react';
import './App.css';
import {HashRouter,Route, Routes} from 'react-router-dom';
import About from './routes/About';
import Home from './routes/Home';
import Navigation from './components/Navigation';

function App(){
  return(
    <HashRouter>
      <Navigation/>
      <Routes>
      <Route path="/" element={<Home/>}/>
      <Route path="/about" element={<About/>}/>
      </Routes>
    </HashRouter>
  )
}

export default App;

 

Navigation.css

.nav {
    z-index: 1;
    position: fixed;
    top: 50px;
    left: 10px;
    display: flex;
    flex-direction: column;
    background-color: white;
    padding: 10px 20px;
    box-shadow: 0 13px 27px -5px rgba(50, 50, 93, 0.25), 0 8px 16px -8px rgba(0, 0, 0, 0.3),
      0 -6px 16px -6px rgba(0, 0, 0, 0.025);
    border-radius: 5px;
  }
  
  @media screen and (max-width: 1090px) {
    .nav {
      left: initial;
      top: initial;
      bottom: 0px;
      width: 100%;
    }
  }
  
  .nav a {
    text-decoration: none;
    color: #0008fc;
    text-transform: uppercase;
    font-size: 12px;
    text-align: center;
    font-weight: 600;
  }
  
  .nav a:not(:last-child) {
    margin-bottom: 20px;
  }

 


6. 영화앱 여러 기능 추가하기-영화 상세 정보 기능 만들어보기

Home에서 볼 수 있는 영화 정보는 아주 일부분인데 영화 카드를 누르면 상세 정보를 보여주는 기능을 만들 것이다.

 

이 기능을 만들기 위해서는 route props를 알아야한다

route props는 라우팅 대상이 되는 커포넌트에 넘겨주는 기본 props를 말하는데

우리가 직접 넘겨주지 않아도 기본으로 넘어가는 route props라는 것이 있고 이것을 이용해야 영화 데이터를 상세 정보 컴포넌트에 전달할 수 있다.

 

우선 Link를 import 하고

카드를 누르면 상세 정보를 보여줘야하므로 영화 div 태그안에 Link태그로 감싼다.

그리고 Link에 클릭시 state값을 넘겨준다.

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

function Movie({ title, year, summary, poster, genres }) {
  return (
    <div className="movie">
      <Link //여기
        to={"/movie-detail"}
        state={{ year, title, summary, poster, genres }}
      >
        <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) => {
              return (
                <li key={index} className="movie__genre">
                  {genre}
                </li>
              );
            })}
          </ul>
          <p className="movie__summary">{summary.slice(0, 180)}</p>
        </div>
      </Link> //여기
    </div>
  );
}

Movie.propTypes = {
  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;

 

카드를 누르면 detail화면에 넘어가게 만들었으니 이제 detail 컴포넌트를 만들자

 

detail 컴포넌트를 우선 간단하게 만들어주고

import React from 'react';
import {useLocation} from "react-router-dom";

function Detail(props){
    const location=useLocation();
    console.log(location);
    return <span>hello</span>;
    }

export default Detail;

 

App.js에 Detail import 추가하고

route path 추가해준다.

import React from 'react';
import './App.css';
import {HashRouter,Route, Routes} from 'react-router-dom';
import About from './routes/About';
import Home from './routes/Home';
import Navigation from './components/Navigation';
import Detail from './routes/Detail'; //여기

function App(){
  return(
    <HashRouter>
      <Navigation/>
      <Routes>
      <Route path="/"  element={<Home/>}/>
      <Route path="/about" element={<About/>}/>
      <Route path="/movie-detail" element={<Detail/>}/> //여기
      </Routes>
    </HashRouter>
  )
}

export default App;

state 값이 잘 들어가져있는 것을 볼 수 있다


7. 영화앱 여러 기능 추가하기-리다이렉트 기능 만들어 보기

영화 카드를 누르면 state가 잘 들어있는데 URL로 직접 /movie-detail 쳐서 들어가면 state값이 null은 것을 확인할 수 있다

Detail 컴포넌트로 영화 데이터가 넘어가지 못했기 때문에 이런 경우 사용자를 강제로 Home으로 돌려보내야한다.

이런 기능을 리다이렉트 기능이라고 부른다.

 

Detail.js 파일을 아래와 같이 바꾼다

import React from 'react';
import {useLocation, useNavigate} from "react-router-dom";
import { useEffect } from 'react';

function Detail(props){
    const location=useLocation();
    const navigate = useNavigate();
    useEffect(() => {
        
        if(location.state===null){
            navigate('/'); //home으로 되돌아 가는 기능
        }
});
   
    if(location.state){
        return <span>{location.state.title}</span> //제목 출력
    }
    return null;
    }

export default Detail;

 

영화앱을 완성했다. 이제 배포해보자.


8. 영화 앱 깃허브에 배포하기

아래 링크보고 따라하면 된다

https://velog.io/@nemo/github-page-deploy-%EB%B0%B0%ED%8F%AC

 


참고문헌

Do it! 클론 코딩 영화 평점 웹서비스-니꼴라스

[React] Link를 통해 State 전달하기 ( Router v6 ) (velog.io)

https://blog.woolta.com/categories/1/posts/211

https://velog.io/@boyfromthewell/React-useLocation-%EC%82%AC%EC%9A%A9%EC%8B%9C-state-%EA%B0%92%EC%9D%B4-null%EB%A1%9C-%EB%93%A4%EC%96%B4%EC%98%A4%EB%8A%94-%EB%AC%B8%EC%A0%9C

 

+ Recent posts