한 걸음씩

[React] Movie App 본문

React

[React] Movie App

winter17 2023. 7. 13. 15:04

파일 구조

routes폴더에서 만드는 파일들은 주로 라우팅과 관련된 정보들을 가지고 있다. 

라우팅은 사용자가 웹 애플리케이션 내에서 다른 페이지로 이동하는 기능을 의미하며 'react-router-dom' 라이브러리를 사용한다.

 

components폴더에서 만드는 파일은 직접적으로 화면에 표시될 부분이다. 

각 컴포넌트는 독립적으로 재사용 가능한 기능을 갖고 있고, 이러한 컴포넌트들을 조합하여 전체 애플리케이션을 구성한다.

 

// index.js
import React from 'react';
import {createRoot} from 'react-dom/client';
import App from './App';
import "./styles.css";

const root = createRoot(document.getElementById('root'))
root.render(<App tab="home" />)

 

// styles.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%;
}

 

// App.js
// 페이지 이동을 맡은 페이지
import styles from "./App.module.css"
import {
  BrowserRouter as Router,
  Routes,
  Route,
  Link,
} from "react-router-dom"
import Home from "./routes/Home"
import Detail from "./routes/Detail";

// App.js는 router는 render한다
// router는 URL을 보고있는 component (App.js는 이동을 맡은 페이지)
function App(){
  return (
    <Router>
      <Routes>
        {/* 메인 홈으로 이동 */}
        <Route path="/" element={<Home/>}/>
        {/* id값과 함께 디테일 페이지로 이동 */}
        <Route path="/movie/:id" element={<Detail/>}/>
      </Routes>
    </Router>
  )
}

export default App;

⎣ 페이지 이동을 맡은 App.js

 

BrowserRouter, Routes, Route, Link는 React Router 라이브러리에서 제공되는 컴포넌트이다.
React 앱에서 라우팅을 구현하고 URL 관리를 할 수 있도록 도와준다.
BrowserRouter 앱의 최상위에 위치하고 URL 경로에 따라 해당하는 컴포넌트를 랜더링한다
Routes BrowserRouter 안에 사용되는 컴포넌트로, URL 경로와 각 경로에 대한 매칭되는 컴포넌트를 정의한다.
Routes는 여러 개의 Route 컴포넌트를 포함할 수 있고, 경로에 따라 적절한 Route를 선택하여 랜더링한다.
Route 특정 URL 경로와 해당 경로에 대응하는 컴포넌트를 정의한다.
Route컴포넌트는 'path' prop을 사용하여 경로를 지정하고, 해당 경로가 매칭될 때 랜더링될 컴포넌트를 지정한다.
예를 들어, <Route path="/about" component={About} />와 같이 사용할 수 있다.
Link 클릭 가능한 링크를 생성하는 역할을 한다.
사용자가 클릭하면 지정된 URL로 페이지를 이동시키고 BrowerRouter를 통해 해당 URL에 대한 랜더링을 처리한다.
일반적으로 네비게이션 메뉴나 페이지 간 링크를 생성할 때 사용된다.
예를 들어, <Link to="/about">About</Link>와 같이 사용할 수 있다.
--------------------------------------------------------------------
<a>태그와 Link 둘 다 링크를 생성한다는 점에서는 같지만 몇가지 차이점이 있다.
Link는 페이지 새로고침을 하지 않고 페이지 전환할 수 있고, 브라우저 히스토리를 관리한다.
이 말은 뒤로가기, 앞으로 가기 등과 같은 브라우저 내 탐색 동작을 올바르게 처리하는 데 도움이 된다.
<a>태그는 단순히 새로운 URL로 이동할 뿐이다.
react router설치 명령어 npm install react-router-dom (설치)
npm i react-router-dom@6.14.1 (버전 업그레이드)

 

 

// Home.js
// Home.js는 영화 데이터를 가져오고 로딩 상태를 관리(메인 페이지)

import Movie from "../components/Movie";
import { useEffect, useState } from "react";
import styles from "./Home.module.css"

function Home(){
  // loading할 useState
  const [loading, setLoading] = useState(true)
  // 영화정보를 불러올 useState
  const [movies, setMovies] = useState([])
  // then 대신 async, await 사용
  const getMovies = async() => {
    const json = await (
      await fetch(
      'https://yts.mx/api/v2/list_movies.json?minimum_rating=9&sort_by=year'
      )).json()
    setMovies(json.data.movies)
    setLoading(false)
  }
  // 최초 1회만 실행될 수 있도록 useEffect사용
  useEffect(() => {
    getMovies()
  }, [])

  return (
    <div className={styles.container}>
      {loading ? (
        <div className={styles.loader}>
          <span>Loading...</span>
        </div>
      ) : (
        <div className={styles.movies}>
          {movies.map((movie) => (
            // Movie 컴포넌트 랜더링하고 Movie 컴포넌트에 정보 전달하여 데이터 처리
            <Movie
              key={movie.id}
              id={movie.id}
              year={movie.year}
              coverImg={movie.medium_cover_image}
              title={movie.title}
              summary={movie.summary}
              genres={movie.genres}
            />
          ))}
        </div>
      )}
    </div>
  );
}

export default Home;

⎣ Home.js는 Movie.js에 보낼 데이터를 관리하는 파일

⎣ API로부터 데이터를 받아 useEffect로 최초 1회 랜더링을 하고 Movie 컴포넌트에 보낼 데이터를 JSX 코드로 작성

 

async, await는 Promise를 보다 쉽게 다룰 수 있게 해주는 문법적인 도구이다. 이를 통해 비동기 작업의 코드를 간결하고 직관적으로 작성할 수 있으며, 콜백 함수나 .then() 메서드 체인을 사용하는 것보다 가독성이 좋아진다.
async 1. 함수를 비동기 함수로 정의하는 데 사용
2. 비동기 함수는 내부에서 await 키워드를 사용하여 비동기 작업의 완료를 기다릴 수 있다
3. 항상 Promise를 반환한다. Promise의 결과는 비동기적으로 해결되거나 거부된다
await 1. async 함수 내부에서만 사용할 수 있다
2. 비동기 함수나 Promise 앞에 사용되어 해당 작업의 완료를 기다린다
3. 비동기 작업이 완료될 때까지 함수의 실행을 일시 중지하고, 작업이 완료되면 결과를 반환한다. 

 

// Movie.js
// Movie는 각 영화 아이템을 표시

import styles from "./Movie.module.css"
// prop-types는 리액트 컴포넌트에서 전달받은 프로퍼티(props)의 유효성을 검사하는 데 사용되는 라이브러리
// 잘못된 데이터를 전달하는 경우를 사전에 감지하고 오류를 방지할 수 있다
import PropTypes from 'prop-types'
// Link 컴포넌트는 페이지 간의 경로 이동을 처리해준다
// <a>태그와 유사한 역할을 수행하지만 Link는 페이지 전환 시에 브라우저를 새로고침하지 않는다(단순히 이동만!)
import {Link} from 'react-router-dom'

// Movie component는 이 props(coverImg, title, summary, genres)를 부모 component로부터 받아옴
function Movie({ id, coverImg, title, year, summary, genres }) {
  return (
    <div className={styles.movie}>
      <img src={coverImg} alt={title} className={styles.movie__img} />
      <div>
        <h2 className={styles.movie__title}>
          <Link to={`/movie/${id}`}>{title}</Link>
        </h2>
        <h3 className={styles.movie__year}>{year}</h3>
        {summary && (
          <p>
            {`${summary.length > 235}` ? `${summary.slice(0, 235)}...` : `${summary}`}
          </p>
        )}
        <ul className={styles.movie__genres}>
          {genres.map((g) => (
            <li key={g}>{g}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}
// Movie컴포넌트가 전달받는 프로퍼티의 유효성을 검사하는 역할
Movie.propTypes = {
  // isRequired는 필수적으로 전달되어야한다. 누락시 경고 메시지 표시
  id: PropTypes.number.isRequired,
  coverImg: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
  summary: PropTypes.string.isRequired,
  // genres는 문자열로 이루어진 배열 타입이어야한다
  genres: PropTypes.arrayOf(PropTypes.string).isRequired,
}
export default Movie

⎣ Movie.js는 실제 화면에 나타낼 코드를 JSX로 작성

⎣ Link를 사용하여 페이지 새 로고 침 없이 페이지 전환

 

// Detail.js
import { useEffect, useState } from "react"
import Movie from "../components/Movie";
import PropTypes from 'prop-types'
import { useParams } from "react-router-dom"
// useParams는 URL의 동적 세그먼트에 해당하는 값을 추출할 때 사용
// 일반적으로 URL의 경로에 존재하는 매개변수를 가져오기 위해 사용
// '/movie/123'과 같은 URL이 있다면 useParams를 사용하여 id값인 123을 가져옴

function Detail(){
  const [loading, setLoading] = useState(true)
  const [movie, setMovie] = useState(null)
  // App.js에서 받은 URL에 포함된 id값을 useparams가 받아 
  // id값을 기반으로 api를 호출하여 페이지 이동
  const {id} = useParams()
  const getMovie = async() => {
    const json = await (
      await fetch(`https://yts.mx/api/v2/movie_details.json?movie_id=${id}`)
    ).json()
    setMovie(json.data.movie)
    setLoading(false) // 데이터를 가져왔으니까 로딩상태를 false
  }
  useEffect(() => {
   getMovie()
  }, [])

  return (
    <div>
      {loading ? (
        <h1>Loading...</h1>
      ) : (
        <div>
          <h1>{movie.title}</h1>
          <p>{movie.summary}</p>
            <Movie 
              key={movie.id}
              id={movie.id}
              year={movie.year}
              coverImg={movie.medium_cover_image}
              title={movie.title}
              summary={movie.summary}
              genres={movie.genres}
            />
        </div>
      )}
    </div>
  )
}

Detail.propTypes = {
  id: PropTypes.string.isRequired,
}

export default Detail

⎣ Detail.js는 상세 페이지인데, useParams 훅을 사용하여 URL에서 추출한 id값을 기반으로 API를 호출하여 데이터를 가져온다.

Detail컴포넌트에서 받은 id값으로 url접속하면 summary목록이 없는데 이 부분은 확인하고 코드 작성해야 함!

 


<css>

// styles.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%;
}

 

// Home.module.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;
  /* 그리드 컨테이너의 열을 설정 : 2개의 열을 생성하여 최소 너비는 400px, 남은 공간은 동일하게 분할 */
  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%;
  }
}

 

// Movie.module.css
.movie {
  background-color: white;
  margin-bottom: 70px;
  font-weight: 300;
  padding: 20px;
  border-radius: 5px;
  color: #adaeb9;
  display: grid;
  grid-template-columns: minmax(150px, 1fr) 2fr;
  grid-gap: 20px;
  text-decoration: none;
  color: inherit;
  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);
}

.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__title,
.movie__year {
  margin: 0;
  font-weight: 300;
  text-decoration: none;
}

.movie__title a {
  margin-bottom: 5px;
  font-size: 24px;
  color: #2c2c2c;
  text-decoration: none;
}

.movie__genres {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  /* 아이템을 여러줄에 걸쳐 배치하는데 너비가 충분하지 않으면 줄을 바꿈 */
  flex-wrap: wrap;
  margin: 5px 0px;
}

.movie__genres li,
.movie__year {
  margin-right: 10px;
  font-size: 14px;
}

 

'React' 카테고리의 다른 글

[React] Styled Components  (0) 2023.07.20
리액트로 GitHub Pages에 배포하는 방법  (0) 2023.07.19
[React] Coin Tracker  (0) 2023.07.12
[React] To Do List  (0) 2023.07.10
[React] useEffect, cleanup  (0) 2023.07.07