Routing이란?
라우팅(Routing)은 네트워크 상에서 경로를 선택하는 프로세스 또는 그 행위를 일컫는 말이다.
SPA에서 Routing을 사용하는 이유
SPA에서 <a>
태그를 사용할 경우 페이지가 새로고침되며 화면 깜빡임이 발생한다. 다시 말해서 페이지 전체가 새롭게 로딩되는 것인데, 이는 사용자 경험 측면에서 굉장히 좋지 않다.
그렇다면 모든 페이지를 초기 로딩 시점에 전부 렌더링하고, JS 코드로 페이지 전환만 시켜주면 되는거 아닌가? 라는 의문이 생길 수 있는데, 이 경우 생기는 다양한 문제점들이 있다.
1. 초기 로딩 시간 증가
- 페이지가 많아질수록 초기 로딩 시간이 길어진다. 또한, 모든 페이지가 로딩될 때까지 기다려야하므로, 사용자 경험 측면에서 좋지 않다.
2. 메모리의 사용량 증가
- 클라이언트 측 메모리 사용량이 크게 증가한다. 이는 곧 브라우저 성능 저하 및 충돌을 일으킬 수 있다.
3. 특정 페이지 즐겨찾기 / 공유 불가
- URL이 고정되어 변하지 않기 때문에 웹앱에서 특정 페이지를 즐겨찾기에 추가하거나 공유할 수 없다.
4. 뒤로가기 및 앞으로가기 기능 제한
- 웹페이지 내에서 URL이 변경되지 않으므로, 페이지 이동이 인식되지 않아 해당 기능을 이용할 수 없다.
5. 복잡한 상태관리
- 각 페이지에서 필요로 하는 상태 관리가 복잡해진다. 페이지 전환 시의 모든 상태를 고려해야 한다.
다양한 문제점이 있겠으나, 결국 사용자 경험과 개발 경험 측면에서 사용성이 저하된다.
React-Router-DOM
위의 문제들을 해결하기 위해 리액트 프로젝트에선 React-Router-DOM을 사용한다.
React-Router-DOM의 라우팅 기능을 이용해 다양한 페이지로 전환 및 URL 관리를 쉽게 할 수 있다.
리액트 라우터 돔에는 크게 주 가지 종류의 라우터가 있다.
1. BrowserRouter
- HTML5의
History API
를 사용해 URL을 관리한다. - 별도 설정이 없는 경로의 경우 404 에러를 반환한다.
- 대부분의 리액트 프로젝트에서 사용된다.
import { BrowserRouter, Route, Routes } from 'react-router-dom';
const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const Users = () => <h2>Users</h2>;
function App() {
return (
<Router>
<Routes>
<Route path="/about" element={<About />} />
<Route path="/users" element={<Users />} />
<Route path="/" element={<Home />} />
</Routes>
</Router>
);
}
export default App;
2. HashRouter
- URL의 해시(#)를 이용해 경로를 관리한다. http://example.com/#/about 와 같은 경로에서
#
이후의 값은 서버가 읽지 못하고 브라우저에서만 관리된다. - 검색엔진이 경로를 파악할 수 없어, SEO 최적화에 불리하다.
- 서버 설정이 어려운 환경 또는 테스트 환경에서 사용하기 적합하다.
이번 내용에선 BrowserRouter
를 주로 사용하며, 모든 설명은 React-Router-DOM v6 기반으로 진행하려고 한다.
React-Router-DOM 사용하기
import { BrowserRouter, Routes, Route } from 'react-router-dom'
...
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/about" element={<About />} />
<Route path="/users" element={<Users />} />
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>
);
}
...
리액트 프로젝트에서 라우터를 구성할 때, 주로 위 3가지 모듈을 사용한다.
1. BrowserRouter
- HTML5의
History API
를 사용한다.- History API?
- 브라우저는 세션 히스토리를 스택 구조로 관리하며, 접근한 페이지를 history Stack에 저장한다.
pushState, replaceState
등의 History 관리 메서드를 통해, Stack을 관리한다.back(), forward(), go(), go(0)
등의 메서드를 통해 History 내에서 이동 가능하다.
- 브라우저의 세션 히스토리를 조작할 수 있는 Window 객체.
- History API?
- Provider 와 같이, 라우팅을 진행할 컴포넌트 상위에 위치시켜야 한다.
2. Route
- 현재 브라우저의
location(window.href.location)
상태에 따른 Element(**요소/컴포넌트/페이지)**를 렌더링한다. - 렌더링할 Element를 element={ } 의 형식으로 전달한다.
- /XXX 의 경로가 path 속성의 값과 같으면, 해당 경로와 일치하는 element를 렌더링한다.
3. Routes
- Route 의 상위에 존재하며, location이 변경되면 하위의 Route 들을 조회하여 현재 location 정보와 맞는 Route 를 찾아 연결한다.
페이지를 이동시키는 방법
1. Link
Link 컴포넌트는 HTML의 태그와 같이 직접적인 이동을 제어한다.
import React from 'react';
import {Link} from 'react-router-dom';
function Example(){
return (
<div>
<Link to='/'> Home </Link>
<Link to='/login'> Login </Link>
</div>
);
}
export default Example;
컴포넌트는 to 속성을 가지며, 해당 속성에 경로를 지정해 사용한다.
또한 preventScrollReset , state 등의 옵션을 통해 다양한 방식으로 사용할 수 있다,
- preventScrollReset : Link 컴포넌트를 클릭하면 스크롤 높이가 초기화되지만, preventScrollReset 을 true 로 설정하면 초기화가 진행되지 않는다.
- state: useLocation 훅과 함께 사용하는 경우, 특정 data를 넘길 수 있다.
<Link to="new-path" state={{ name: "Kim" }} /> let { state } = useLocation();
2. useNavigate
Link
컴포넌트가 아닌, 프로그래밍 방식의 내비게이션 동작을 가능하게 한다.
주로 이벤트를 처리하는 함수 내에 사용해 특정 이벤트 발생시 페이지 이동을 가능하게 하며, 버튼 클릭, 폼 제출 등의 이벤트를 통해 동적 라우팅을 처리할 수 있다.
import React from 'react';
import { useNavigate } from 'react-router-dom';
const MyComponent = () => {
const navigate = useNavigate();
const handleClick = () => {
navigate('/about'); // '/about' 경로로 이동
};
return (
<div>
<h1>Home Page</h1>
<button onClick={handleClick}>Go to About Page</button>
</div>
);
};
export default MyComponent;
useNavaigate 역시 state를 통해 data를 전달할 수 있다.
navigate('/about', { state: { from: 'homepage' } });
중첩 라우팅 (Nested Routing)
하나의 Route
안에 다른 Route
를 포함시키는 방식으로, 복잡한 구조를 효율적으로 렌더링할 수 있다.
예를 들어 /home
페이지의 하위에 Location
이라는 컴포넌트가 위치하고 있을 경우, /home/location
으로 접속하면, Home
컴포넌트 내에서 Location
이라는 컴포넌트를 함께 보여줄 수 있는 것이다.
중첩 라우팅을 구현하는 두 가지 방법이 있는데, Outlet
과 \*
을 이용하는 방법이다.
1. Outlet으로 구현하기
Outlet
은 Vue-Router
의 와 같이 동작하는데, 이는 중첩된 라우트 컴포넌트가 컴포넌트의 위치에 렌더링 된다는 것을 의미한다.
다시 말해서 상위 Route
레이아웃 내에서, 하위 Route
컴포넌트가 Outlet
의 자리에 보여진다는 것이다.
이를 이용하면, 하위 라우트(중첩된 라우트)가 여러개일 때 접속한 주소에 따라 의 위치에 여러 컴포넌트를 변경해 사용할 수 있게 된다.
import { Outlet } from 'react-router-dom';
function Home() {
return (
<div>
<div>
<h1>HomaPage</h1>
</div>
<Outlet />
</div>
);
}
위 코드 예시에서 만약 Home
컴포넌트 라우트 내에 Location
라우트가 있다면(중첩 라우트라면), Outlet
컴포넌트는 Location
컴포넌트로 대체될 것이다.
2. Outlet 없이 구현하기
중첩 라우팅을 사용하려는 라우트 경로에 \*
을 추가해 사용할 수 있다.
<Routes>
<Route path="/" element={<Main />}></Route>
<Route path="/home/*" element={<Home />}></Route>
<Route path="/about" element={<About />}></Route>
</Routes>
그리고 Home
컴포넌트 내에 중첩해서 컴포넌트를 렌더링할 위치(Outlet 이 있던 위치)에 라우트를 명시하면 된다.
function Home() {
return (
<div>
<div>
<h1>HomaPage</h1>
</div>
<Routes>
<Route path="/location" element={<Location />}></Route>
</Routes>
</div>
);
위 코드는 Outlet
을 사용한 코드와 동일하게 작동한다.
URL Data 처리하기 - useParams / useSearchParams
URL 경로에 특정 데이터를 심어, 주소를 기반으로 특정 동작을 수행할 수 있다.
이때 URL에 존재하는 데이터는 크게 URL 파라미터와 쿼리스트링으로 나눌 수 있다.
1. URL 파라미터
/page/3
등의 경로를 가지는 페이지가 있을 때, 주소 경로에서 3이라는 값은 URL 파라미터라고 부른다.
리액트 라우터 돔에서 이 파라미터를 설정하기 위해선, Route 의 경로에 /:XX
의 형식으로 데이터를 붙여주면 된다.
<Route path="/page/:id" element={<Detail />}></Route>
이렇게 보낸 URL 파라미터 데이터를 Detail
컴포넌트에서 처리하려면, useParams
훅을 사용해 추출할 수 있다.
import React from 'react';
import { useParams } from 'react-router-dom';
function Detail() {
const { id } = useParams();
return (
<div>
<p>{id} 번째 페이지입니다.</p>
</div>
)
}
export default Detail;
page/3/12
등과 같이 파라미터도 중첩해 사용할 수 있는데, 이 경우 Route 에는 /:id/:lastId
와 같이 구성한다.
useParams()
객체로 받아올 때는 두 파라미터 값을 모두 구조분해해서 받아오면 된다.
<Routes>
<Route path="/new/:id/:lastId" element={<Detail />} />
</Routes>
import React from 'react';
import { useParams } from 'react-router-dom';
function Detail() {
const { id, lastId } = useParams();
return (
<div>
<p>{id} 번째 페이지입니다.</p>
<p>lastId의 값은 {lastId}입니다.</p>
</div>
)
}
export default Detail;
2. URL 쿼리스트링
URL.에서 ?
로 시작하는 쿼리스트링의 경우 useSearchParams
훅을 이용해서 받아올 수 있다.
const [serchParams, setSearchParams] = useSearchParams();
searchParams.get('키')
의 형식으로 값을 받아올 수 있으며 쿼리스트링이 여러개라면(&), 각 키를 통해 값을 받아올 수 있다.
setSearchParams
함수를 통해 현재 페이지의 경로에서 쿼리스트링의 값만 변경할 수 있다.
쿼리스트링 값을 변경할 경우, 현재 페이지가 리렌더링 되고 자식 컴포넌트 또한 다시 렌더링이 이뤄진다.
import React from 'react';
import { useSearchParams } from 'react-router-dom';
const MyComponent = () => {
const [searchParams, setSearchParams] = useSearchParams();
const id = searchParams.get('id');
const name = searchParams.get('name');
const updateQueryString = () => {
setSearchParams({ id: '789', name: 'Smith' });
};
return (
<div>
<h2>My Component</h2>
<p>ID: {id}</p>
<p>Name: {name}</p>
<button onClick={updateQueryString}>Update Query String</button>
</div>
);
};
export default MyComponent;
이는 React-Router-DOM의 내부 구조가 Context API
를 통해 리액트의 상태관리 형태와 유사하게 URL과 관련된 상태를 관리하기 때문이며,
LocationContext
, NavigationContext
와 같은 여러 컨텍스트를 활용해 URL과 네비게이션 상태를 처리하기 때문이다.
useSearchParams
의 동작 방식을 useLocation
, useNavigate
를 통해 구현해보면 아래와 같다.
import { useContext, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
function useSearchParams() {
const location = useLocation();
const navigate = useNavigate();
const [searchParams, setSearchParamsState] = useState(new URLSearchParams(location.search));
useEffect(() => {
setSearchParamsState(new URLSearchParams(location.search));
}, [location.search]);
const setSearchParams = (params) => {
const newSearchParams = new URLSearchParams(params);
navigate({ search: newSearchParams.toString() });
};
return [searchParams, setSearchParams];
}
export default useSearchParams;
useLocation
: 현재 URL의 위치 객체를 반환한다. location.search 를 통해 쿼리스트링에 접근한다.useNavigate
: 네비게이션 함수(navigate)를 반환한다. 이를 이용해 쿼리스트링을 업데이트, 변경할 수 있다.useEffect
로 쿼리스트링 상태(값)이 변경되면 이를 감지하고, 페이지 렌더링을 다시 진행한다.URLSearchParams
객체는 JS에서 URL의 쿼리 스트링을 조회/조작하는 내장 객체이며, 메서드를 통해 쿼리스트링에 접근/변경 할 수 있다.
참고
https://velog.io/@kandy1002/React-Router-Dom-개념잡기
https://velog.io/@kingyong9169/history-API에-대해-알아보자