한 걸음씩
[React] Movie App 본문
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 |