React Router Dom
React Router DOM을 사용하면 웹 앱에서 동적 라우팅을 구현할 수 있다. 라우팅이 실행중인 앱 외부의 구성에서 처리되는 기존 라우팅 아키텍처와 달리, React Router DOM은 앱 및 플랫폼의 요구 사항에 따라 컴포넌트 기반 라우팅을 용이하게 된다.
쉽게 설명하자면, 어떠한 뷰가 a,b,c 3개 있으면, a뷰를 보여줘야 할 때 a뷰를 보여주고, b뷰를 보여줘야 할 때 b뷰를 보여주고, c뷰를 보여줘야 할 때 c뷰를 보여주고, 이런식으로 라우팅하는 것을 용이하게 해주는 라이브러리이다.
리액트는 SPA이기 때문에 하나의 index.html 템플릿 파일을 가지고 있다. 이 하나의 템플릿에 자바스크립트를 이용해 다른 컴포넌트를 이 index.html 템플릿에 넣으므로 페이지를 변경해주게 된다. 이 때 이 React Router Dom 라이브러리가 새 컴포넌트로 라우팅/탐색을 하고 렌더링하는데 도움을 주게 된다.
npm install react-router-dom --save
yarn add react-router-dom
설치가 완료된 후 가장 먼저 할 일은 앱 어디에서나 React Router를 사용할 수 있도록 하는 것이다.
이렇게 하려면 src 폴더에서 index.js 파일을 열고, react-router-dom에서 BrowserRouter를 가져온 다음 루트 구성 요소(App 구성 요소)를 그 안에 래핑한다.
BrowserRouter -> HTML5 History API(pushState, replaceState 및 popstate 이벤트)를 사용해 UI를 URL과 동기화된 상태로 유지해준다.
Link 구성 요소는 HTML의 앵커 요소와 유사하다. Link의 to 속성은 링크가 나를 데려가는 경로를 지정한다.
앱 구성요소에 나열된 경로 이름을 생성했기 때문에 링크를 클릭하면 경로를 살펴보고 해당 경로 이름으로 구성 요소를 렌더링한다.
React Router Dom APIs
이것은 React Router의 가장 강력한 기능 중 하나이므로 복잡한 레이아웃 코드를 어지럽힐 필요가 없다. 대부분의 레이아웃은 URL의 세그먼트에 연결되며 React Router는 이를 완전히 수용한다.
자식 경로 요소를 렌더링하려면 부모 경로 요소에서 Outlet을 사용해야 한다. 이렇게 하면 하위 경로가 렌더링될 때 중첩된 UI가 표시될 수 있다. 부모 라우트가 정확 히 일치하면 자식 인덱스 라우트를 렌더링하거나 인덱스 라우트가 없으면 아무것도 렌더링하지 않는다.
react-router-dom에서 가져와서 사용한다.
경로를 바꿔준다.
navigate('/home') ===> localhost:3000/home 으로 간다.
:style 문법을 path 경로에 사용하였다면 useParams()로 읽을 수 있다. 아래는 :invoiceId가 무엇인지 알기위해 usePrams를 사용했다.
이 Hooks는 현재 위치 객체를 반환한다. 이것은 다른 컴포넌트로 갈 때마다 일부 side effect를 수행하려는 경우에 유용할 수 있다.
useRoutes Hooks는 Routes와 기능적으로 동일하지만 Route 요소 대신 JavaScript 객체를 사용하여 경로를 정의합니다. 이러한 객체는 일반 Route 요소와 동일한 속성을 갖지만 JSX가 필요하지 않습니다.
Netflix 앱에 React Router Dom 적용하기
검색 페이지
디테일 페이지
해당 모달을 클릭하면 나오는 영화에 대한 상세 페이지
페이지를 담는 pages 폴더와 그안에 해당 페이지들을 넣어주자. 이 애플리케이션에서는 메인 페이지와 상세 페이지만 만들자.
현재 App.js가 메인 페이지를 위한 컴포넌트로 이루어져 있다.
이 부분을 다 MainPage/index.js로 옮겨주고, 이제 App.js는 라우팅을 위한 파일로 바꿔주자.
일단 DetailPage, MainPage, SearchPage의 index.js에 rfc를 입력해서 구조를 만들어주고, class의 이름을 변경해주자.
import React from 'react'
export default function DetailPage() {
return (
<div>DetailPage</div>
)
}
이런식으로 말이다.
그리고 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에 BrowserRouter 태그로 바꿔준다.
<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')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
MainPage 내의 index.js에 원래 App.js에 있던 메인 페이지 구현 부분을 모두 옮겨야 하기 때문에 옮겨주고, 필요한 부분들은 import해온다.
<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" //ETFLIX ORIGINALS 줄임말
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>
)
}
검색 페이지 구현
search bar는 NavBar(Navigation Bar)에 있다. 그래서 NavBar에 검색 Input을 생성해야 한다. 그리고 스타일링도 해주자.
<Nav.js>
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>
)
}
<Nav.css>
.nav {
position: fixed;
top: 0;
width: 100%;
height: 30px;
z-index: 1;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
transition-timing-function: ease-in;
transition: all 0.5s;
}
.nav__black {
background-color: #111;
}
.nav__logo {
position: fixed;
left: 40px;
width: 80px;
object-fit: contain;
}
.nav__avatar {
position: fixed;
right: 40px;
width: 30px;
object-fit: contain;
}
/*이부분을 추가해주면 된다.*/
.nav__input {
position: fixed;
left: 50%;
transform: translate(-50%, 0);
background-color: rgba(0, 0, 0, 0.464);
border-radius: 5px;
color: white;
padding: 5px;
border: none;
}
여기까지 작성해주면 다음과 같이 css가 적용되어 검색창에 스타일링이 적용되고, 키워드를 입력하면 url에 반영되는 것을 확인할 수 있다.
위의 이미지에서 보면 url이 다음과 같다.
http://localhost:3000/search?q=spiderma
저기서 'spiderma'부분이 e.target.value 즉, SearchTerm이다. 이를 SearchPage 컴포넌트에서 가져와야 한다. 가져온 SearchTerm을 이용해서 DB API에서 데이터를 받아올 수 있다.
<SearchPage/index.js>
import { useLocation } from 'react-router-dom'
export default function SearchPage() {
console.log('useLocation()',useLocation());
const useQuery =() => {
return new URLSearchParams(useLocation().search)
}
return (
<div>SearchPage</div>
)
}
위와 같이 코드를 작성해주고 이를 콘솔창에 띄워서 확인해보면, 뭘 가져오는지 이해할 수 있다.
콘솔창을 보면 search: "?=spiderma" 라고 되어있는데, 여기서 spiderma만 가져와야 한다. 이를 위해 다음과 같이 코드를 추가적으로 작성해주어야 한다.
<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>
)
}
위 처럼 작성해주고 이를 콘솔창을 통해 확인하면, 다음과 같이 필요한 SearchTerm 부분만 가져오는 것을 확인할 수 있다.
이제 이 잡아준 데이터를 통해 영화 상세 데이터를 가져와주어야 한다.
방금 처럼 spiderma 라고 쳤다가 이제 다른 영화를 검색하고 싶을때 다시 지우고 새로운 검색키워드를 입력해야 할텐데, 이렇게 새로 바뀌는 키워드에 따라서 요청을 한 번 더 보내서 새로운 영화 데이터를 가져와야한다.
<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>
)
}
이렇게 s,p,i,d,e,r,m,a,n 알파벳 하나하나 입력할 때마다 계속해서 response를 보내는 것을 확인할 수 있다.
그리고, 다음과 같이 response가 온 것을 확인해보면, spiderman에 관련된 영화가 많이 나오는 것을 확인할 수 있다. 이는 setSearchResults(request.data.results); 를 해서 담은 것이다.
검색 페이지 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가 없기 때문에, css를 다음과 같이 작성하자.
<SearchPage.css>
.searchContent {
height: 100vh;
background-color: black;
}
.search-container {
background-color: black;
width: 100%;
text-align: center;
padding: 5rem 0;
}
.no-results {
display: flex;
justify-content: center;
align-content: center;
color: #c5c5c5;
height: 100%;
padding: 8rem;
}
.movie {
flex: 1 1 auto;
display: inline-block;
padding-right: 0.5rem;
padding-bottom: 7rem;
}
.movie__column-poster {
cursor: pointer;
transition: transform 0.3s;
-webkit-transition: transform 0.3s;
}
.movie__column-poster :hover {
transform: scale(1.25);
}
.movie__poster {
width: 90%;
border-radius: 5px;
}
다음과 같은 결과를 볼 수 있다.
spiderman을 입력하면 다음과 같이 관련 영화가 뜨는 것을 확인할 수 있고, 입력한 키워드에 맞는 영화가 없으면 맞는 결과가 없다는 텍스트를 띄워주는 것을 확인할 수 있다.