url 경로가 변경되면 시각적 콘텐츠도 변경되어야한다.
JavaScript로 즉각적인 반응형 사용자 경험을 제공할 수 있기 때문에 JavaScript로 해보겠습니다!
다중 페이지 리액트 애플리케이션을 만드는 데 도움이 되는 패키지가 있는데 바로 리액트 라우터라고 합니다. 라우팅 기능이라 할 수 있습니다.
npm install react-router-dom@5
현재 리액트 라우터는 버전 6까지 나와있지만, 버전 5를 우선 사용하고 6로 업데이트하는 방법까지 알아보겠습니다!
리액트 라우터를 사용한다는 것은 페이지를 다른 경로를 처리하고 다른 경로에 대해 컴포넌트를 로드하게 되는 걸 말합니다.
our-domain.com/ => Component A
out-domain.com/products => Component B
먼저, Pages 폴더를 만들고 Welcome.js와 Products.js를 만들어 도메인을 달리했을 때 각 컴포넌트들이 보여지도록 만들어 보겠습니다.
import { Route } from "react-router-dom";
import Welcome from "./pages/Welcome";
import Products from "./pages/Products";
function App() {
return (
<div>
<Route path="/welcome">
<Welcome />
</Route>
<Route path="/products">
<Products />
</Route>
</div>
);
}
Route를 import하고 path를 정해서 도메인의 주소로 들어가게끔 합니다. 그리고 Route 태그 안에 들어가고자할 컴포넌트를 넣습니다.
다음으로 App.js 의 최상위 Route인 index.js에서 작업을 실행해줍니다.
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
BrowserRouter를 import하고 App컴포넌트를 BrowserRouter 컴포넌트로 감싸주면 이제 실행이 됩니다!
예상한대로 도메인에 products를 붙이면 Products 컴포넌트가 나오는 것이 확인이 됩니다.
현재로서는 수동으로 페이지 주소를 입력해야 이동이 가능합니다. 링크를 페이지에 넣고 다음 곳으로 이동할 수 있는 간편한 페이지 링크를 추가하는 방법을 알아보겠습니다.
먼저, Components 폴더를 생성 후 MainHeader.js를 만들어 클릭 했을 떄 url이 바뀌고 다른 컴포넌트로 이동을 해보겠습니다!
import { Link } from "react-router-dom";
const MainHeader = () => {
return (
<header>
<nav>
<ul>
<li>
<Link to="/welcome">Welcome</Link>
</li>
<li>
<Link to="/products">Products</Link>
</li>
</ul>
</nav>
</header>
);
};
export default MainHeader;
링크를 이동할 떄 보통 a태그를 이용합니다. 하지만 react에서는 Link를 이용해서 링크로 이동합니다! a태그를 사용하면 요청이 새로 보내지게돼서 매끄럽지 못한 부분이 있습니다. 하지만 Link태그를 할 경우 사용자에게 좀 더 매끄러운 화면 전환이 가능합니다.
import MainHeader from "./components/MainHeader";
function App() {
return (
<div>
<MainHeader />
<main>
<Route path="/welcome">
<Welcome />
</Route>
<Route path="/products">
<Products />
</Route>
</main>
</div>
);
}
MainHeader를 import 해줍니다!
index.css와 MainHeader.module.css를 생성하고 스타일을 넣어보겠습니다!
현재 페이지에서는 링크를 클릭할 때 어떤 링크를 클릭했는지 알 수가 없습니다. 그 기능을 하기 위해서는 NavLink를 이용하면 됩니다!
import { NavLink } from "react-router-dom";
import classes from "./MainHeader.module.css";
const MainHeader = () => {
return (
<header className={classes.header}>
<nav>
<ul>
<li>
<NavLink activeClassName={classes.active} to="/welcome">
Welcome
</NavLink>
</li>
<li>
<NavLink activeClassName={classes.active} to="/products">
Products
</NavLink>
</li>
</ul>
</nav>
</header>
);
};
export default MainHeader;
앞 서 있었던 Link 태그 대신, NavLink를 import하고 Prop로 activeClassName을 적은 후
.header a.active {
color: #95bcf0;
padding-bottom: 0.25rem;
border-bottom: 4px solid #95bcf0;
}
a.active 활성화 코드를 집어넣고 {classes.active}를 props에 넣으면 됩니다!
이렇게 되면 Products 클릭 시 선택이 되는 것을 볼 수 있습니다!
Products.js에서 제품들이 보였으면 좋겠습니다!
<h1>The Products Page</h1>
<ul>
<li>A book</li>
<li>A Carpet</li>
<li>An Online Course</li>
</ul>
에 제품들을 추가하고 라우터하기위해서 App.js로 가보겠습니다!
<Route path="/product-detail/:productId">
<ProductDetail />
</Route>
제품들은 A book/ A Carpet/ An Online Course가 있습니다. 각 각에 맞는 detail이 와야하기 때문에 path를 /product-detail 이라고만 적으면 문제가 됩니다!
따라서 동적으로 url을 처리하기 위해서 /: 을 사용합시다! 이렇게 되면 /product-detail/ 뒤 부분에 아무 글씨나 적어도 detail page로 이동하는 것을 볼 수 있습니다!
이 동적 경로 기능을 실제로 활용하려면 로드된 컴포넌트 내부에 입려된 구체적인 값에 엑세스해야 합니다.
import { useParams } from "react-router-dom";
const ProductDetail = () => {
const params = useParams();
console.log(params.productId);
return (
<section>
<h1>Product Detail</h1>
<p>{params.productId}</p>
</section>
);
};
export default ProductDetail;
useParams를 import하고
const params = useParams(); 입력 후
params.productId를 출력하면 url에서 :/productId 부분에 입력한 값이 출력되게 됩니다! params. 뒤 productId는 App.js에서 :/productId와 같은 문구를 입력해야합니다!
<li>
<Link to="products/p1">A book</Link>
</li>
<li>
<Link to="products/p2">A Carpet</Link>
</li>
<li>
<Link to="products/p3">An Online Course</Link>
</li>
각각 Link to로 연결을 시켜주면 App.js에
path에 /products 라는 글씨가 두 곳이 Router 됩니다. 이것은 리액트의 작동 방식에 문제가 있습니다. 이렇게 되면 화면에 Router되는 것이
<Route path="/products" exact>
<Products />
</Route>
<Route path="/products/:productId">
<ProductDetail />
</Route>
이 두 화면이 보여지게 될 것입니다.
이 것을 해결하기 위해서는 Switch를 import 해주면 됩니다!
import { Route, Switch } from "react-router-dom";
<Switch>
<Route path="/welcome">
<Welcome />
</Route>
<Route path="/products" exact>
<Products />
</Route>
<Route path="/products/:productId">
<ProductDetail />
</Route>
</Switch>
해당하는 Route들을 Switch 문으로 감싸주면 일치한 url 항목의 첫번째 Router만 보여지게 됩니다. 이렇게 되면 detail페이지가 아닌 products 페이지만 보여지게 됩니다. 이럴 떄 뒷 Router인 detail페이지가 보여줘야하고
url과 완전히 일치하는 항목만 보여줘야 하기 때문에 /products 라우터에 exact prop를 적어주면 완전히 일치한 url일 때만 보여지도록 설정이 가능합니다!
따라서 detail Router가 보여지게 됩니다!
중첩 경로를 적용해보겠습니다! 예를 들어, Welcome Page 안에서 또 다른 경로를 적용하고 싶다고 가정을 해보겠습니다!
import { Route } from "react-router-dom";
const Welcome = () => {
return (
<section>
<h1>The Welcome Page</h1>
<Route path="/welcome/new-user">
<p>Welcome. new user!</p>
</Route>
</section>
);
};
export default Welcome;
Welcome.js에서 Route를 import하고
Route path를 Welcome.js에서 적어주면 되는데 이 때, 주의할 점은 Welcome component의 Router가 활성화 됐을 때만, 적용이 된다는 점을 주의해야합니다. 따라서 /welcome이 들어가야합니다!
현재 url에 아무것도 입력하지 않으면 빈 페이지가 나옵니다. 이 것을 바꾸어서 처음 시작화면에 welcome page가 나오도록 리디렉션 해보겠습니다!
import { Route, Switch, Redirect } from "react-router-dom";
Redirect를 import 해주고
<Route path="/" exact>
<Redirect to="/welcome" />
</Route>
Route를 추가하되, path를 /로 해주고 중요한 포인트는 exact를 꼭 해줘야 합니다! 안해주면 다른 Route들도 /를 가지고 있기 떄문에 루프에 빠지게 됩니다. 그리고 Redirect를 하고 to로 /welcome 으로 연결해주면 완성!
저번 까지 배운 내용을 연습해보겠습니다!
import { Route, Switch } from "react-router-dom";
return
<Switch>
<Route></Route>
<Route></Route>
<Route></Route>
</Switch>
준비를 해주고
pages폴더에 필요한 page들을
AllQuotes.js/ NewQuote.js/ QuoteDetail.js 만들어줍니다.
import AllQuotes from "./pages/AllQuotes";
import QuoteDetail from "./pages/QuoteDetail";
import NewQuote from "./pages/NewQuote";
<Switch>
<Route path="/qoutes" exact>
<AllQuotes />
</Route>
<Route path="/qoutes/:qouteId">
<QuoteDetail />
</Route>
<Route path="/new-quote">
<NewQuote />
</Route>
</Switch>
그리고 각 component를 import 하고 path를 입력해줍니다. 여기서 포인트는 exact를 꼭 써줘야합니다!
import { BrowserRouter } from "react-router-dom";
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
라우터를 사용하기 위해 App을 BrowserRouter로 감싸주면 끝!
이제 quotes page로 redirect를 해겠습니다!
import { Route, Switch, Redirect } from "react-router-dom";
<Route path="/" exact>
<Redirect to="/quotes" />
</Route>
그리고 QuoteDtail에 입력한 /:quoteId url을 추출해보겠습니다.
import { useParams } from "react-router-dom";
const params = useParams();
<Fragment>
<h1>Quote Detail Page</h1>
<p>{params.quoteId}</p>
</Fragment>
다음으로 중첩 라우터를 이용하여 QuoteDetail 안에서 또 다른 라우터를 사용해보겠습니다!
예를 들어 /Quotes/quoteId/comments 했을 때 만 comments.js의 내용들이 보여지도록 하겠습니다.
import { Route } from "react-router-dom";
<Route path={`/quotes/${params.quoteId}/comments`}>
<Comments />
</Route>
Route를 import 해주고 여기서 중요한 점은
paras.quoteId가 유동적으로 바뀌기 떄문에 ``을 이용하고 ${params.quoteId}를 이용하면 동적으로 받을 수 있습니다!
이상 중첩라우터가 어떻게 작동하는지 확인했습니다.
QuiteItem에 a태그로 View Fullscreen이라는 문구가 있습니다!
이 것을 이용하여 페이지를 이동해보겠습니다.
App.js에서
라우트 경로가 :quoteId 부분에 동적이기 떄문에
import { Link } from "react-router-dom";
<Link className="btn" to={`/quotes/${props.id}`}>
View Fullscreen
</Link>
to={
/quotes/${props.id}
}> 백틱을 이용하고 ${props.id}를 이용해 동적으로 받는 것이 포인트!
다음으로 QuoteDetail에도 더미데이터를 가져와서 넣어보겠습니다!
import HighlightedQuote from "../components/quotes/HighlightedQuote";
const DUMMY_QUOTES = [
{ id: "q1", author: "Max", text: "Learning React is fun!" },
{ id: "q2", author: "Maximilian", text: "Learning React is great!" },
];
const quote = DUMMY_QUOTES.find((quote) => quote.id === params.quoteId);
if (!quote) {
return <p>No quote found!</p>;
}
<HighlightedQuote text={quote.text} author={quote.author} />
더미데이터를 삽입하고 HighlightedQuote 컴포넌트를 가져와서
더미데이터에서 id값이 일치하는 것을 찾아서 text와 author를 보여주되, 만약 quote가 아니면 No quote found! 라는 인용문을 보여줄 것입니다!
전혀 관계 없는 url을 입력하면 빈페이지가 나오는데 404페이지로 보이게 하겠습니다!
const NotFound = () => {
return (
<div className="centered">
<p>Page not found!</p>
</div>
);
};
export default NotFound;
NotFound.js 컴포넌트를 Page폴더에 만들어주고
<Route path="*">
<NotFound />
<Route>
App.js에서 제일 끝 부분의 Route에 path로 *을 입력하는 것이 포인트입니다! 이렇게 되면 현재 App.js 라우터 경로와 일치하지 않을 떄 렌더링 됩니다!
import { useHistory } from "react-router-dom";
const history = useHistory();
hstory.push("quotes");
quote가 제출될 떄 addQuoteHandler 메서드를안에서 useHistory를 통해 페이지 전환이 가능합니다.
useHistory는 방문한 페이지를 기록한다는 의미입니다.
제출을하면 /quote 페이지로 이동하는 것을 볼 수 있습니다!
import { useState } from "react";
import { Prompt } from "react-router-dom";
const QuoteForm = (props) => {
const [isEntering, setIsEntering] = useState(false);
}
const finishEnteringHandler = () => {
setIsEntering(false);
};
const formFocusedHandler = () => {
setIsEntering(true);
};
<Prompt
when={isEntering}
message={(location) => "Are you sure you want to leave?"}
/>
<form
onFocus={formFocusedHandler}
>
<button onClick={finishEnteringHandler} className="btn">
Add Quote
</button>
먼저, useState를 이용하여 기본값을 false로 설정한 후, onFocus props를 이용해 true로 세팅을 바꿔줍니다. 그리고 Prompt를 import하여 isEntering이 true일 떄 메시지를 표시하도록 합니다.
그리고 Add Quote 버튼을 클릭하였을 때는 onClick props를 이용해 다시 false로 바꿔주면 동작합니다!
import { useHistory } from "react-router-dom";
const QuoteList = (props) => {
const history = useHistory();
const changeSortingHandler = () => {
history.push("/quotes?sort=asc");
};
<div className={classes.sorting}>
<button onClick={changeSortingHandler}>Sort Ascending</button>
</div>
클릭을 하면 url이 바뀌는 것을 볼 수 있습니다.
다음 단계는 정렬과 버튼이름을 바꾸는 작업을 해보겠습니다.
import {useLocation } from "react-router-dom";
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const isSortingAscending = queryParams.get("sort") === "asc";
const changeSortingHandler = () => {
history.push("/quotes?sort=" + (isSortingAscending ? "desc" : "asc"));
<button onClick={changeSortingHandler}>
Sort {isSortingAscending ? "Descending" : "Ascending"}
</button>
};
useLocation을 import 하고
URLSearchParams 기본 메서드를 이용해 location의 search에 들어가 key값을 불러오고 그 키값이 asc와 일치한 것을 isSortingAscending에 저장하고
history.push를 활용해 도메인 값을 desc와 asc로 선택이 가능하게하고
마지막 button클릭 시에도 Descending과 Asscending으로 보여지게 선택합니다!
버튼을 눌렀을 떄 url 주소가 바뀌는 것과 버튼 글자가 바뀌는 것이 확인이 됩니다.
다음으로 실제 목록을 정렬해 보겠습니다.
QuoteList가 렌더링 되기 전에 sortQuotes가 렌더링이 되면 됩니다!
const sortQuotes = (quotes, ascending) => {
return quotes.sort((quoteA, quoteB) => {
if (ascending) {
return quoteA.id > quoteB.id ? 1 : -1;
} else {
return quoteA.id < quoteB.id ? 1 : -1;
}
});
};
문구를 QuoteList가 렌더링 되기 전에 추가해줍니다!
const sortedQuotes = sortQuotes(props.quotes, isSortingAscending);
<ul className={classes.list}>
{sortedQuotes.map((quote) => (
<QuoteItem
key={quote.id}
id={quote.id}
author={quote.author}
text={quote.text}
/>
))}
이렇게 추가해주면
내용물도 바뀌는 것을 볼 수 있습니다!