react는 SPA(Single page application)이며 따라서 하나의 프로젝트에 하나의 html만 존재합니다.
그러나 우리는 브라우저 경로에 따른 다른 페이지를 로딩하는 방식을 오래전부터 사용해 왔습니다.
리액트에서도 이처럼 라우팅을 하는게 가능합니다.
그러나 위에서 말씀드렸듯이 단일 페이지로 구성되어 있기 때문에 경로가 변경된다고 페이지가 갈아끼워지는게 아닌, 컴포넌트가 갈아끼워지게 됩니다.
그러면 지금부터 라우팅에 대해 글을 작성해보겠습니다.
먼저 리액트는 프레임워크가 아닌 라이브러리이므로 vue나 angular처럼 내장 라우팅을 지원하지 않습니다.
따라서 router 라이브러리를 별도로 설치해 사용해야 하죠.
이 라이브러리의 이름은 react-router-dom이라고 합니다.
설치 명령어는 아래와 같습니다.
$npm i react-router-dom
라우터의 사용법은 아래와 같습니다.
//Router.js
import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import PageOne from "./components/Page01";
import PageTwo from "./components/Page02";
import PageThree from "./components/Page03";
import App from "./App";
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/pageOne" element={<PageOne />} />
<Route path="/pageTwo" element={<PageTwo />} />
<Route path="/pageThree" element={<PageThree />} />
</Routes>
</BrowserRouter>
);
};
export default Router;
먼저 컴포넌트 변경을 확인하기 위해 page01,02,03을 생성하였습니다.
그리고 BrowserRouter > Routes > Route 순으로 감싸주며
Route path를 통해 이동한 브라우저 주소와 해당 주소에 어떤 컴포넌트를 불러올지 element 속성으로 설정합니다.
//App.js
import React from "react";
import { Link, useNavigate } from "react-router-dom";
const App = () => {
const navigate = useNavigate();
const handleNavigate = () => {
navigate("/pageThree");
};
return (
<div>
<p>App component입니다.</p>
<button>
<Link to="/pageOne">Go to pageOne</Link>
</button>
<button>
<Link to="/pageTwo">Go to pageTwo</Link>
</button>
<button onClick={handleNavigate}>Go to pageThree</button>
</div>
);
};
export default App;
//PageOne.js
import React from "react";
import { Link } from "react-router-dom";
const PageOne = () => {
return (
<div>
<p>page01입니다. 환영합니다.</p>
<button>
<Link to="/">홈으로 이동</Link>
</button>
</div>
);
};
export default PageOne;
//PageTwo.js
import React from "react";
import { Link } from "react-router-dom";
const PageTwo = () => {
return (
<div>
<p>page02입니다. 환영합니다.</p>
<button>
<Link to="/">홈으로 이동</Link>
</button>
</div>
);
};
export default PageTwo;
//PageOne.js
import React from "react";
import { Link } from "react-router-dom";
const PageThree = () => {
return (
<div>
<p>page03입니다. 환영합니다.</p>
<button>
<Link to="/">홈으로 이동</Link>
</button>
</div>
);
};
export default PageThree;
그리고 App ~ Page 1~3을 위와 같이 작성해주도록 합니다.
결과처럼 페이지를 이동하면 주소창에 각각 pageOne/Two/Three로 변경되며 컴포넌트가 교체됩니다.
여기서 App.js를 자세히 보면 PageOne/Two는 react-router-dom 라이브러리의 내장 컴포넌트인 Link 컴포넌트를 사용하였으며
PageThree는 useNavigate hook을 사용하였습니다.
이 둘은 설정한 경로로 이동해주는 역할을 한다는 공통점이 있습니다.
그런데 왜 같은 기능을 두 개나 만들어 두었을까요??
먼저 Link 컴포넌트의 경우 묻지도 따지지도 않고 경로를 이동시켜버립니다.
컴포넌트이기 때문에 다른 함수 안에 선언될 수 없고, 함수로 실행시킬수도 없게되는 것이죠.
따라서 로그인이나 회원가입 등 유효성 검증이 필요한 경우에는 곧바로 컴포넌트를 교체하는 Link는 적합하지 않습니다.
반면에 useNavigate의 경우 함수 안에서 사용이 가능합니다.
즉, 로그인/회원가입시 이런저런 유효성 검사를 따진 후에 이상이 없다면 컴포넌트 교체를 해줄 수 있는 것이죠.
이러한 차이 때문에 Link와 useNavigate 훅이 분리되었습니다.
우리는 위에서 라우팅을 통해 브라우저 경로를 이동 및 화면 이동에 대해 알아보았습니다.
그런데 이러한 방식에는 한가지 불편한점이 존재합니다.
예를 들어 온라인 쇼핑몰을 개발하는 중이라고 가정해봅시다.
쇼핑몰 안에는 다양한 상품이 판매되고 있으며 보통 각각의 상품을 구별하기 위해 서로 다른 Id를 부여받습니다.
그리고 이 상품의 상세페이지에 접근할 때 아래와 같은 형식으로 브라우저 주소가 변경된다고 가정해봅시다.
localhost:3000/productId/1
localhost:3000/productId/2
localhost:3000/productId/3
localhost:3000/productId/4
...
localhost:3000/productId/100000
localhost:3000/productId/100001
localhost:3000/productId/100002
이렇듯 굉장히 많은 상품이 존재하고 각 상품마다 부여받는 브라우저 주소가 Id값에 따라 변할 때
위에서 사용했던 방식으로 라우팅을 구성한다면 우리는 10만개가 넘는 라우팅 주소를 일일이 세팅해야 합니다.
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/productId/1" element={<Product1 />} />
<Route path="/productId/2" element={<Product2 />} />
<Route path="/productId/3" element={<Product3 />} />
<Route path="/productId/4" element={<Product4 />} />
...
<Route path="/productId/1" element={<Product100000 />} />
<Route path="/productId/1" element={<Product100001 />} />
<Route path="/productId/1" element={<Product100002 />} />
</Routes>
</BrowserRouter>
);
};
바로 이렇게 말이죠!
할 수야 있겠지만 팔이 빠져 죽거나, 안압이 올라서 죽거나, 지루해 죽거나 할 것 같습니다.
이러한 약점을 보완하고자 나온 것이 바로 동적 라우팅입니다.
아래 예시를 보면서 차근차근 이해해볼까요??
//Router.js
import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Product from "./components/Product";
import App from "./App";
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/product/:id" element={<Product />} />
</Routes>
</BrowserRouter>
);
};
export default Router;
//App.js
import React from "react";
import { Link, useNavigate } from "react-router-dom";
const App = () => {
return (
<div>
<p>App component입니다.</p>
{PRODUCT_LIST.map((prd) => {
return (
<div key={prd.id}>
<Link to={`/product/${prd.id}`}>{prd.text}</Link>
</div>
);
})}
</div>
);
};
export default App;
const PRODUCT_LIST = [
{ id: 1, text: "go to product/1" },
{ id: 2, text: "go to product/2" },
{ id: 3, text: "go to product/3" },
{ id: 4, text: "go to product/4" },
{ id: 5, text: "go to product/5" },
{ id: 6, text: "go to product/6" },
{ id: 7, text: "go to product/7" },
{ id: 8, text: "go to product/8" },
{ id: 9, text: "go to product/9" },
{ id: 10, text: "go to product/10" },
{ id: 11, text: "go to product/11" },
{ id: 12, text: "go to product/12" },
{ id: 13, text: "go to product/13" },
];
//Product.js
import React from "react";
import { Link, useParams } from "react-router-dom";
const Product = () => {
const params = useParams();
const prdId = params.id;
return (
<div>
<p>{`/product/${prdId}`}입니다. 환영합니다.</p>
<button>
<Link to="/">홈으로 이동</Link>
</button>
</div>
);
};
export default Product;
위 로직이 동적 라우팅을 구현한 예시입니다.
<Route path="/product/:id" element={} />
먼저 Router.js에서 해당 로직을 통해 프로덕트의 Id에 따른 페이지 이동을 설정해줍니다.
아이디에 따라 /product/1, /product/2 등으로 브라우저 경로가 설정됩니다.
이 경로는 바로 App.js에서 구현되었습니다.
상품 백엔드 API 대신 가상의 상품 13개를 상수데이터로 선언하고 map 함수를 통해 각 상품으로 이동하는 로직입니다.
각각의 링크를 누르게 되면 상수데이터에 선언된 id값이 브라우저의 엔드포인트로 설정되며
Router.js에서 이를 이용해 브라우저 경로를 사용할 수 있습니다.
마지막으로 Product.js에서 react-router-dom의 내장 hook인 useParams()를 사용합니다.
params에는 각 상품마다 {id:${id}} << 이와 같은 객체 형태의 데이터가 들어있습니다.
prdId로 각 상품의 id값을 추출해 return문 안에서 활용하였습니다.
이렇게 동적 라우팅까지 구현해보았습니다.
이어서 query String를 포스팅 하겠습니다.
쿼리스트링은 브라우저 주소에 key=value 형태의 데이터를 함께 넘겨 보다 디테일한 정보를 구현할 수 있습니다.
형태는 아래와 같습니다.
도메인네임 후반 '?'가 쿼리스트링을 시작하는 구분자입니다.
그리고 color=blue, sort=newest와 같이 key=value 형태로 조건을 지정합니다.
이 둘 사이에는 '&'로 서로를 구분해줍니다.
import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import List from "./components/List";
import App from "./App";
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/list" element={<List />} />
</Routes>
</BrowserRouter>
);
};
export default Router;
import React from "react";
import { Link } from "react-router-dom";
const App = () => {
return (
<div>
<p>App component입니다.</p>
<Link to="/list?sort=newest&sort=popular&offset=0&limit=20">
List페이지로 이동
</Link>
</div>
);
};
export default App;
import React from "react";
const List = () => {
return (
<section>
<h1>Welcome to list Page 😃</h1>
</section>
);
};
export default List;
링크를 클릭하면
App Component에서 List페이지로 이동하는 링크가 있습니다.
링크에는 sort, offset, limit등의 쿼리 파라미터가 들어있습니다.
useLocation
//List.js
import React from "react";
import { useLocation } from "react-router-dom";
const List = () => {
const location = useLocation();
console.log(location);
return (
<section>
<h1>Welcome to list Page 😃</h1>
</section>
);
};
export default List;
먼저 useLocation입니다.
뭐하는 친구인지 콘솔을 찍어봅시다.
여러가지 데이터가 나오네요 ^^
여기서 의미있는 데이터는 search인 것으로 보입니다.
그러나 search를 보면 하나의 스트링으로 모든 조건이 걸려있는 것을 볼 수 있습니다.
각각의 데이터를 사용하려면 &로 split을 하고 key,value를 또 각각 구하는 등의 번거로움이 있네요.
이 때 사용하면 좋은 것인 useSearchParams훅입니다.
useSearchParams
사용법
const [searchParams, setSearchParams] = useSearchParams();
형태가 useState와 유사하죠??
그렇지만 조금 다릅니다.
뭔가 나오기는 했는데 의미있는 데이터는 딱히 보이지 않네요 😅
우리는 이 녀석에게 들어있는 내부 메소드들을 활용해야 합니다.
이 중 유의미한 데이터를 뽑아내는 것들은 무엇이 있는지 살펴봅시다.
searchParams.get(key)
=> 특정한 key-value를 가져오는 메소드입니다.
searchParams.get('offset') = 20
searchParams.getAll(key)
=> 동일한 키 값이 복수개일 때 get 메소드를 활용하면 맨 처음 값만 리턴하지만 getAll은 배열 형태로 전부 리턴합니다.
searchParams.getAll('sort') = ['newest', 'popular']
searchParams.toString()
=> useLocation.search와 같이 쿼리 스트링을 그냥 string 형태로 뽑아냅니다.
searchParams.toString() = "?sort=newest&sort=popular&offset=0&limit=20"
searchParams.set(key, value)
=> 인자로 전달한 key 값에 새로운 value를 설정합니다.
=> sort ['newest', 'popular']같이 복수개의 값을 가진 key일 경우 새로 설정하는 value만 저장합니다.
searchParams.set('sort', 'popular')
결과 : "?sort=clear&offset=0&limit=20"
searchParams.append(key, value)
=> 기존 쿼리 스트링에 새로운 key=value를 추가합니다.
searchParams.append('sort', 'clear)
결과 : "?sort=clear&sort=popular&sort=clear&offset=0&limit=20"
//List.js
import React from "react";
import { useLocation, useSearchParams } from "react-router-dom";
const List = () => {
const location = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
console.log("useLocation", location.search);
console.log("searchParams.get", searchParams.get("limit"));
console.log("searchParams.getAll", searchParams.getAll("sort"));
console.log("searchParams.toString()", searchParams.toString());
searchParams.append("sort", "clear");
console.log("searchParams.append", searchParams.toString());
searchParams.set("sort", "clear");
console.log("searchParams.set", searchParams.getAll("sort"));
return (
<section>
<h1>Welcome to list Page 😃</h1>
</section>
);
};
export default List;
마치며
react-router-dom의 전반적인 내용을 다루어 보았습니다.
정리하며 저도 배운게 참 많았던 것 같습니다.
많이 부족한 포스팅이지만 읽으시는 분들께 조금이나마 도움이 되었으면 합니다.
오류가 있는 부분은 매너있게 지적해주시면 감사하겠습니다.
개발하시는 모든 분들 화이팅!