5. Netflix 앱 완성하기
영화 나열을 위한 Row 컴포넌트 생성하기
1. Row 컴포넌트 생성
2. 소스코드 작성
2.1 영화 정보 가져오기
const[ movies, setMovies] = useState([]);
useEffect(()=> {
fetchMovieData();
}, [fetchURL])
const fetchMovieData = async() => {
const request = await axios.get(fetchURL);
setMovies(request.data.results);
}
2.2 UI 생성하기
return (
<section className="row">
<h2>{title}</h2>
<div className="slider">
<div className="slider__arrow-left">
<span className="arrow"
onClick={() => {
document.getElementById(id).scrollLeft -= window.innerWidth - 80;
}}
>{"<"}</span>
</div>
<div id={id} className="row__posters">
{movies.map(movie => (
<img
key={movie.id}
className={`row__poster ${isLargeRow && "row__posterLarge"}`}
src={`https://image.tmdb.org/t/p/original/${
isLargeRow ? movie.poster_path : movie.backdrop_path
} `}
alt={movie.name}
/>
))}
</div>
<div className="slider__arrow-right">
<span className="arrow"
onClick={() => {
document.getElementById(id).scrollLeft += window.innerWidth - 80;
}}
>{">"}</span>
</div>
</div>
</section>
)
}
3. CSS 작성
슬라이드 기능 추가하기
: 화살표 방향 클릭 시 슬라이드 구현
1. 오른쪽 화살표
<span className="arrow"
onClick={() => {
document.getElementById(id).scrollLeft += window.innerWidth - 80;
}}
>{">"}</span>
-> 영화 고유의 id 로 Element 를 찾는다.
-> scrollLeft : 요소의 콘텐츠가 왼쪽 가장자리에서 스크롤되는 픽셀 수
-> window.innerWidth : 안쪽의 가로폭
2. 왼쪽 화살표
<span className="arrow"
onClick={() => {
document.getElementById(id).scrollLeft -= window.innerWidth - 80;
}}
>{"<"}</span>
=> 즉, onClick 이 발생했을 때 영화 이미지를 (window.innerWidth - 80) 만큼 왼쪽으로 옮긴다.
#오른쪽으로 옮길 경우에는 픽셀을 더해주면 된다.
Styled Component를 이용해서 Footer 생성하기
1. UI 생성하기
export default function Footer() {
return (
<FooterContainer>
<FooterContent>
<FooterLinkContainer>
<FooterLinkTitle>넷플릭스 대한민국</FooterLinkTitle>
<FooterLinkContent>
<FooterLink href="https://help.netflix.com/ko/node/412">
넷플릭스 소개
</FooterLink>
<FooterLink href="https://help.netflix.com/ko">
고객 센터
</FooterLink>
<FooterLink href="https://help.netflix.com/ko/">
미디어 센터
</FooterLink>
<FooterLink href="https://help.netflix.com/ko/">
이용 약관
</FooterLink>
</FooterLinkContent>
<FooterDescContainer>
<FooterDescRights>
Netflix Rights Reserved.
</FooterDescRights>
</FooterDescContainer>
</FooterLinkContainer>
</FooterContent>
</FooterContainer>
);
}
2. Styled componet 이용하기
const FooterContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
border-top: 1px solid rgb(25, 25, 25);
width: 100%;
position: relative;
z-index: 100;
@media (max-width: 769px) {
padding: 20px 20px;
padding-bottom: 30px;
}
`;
-> @media : 동적인 웹페이지를 만들 때 사용.
=> max-width 보다 작아졌을 때, @media 안쪽 스타일링으로 바꾼다.
const FooterContent = styled.div``;
const FooterLinkContainer = styled.div`
width: 500px;
@media (max-width: 768px) {
width: 100%;
}
`;
const FooterLinkTitle = styled.h1`
color: gray;
font-size: 17px;
`;
const FooterLinkContent = styled.div`
display: flex;
justify-content: space-bewteen;
flex-wrap: wrap;
margin-top: 35px;
@media (max-width: 768px) {
margin-top: 26px;
}
`;
-> FooterContent 는 스타일링이 없지만, 통일성을 위해 만들어준다.
const FooterLink = styled.a`
color: gray;
font-size: 14px;
width: 110px;
margin-bottom: 21px;
text-decoration: none;
&:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
margin-bottom: 16px;
}
`;
const FooterDescContainer = styled.div`
margin-top: 30px @media (max-width: 768px) {
margin-top: 20px;
}
`;
const FooterDescRights = styled.h2`
color: white;
font-size: 14px;
text-align: center;
`;
-> &:hover : 마우스를 올렸을 때
=> 마우스를 올렸을 때 hover 안쪽 스타일링으로 바꾼다.
영화 자세히 보기 클릭 시 모달 생성하기
1. 해당 영화 클릭 시 모달 OPEN
const [ modalOpen, setModalOpen ] = useState(false);
const [ movieSelected, setMovieSelected ] = useState({});
const handleClick = (movie) => {
setModalOpen(true);
setMovieSelected(movie);
}
<img
key={movie.id}
className={`row__poster ${isLargeRow && "row__posterLarge"}`}
src={`https://image.tmdb.org/t/p/original/${
isLargeRow ? movie.poster_path : movie.backdrop_path
} `}
alt={movie.name}
onClick={()=> handleClick(movie)}
/>
2. 클릭 한 영화의 정보 가져오기
{
modalOpen && (
<MovieModal {...setMovieSelected} setModalOpen={setModalOpen}/>
)
}
3. 모달 컴포넌트 생성
4. Props 가져오기
import React from 'react'
import "./MovieModal.css";
function MovieModal({
backdrop_path,
title,
overview,
name,
release_date,
first_air_date_,
vote_average,
setModalOpen
}) {
return (
<div>
</div>
)
}
export default MovieModal
Movie 모달 UI 생성하기
1. UI 생성하기
return (
<div className="presentation">
<div className="wrapper-modal">
<div className="modal">
<span onClick={() => setModalOpen(false)} className="modal-close">
X
</span>
<img
className="modal__poster-img"
src={`https://image.tmdb.org/t/p/original/${backdrop_path}`}
alt="modal__poster-img"
/>
<div className="modal__content">
<p className="modal__details">
<span className="modal__user_perc">100% for you</span>{" "}
{release_date ? release_date : first_air_date}
</p>
<h2 className="modal__title">{title ? title : name}</h2>
<p className="modal__overview"> 평점: {vote_average}</p>
<p className="modal__overview"> {overview}</p>
</div>
</div>
</div>
</div>
);
}
.presentation {
z-index: 1200;
position: absolute;
}
-> 다른 UI 보다 위로 올라오게끔 함
.wrapper-modal {
position: fixed;
inset: 0px;
background-color: rgb(0 0 0 / 71%);
-webkit-tap-highlight-color: transparent;
display: flex;
justify-content: center;
}
-> 화면 가운데에 위치하게끔 함
.modal-close {
position: absolute;
right: 20px;
top: 20px;
cursor: pointer;
z-index: 1000;
color: white;
}
-> X 버튼
-> 우측 상단에 위치하게끔 함
React Router Dom
1. React Router Dom 이란?
: React Router Dom 을 사용하면 웹 앱에서 동적 라우팅을 구현할 수 있습니다. 라우팅이 실행 중인 앱 외부의 구성에서 처리되는 기존 라우팅 아카텍처와 달리 React Router Dom 은 앱 및 플랫폼의 요구 사항에 따라 컴포넌트 기반 라우팅을 용이하게 함.
2. Single Page Application (SPA)
: 리액트는 SPA 기반이기 때문에 하나의 index.html 템플릿 파일을 가지고 있다. 이 하나의 템플릿에 자바스크립트를 이용해서 다른 컴포넌트를 이 index.html 템플릿에 넣으므로 페이지를 변경해주게 된다. 이때 이 React Router Dom 라이브러리가 새 컴포넌트로 라우팅/탐색을 하고 렌더링하는데 도움을 주게 된다.
3. React Router Dom 설치하기
-> npm install react-router-dom --save
4. React Router 설정하기
-> BrowserRouter 로 루트 컴포넌트 감싸주기
-> 여러 컴포넌트 생성 및 라우트 정의하기
-> <Link /> 를 이용해 경로를 이동하기
React Router Dom APIs
1. 중첩 라우팅 (Nested Routes)
: React Router 의 가장 강력한 기능 중 하나.
-> 레이아웃 코드를 어지럽힐 필요가 없다.
2. Outlet
: 자식 경로 요소를 렌더링하려면 부모 경로 요소에서 <Outlet> 을 사용해야 한다. 이렇게 하면 하위 경로가 렌더링될 때 중첩된 UI가 표시될 수 있다.
3. useNavigate
: 경로를 바꿔준다.
#navigate('/home') ==> localhost:3000/home 으로 간다.
4. useParams
: style 문법을 path 경로에 사용하였다면 useParams 를 사용할 수 있다.
5. useLocation
: 현재 위치 객체를 반환한다.
-> 현재 위치가 변경될 때마다 일부 side effect를 수행하려는 경우에 유용하다.
6.useRoutes
: <Routes>와 기능적으로 동일하나, <Route> 대신 JS 객체를 사용하여 경로를 정의한다.
-> 일반 <Route> 요소와 동일하나, JSX가 필요하지 않다.
#이번 앱 만들 때 사용X
Netflix 앱에 React Router Dom 적용하기
1. React Router Dom 이용해서 구현할 부분
-> 검색 페이지
: 영화에 관련된 단어를 타이핑하여 관련된 영화가 있으면 해당 영화의 포스터 표시
-> 디테일 페이지
: 검색 페이지에서 영화의 포스터를 클릭하면 해당 영화의 세부 정보를 보여주는 페이지
2. 페이지 생성을 위한 폴더 및 파일 추가
3. App.js 를 라우팅을 위한 파일로 변경
-> App.js
import './App.css';
import Nav from './components/Nav';
import Footer from './components/Footer';
import { Outlet, Routes, Route } from 'react-router-dom';
import MainPage from './pages/MainPage';
import DetailPage from './pages/DetailPage';
import SearchPage from './pages/SearchPage';
const Layout = () => {
return(
<div>
<Nav />
<Outlet />
<Footer />
</div>
)
}
function App() {
return (
<div className="App">
<Routes>
<Route path='/' element={<Layout/>}>
<Route index element={<MainPage/>} />
<Route path=":movieId" element={<DetailPage/>} />
<Route path="search" element={<SearchPage/>} />
</Route>
</Routes>
</div>
);
}
export default App;
-> index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
reportWebVitals();
-> MainPage/index.js
import React from 'react'
import Banner from "../../components/Banner";
import Row from "../../components/Row";
import requests from '../../api/requests';
export default function MainPage() {
return (
<div>
<Banner />
<Row
title="NETFLIX ORIGINALS"
id="NO"
fetchUrl={requests.fetchNetflixOrignials}
isLargeRow
/>
<Row title="Trending Now" id="TN" fetchUrl={requests.fetchTrending}/>
<Row title="Top Rated" id="TR" fetchUrl={requests.fetchTopRated}/>
<Row title="Action Movies" id="AM" fetchUrl={requests.fetchActionMovies}/>
<Row title="Comedy Movies" id="CM" fetchUrl={requests.fetchComedyMovies}/>
</div>
)
}
useLocation을 이용한 검색 페이지 구현하기
1. NavBar 에 검색 Input 생성
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom';
import "./Nav.css"
export default function Nav() {
const [show, setShow] = useState(false);
const [searchValue, setSearchValue] = useState(); //useState를 생성해준다.
const navigate = useNavigate();
useEffect(() => {
window.addEventListener("scroll", () => {
console.log('window.scrollY',window.scrollY);
if(window.scrollY > 50) {
setShow(true);
} else {
setShow(false);
}
})
return () => {
window.removeEventListener("scroll", () => {});
};
}, []);
const handleChange = (e) => { //검섹창에 무언가를 칠 때마다 url주소창에 바로 반영이 된다.
setSearchValue(e.target.value);
navigate(`/search?q=${e.target.value}`)
}
return (
<nav className={`nav ${show && "nav__black"}`}>
<img
alt='Netflix logo'
src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Netflix_2015_logo.svg/400px-Netflix_2015_logo.svg.png"
className='nav__logo'
onClick={() => window.location.reload()}
/>
<input
value={searchValue}
onChange={handleChange}
className="nav__input"
type="text"
placeholder='영화를 검색해주세요.'
/>
<img
alt="User logged"
src="https://upload.wikimedia.org/wikipedia/commons/0/0b/Netflix-avatar.png?20201013161117"
className='nav__avatar'
/>
</nav>
)
}
#CSS 코드는 복사해온다.
2. Search 페이지에서 SearchTerm 가져오기
-> SearchPage/index.js
import React from 'react'
import { useLocation } from 'react-router-dom'
export default function SearchPage() {
console.log('useLocation()',useLocation());
const useQuery =() => {
return new URLSearchParams(useLocation().search)
}
let query = useQuery();
const searchTerm = query.get("q")
console.log('searchTerm',searchTerm);
return (
<div>SearchPage</div>
)
}
3. SearchTerm 이 바뀔 때마다 새로 영화 데이터 가져오기
: 단어를 타이핑할 때마다 찾는 것이 달라지게끔 한다.
#Spider 에서 S / Sp / Spi 이런식으로 타이핑할 때마다 요청을 새로 보낸다.
-> SearchPage/index.js
import axios from '../../api/axios';
import React, { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
export default function SearchPage() {
const [searchResults, setSearchResults] = useState([]);
const useQuery =() => {
return new URLSearchParams(useLocation().search)
}
let query = useQuery();
const searchTerm = query.get("q")
console.log('searchTerm',searchTerm);
useEffect(() => {
if(searchTerm) {
fetchSearchMovie(searchTerm);
}
}, [searchTerm]);
const fetchSearchMovie = async (searchTerm) => { //try-catch문으로 요청을 보낼 때, error가 있으면 catch해준다.
try{
const request = await axios.get(
`/search/multi?include_adult=false&query=${searchTerm}`
)
console.log(request);
setSearchResults(request.data.results);
} catch (error) {
console.log("error", error)
}
}
return (
<div>SearchPage</div>
)
}
검색 페이지 UI 구현하기
-> SearchPage/index.js
import axios from '../../api/axios';
import React, { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import "./SearchPage.css"
export default function SearchPage() {
const [searchResults, setSearchResults] = useState([]); // searchResults 상태를 관리하는 state를 생성합니다.
const useQuery =() => { // URL의 query string을 가져오는 함수입니다.
return new URLSearchParams(useLocation().search)
}
let query = useQuery(); // query 변수에 query string을 저장합니다.
const searchTerm = query.get("q") // searchTerm 변수에 검색어를 저장합니다.
console.log('searchTerm',searchTerm);
useEffect(() => { // 컴포넌트가 마운트되거나 searchTerm이 업데이트 될 때마다 실행됩니다.
if(searchTerm) {
fetchSearchMovie(searchTerm); // searchTerm이 있는 경우 fetchSearchMovie 함수를 호출합니다.
}
}, [searchTerm]);
const fetchSearchMovie = async (searchTerm) => { // 검색어에 따른 영화 데이터를 가져오는 함수입니다.
try {
const request = await axios.get( // API 요청을 보냅니다.
`/search/multi?include_adult=false&query=${searchTerm}`
)
console.log(request);
setSearchResults(request.data.results); // 결과를 searchResults 상태에 저장합니다.
} catch (error) {
console.log("error", error) // 에러가 발생한 경우 에러를 출력합니다.
}
}
const renderSearchResults = () => { // 검색 결과를 렌더링하는 함수입니다.
return searchResults.length > 0 ? ( // 검색 결과가 있는 경우
<section className="search-container">
{searchResults.map((movie) => { // 결과 배열을 순회하며
if(movie.backdrop_path !== null && movie.media_type !== "person") { // 이미지가 있고, 결과가 사람이 아닌 경우
const movieImageUrl =
"https://image.tmdb.org/t/p/w500" + movie.backdrop_path
return(
<div className='movie'>
<div
className="movie__column-poster"
>
<img
src={movieImageUrl} alt="movie"
className='movie__poster'
/>
</div>
</div>
)
}
})}
</section>
) : ( // 검색 결과가 없는 경우
<section className='no-results'>
<div className='no-results__text'>
<p>찾고자 하는 검색어"{searchTerm}"에 맞는 영화가 없습니다.</p>
</div>
</section>
)
}
return renderSearchResults(); // renderSearchResults 함수의 결과를 반환합니다.
}
#CSS 코드는 복사해온다.
useDebounce Custom Hooks 만들기
1. Debounce 란?
: 검색 입력에 입력할 때 입력 결과가 나타날 때까지 지연이 생기는데, 이 기능은 debounce 에 의해 제어된다. debounce 는 미리 결정된 시간 동안 사용자가 타이핑을 멈출 때까지 keyup 이벤트의 처리를 지연시킨다.
-> UI 코드가 모든 이벤트를 처리할 필요가 없어지고, 서버로 전송되는 API 호출 횟수도 크게 줄어든다. 입력된 모든 문자를 처리하면 성능이 저하되고 백엔드에 불필요한 로드가 추가될 수 있다.
#즉, Spider 를 검색할 때 Spi / der 이런식으로 타이핑을 중간에 멈춘다면, Spi / Spider 에 대한 요청만 보내는 것.
전에는 Spider 를 타이핑 하면 S / Sp / Spi / Spid ... 이런식으로 요청을 보냈다. 매우 비효율적.
2. hooks 폴더 및 파일 생성
-> useDebounce.js
import { useState, useEffect } from "react";
export const useDebounce = (value, delay) => {
const [debounceValue, setDebounceValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebounceValue(value)
}, delay);
return () => {
clearTimeout(handler)
};
}, [value, delay]);
return debounceValue;
}
3. searchTerm => debouncedSearchTerm
-> SearchPage/index.js
import axios from '../../api/axios';
import React, { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useDebounce } from '../../hooks/useDebounce'; // useDebounce 커스텀 훅을 불러옵니다.
import "./SearchPage.css"
export default function SearchPage() {
const [searchResults, setSearchResults] = useState([]);
const useQuery =() => {
return new URLSearchParams(useLocation().search)
}
let query = useQuery();
const searchTerm = query.get("q")
const debouncedSearchTerm = useDebounce(searchTerm, 500); // 검색어를 500ms 간격으로 디바운스 합니다.
console.log('searchTerm',searchTerm);
useEffect(() => {
if(debouncedSearchTerm) { // 디바운스된 검색어가 있을 경우
fetchSearchMovie(debouncedSearchTerm); // 디바운스된 검색어를 이용해 영화를 검색합니다.
}
}, [debouncedSearchTerm]);
const fetchSearchMovie = async (searchTerm) => {
try {
const request = await axios.get(
`/search/multi?include_adult=false&query=${searchTerm}`
)
console.log(request);
setSearchResults(request.data.results);
} catch (error) {
console.log("error", error)
}
}
const renderSearchResults = () => {
return searchResults.length > 0 ? (
<section className="search-container">
{searchResults.map((movie) => {
if(movie.backdrop_path !== null && movie.media_type !== "person") {
const movieImageUrl =
"https://image.tmdb.org/t/p/w500" + movie.backdrop_path
return(
<div className='movie' key = {movie.id}>
<div
className="movie__column-poster"
>
<img
src={movieImageUrl} alt="movie"
className='movie__poster'
/>
</div>
</div>
)
}
})}
</section>
) : (
<section className='no-results'>
<div className='no-results__text'>
<p>찾고자 하는 검색어"{debouncedSearchTerm}"에 맞는 영화가 없습니다.</p>
</div>
</section>
)
}
return renderSearchResults();
}
useParams를 이용한 영화 상세 페이지 구현하기
1. 포스터 클릭 시 상세 페이지로
-> SearchPage/index.js
const navigate = useNavigate();
<div className='movie' key = {movie.id}>
<div onClick={() => navigate(`/${movie.id}`)} className="movie__column-poster">
2. 상세 페이지에서 영화 상세 정보 가져오기
-> DetailPage/index.js
import React from 'react'
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import axios from '../../api/axios';
export default function DetailPage() {
const { movieId } = useParams();
const[movie, setMovie] = useState({}); // 영화 정보 가져온 것을 state에 넣어줘야 한다.
useEffect(() => {
async function fetchData() {
const request = await axios.get (`/movie/${movieId}`);
setMovie(request.data); // 정보들을 movie state에 넣어준다.
}
fetchData();
}, [movieId]); // movieId가 바뀔 때마다 fetchData를 call해주기
return (
<div>DetailPage</div>
)
}
3. UI 완성하기
if(!movie) return <div>...loading</div>; // movie가 없을 때는 loading 텍스트 보여주기
return <section>
<img
className='modal__poster-img'
src= {`https://image.tmdb.org/t/p/original/${movie.backdrop_path}`}
alt="poster"
/>
</section>
}
모달 창 외부 클릭 시 모달 닫게 만드는 Custom Hooks 생성
-> 어디를 클릭하는지 구분
-> react hooks 생성
-> 모달창 밖을 클릭하면 CallBack 함수를 호출하는 Event 를 등록
-> CallBack 함수 안에서 모달 닫아주기
=> useRef 를 이용해서 어디를 클릭하는지 구분할 수 있다.
1. useRef 란?
: 특정 Dom 을 선택할 때 사용하는 React Hooks
2. 특정 Dom 선택하기
3. Dom 을 직접 선택해야 하는 경우
-> 엘리먼트 크기를 가져와야 할 때
-> 스크롤바 위치를 가져와야 할 때
-> 엘리먼트에 포커스를 설정해줘야 할 때 등등
4. useRef 사용법
-> useRef() 를 통해 Ref 객체를 생성
-> 이 객체를 특정 Dom 에 ref 값으로 설정
-> Ref 객체의 .current 값이 특정 Dom 을 가르키게 된다.
5. React hooks 생성
6. 모달 창 바깥을 클릭하면 Callback 함수를 호출하는 Event 를 등록해주기
-> 클릭 시 모달 창 안이면 그냥 return
7. Callback 함수 안에서 모달 닫아주기
<코드>
-> useOnClickOutside.js
import React, { useEffect } from 'react'
const useOnClickOutside = (ref, handler) => {
useEffect(() => {
const listener = (event) =>{
console.log('ref', ref.current)
if(!ref.current || ref.current.contains(event.target)) {
return;
}
handler();
};
document.addEventListener("mousedown", listener)
document.addEventListener("touchstart", listener)
return () => {
document.addEventListener("mousedown", listener)
document.addEventListener("touchstart", listener)
};
}, []);
};
export default useOnClickOutside;
-> MovieModal/index.js
import React, { useRef } from "react";
import useOnClickOutside from "../../hooks/useOnClickOutside";
import "./MovieModal.css";
function MovieModal({
backdrop_path,
title,
overview,
name,
release_date,
first_air_date,
vote_average,
setModalOpen,
})
{
const ref = useRef()
useOnClickOutside(ref, () => {setModalOpen(false)});
return (
<div className='presentation'>
<div className="wrapper-modal">
<div className="modal"ref={ref}>
<span onClick={()=> setModalOpen(false)} className='modal-close'>
X
</span>
<img
className='modal__poster-img'
src={`https://image.tmdb.org/t/p/original/${backdrop_path}`}
alt='modal__poster-img'
/>
<div className="modal__content">
<p className="modal__details">
<span className="modal__user-perc">
100% for you
</span>
{release_date ? release_date : first_air_date}
</p>
<h2 className="modal__title">{title? title: name}</h2>
<p className="modal__overview"> 평점: {vote_average}</p>
<p className="modal_overview"></p>
</div>
</div>
</div>
</div>
)
}
export default MovieModal;
swiper 모듈을 이용한 터치 슬라이드 구현하기
-> https://swiperjs.com/react#usage
Swiper React Components
Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.
swiperjs.com
npm install swiper --save
-> Row.js
import React, { useEffect, useState } from 'react'
import axios from '../api/axios';
import MovieModal from "./MovieModal";
import "./Row.css";
import { Navigation, Pagination, Scrollbar, A11y } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/scrollbar';
export default function Row({isLargeRow, title, id, fetchUrl}) {
const [movies, setMovies] = useState([]);
const [modalOpen, setModalOpen] = useState(false);
const [movieSelected, setMovieSelected] = useState({});
useEffect(() => {
fetchMovieData();
}, []);
const fetchMovieData = async () => {
const request = await axios.get(fetchUrl);
setMovies(request.data.results)
};
const handleClick = (movie) => {
setModalOpen(true)
setMovieSelected(movie);
};
return (
<section className='row'>
<h2>{title}</h2>
<Swiper
modules={[Navigation, Pagination, Scrollbar, A11y]}
navigation // 화살표 버튼 사용 유무
pagination={{ clickable: true }}
loop={true} //loop 기능을 사용할 것인지, true로 하면 영화 목록의 마지막에 도달했을 때, 화살표를 누르면 처음으로 돌아간다.
breakpoints={{
1378: {
slidesPerView: 6,
slidesPerGroup: 6,
},
998: {
slidesPerView: 5,
slidesPerGroup: 5,
},
625: {
slidesPerView: 4,
slidesPerGroup: 4,
},
0: {
slidesPerView: 3,
slidesPerGroup: 3,
},
}}
>
<div id={id} className="row__posters">
{movies.map(movie => (
<SwiperSlide>
<img
key={movie.id}
className={`row__poster ${isLargeRow && "new__posterLarge"}`}
src={`https://image.tmdb.org/t/p/original/${
isLargeRow ? movie.poster_path : movie.backdrop_path
} `}
alt={movie.name}
onClick={() => handleClick(movie)}
/>
</SwiperSlide>
))}
</div>
</Swiper>
{
modalOpen && (
<MovieModal {...movieSelected} setModalOpen={setModalOpen}/>
)
}
</section>
)
}
#CSS도 수정해주어야 한다.
github를 이용해서 배포하기
1. 깃허브 저장소 생성
2. API_KEY 환경변수로 숨기기
-> .env 는 깃허브에 올라가지 않는다. 즉, .env 파일을 생성하여 이 안에서 환경변수를 만든다.
이후 axios 에서 API_KEY 부분을 수정해주면 된다.
-> axios.js
import axios from "axios";
const instance = axios.create({
baseURL: "https://api.themoviedb.org/3",
params: {
api_key : process.env.REACT_APP_MOVIE_DB_API_KEY,
language: "ko-KR",
},
});
export default instance;
3. 로컬 앱과 저장소 연결
4. gh-pages 모듈 설치
npm install gh-pages --save-dev
5. 홈페이지 url 작성
-> package.json
homepage : "https://깃허브 유저 이름.github.io/저장소 이름/"
6. 배포를 위한 script 추가
-> package.json
"predeploy": "npm run build",
"deploy": "gh-pages -d build",
7. react router dom 의 기본 경로 변경
-> index.js
ReactDOM.render(
<BrowserRouter basename='저장소의 이름'>
<App />
</BrowserRouter>,
document.getElementById('root')
);
8. deploy 시작
npm run deploy