[Next.js] 특정 Query Parameter 값 유지하기
요구사항
Next.js 웹앱에서 특정 쿼리 파라미터의 값을 페이지 이동 시에도 유지하고 싶다는 요청을 받았다.
예를 들어 아래처럼 Next.js 웹앱에 접근한다면
http://localhost:3000/main?experiment=someSpec
앱 내 다른 페이지로 이동해도 experiment
쿼리 파라미터의 값은 유지하고 싶다는 것이다.
http://localhost:3000/anotherPage?experiment=someSpec
간단한 방법
간단하게 생각할 수 있는 방법으로는 next/router 의 useRouter()
와 next/link 의 <Link />
를 한 번 래핑해서 사용하는 것이 있다.
// `useRouter()` 래핑 예시
const persistKeys = ["experiment"];
const useMyRouter = () => {
const router = useRouter();
return {
push: ({ query, ...props }) => {
const queryWithPersistKeys = persistKeys.reduce(
(acc, key) => ({
...acc,
[key]: router.query[key],
}),
{ ...query }
);
router.push({ query: queryWithPersistKeys, ...props });
},
// ...
};
};
// `<Link />` 래핑 예시
const persistKeys = ["experiment"];
const MyLink = ({ href, ...props }) => {
const router = useRouter();
const [pathname, queryString] = href.split("?");
const searchParams = new URLSearchParams(query);
persistKeys.forEach((key) => {
searchParams.set(key, router.query[key]);
});
const hrefWithQuery =
searchParams.size > 0 ? `${pathname}?${searchParams}` : pathname;
return <Link href={hrefWithQuery} {...props} />;
};
useRouter()
대신 useMyRouter()
를, <Link />
대신에 <MyLink />
를 사용한다면 페이지 이동 시에도 원하는 쿼리 파라미터의 값을 유지시킬 수 있을 것이다.
"간단한 방법"의 이슈
하지만 이 방식에는 몇 가지 이슈가 있다.
- 이미
useRouter()
와<Link />
를 래핑하지 않은 채로 많은 곳에서 사용하고 있다면, 일일이 찾아 바꾸기에는 너무 번거롭다. - 외부 라이브러리 중
useRouter()
,<Link />
를 내부적으로 사용하고 있는 라이브러리가 있다면, 그들에 대해서도 라이브러리 수정 없이 대응할 수 있어야 한다. - 혹시라도 팀원 중 한명이 래핑한
useMyRouter()
,<MyLink />
가 아니라 실수로useRouter()
,<Link />
를 사용한다면, 래핑한 훅/컴포넌트의 로직은 무용지물이 된다.
더 좋은 방법
그렇다면 이 약점들을 극복하기 위한 방법은 무엇이 있을까? 바로 next/router 의 이벤트 리스너를 사용하는 것이다. next/router 에는 아래와 같은 이벤트 리스너들이 있다.
routeChangeStart(url, { shallow })
- Fires when a route starts to changerouteChangeComplete(url, { shallow })
- Fires when a route changed completelyrouteChangeError(err, url, { shallow })
- Fires when there's an error when changing routes, or a route load is cancelled
err.cancelled
- Indicates if the navigation was cancelledbeforeHistoryChange(url, { shallow })
- Fires before changing the browser's historyhashChangeStart(url, { shallow })
- Fires when the hash will change but not the pagehashChangeComplete(url, { shallow })
- Fires when the hash has changed but not the page
이 중 routeChangeStart
가 우리가 하고자 하는 일에 적합한 리스너이다.
문서에는 자세히 적혀있지 않지만 routeChangeStart
내에서 페이지 이동 함수 (예를 들면 router.push()
) 를 실행하면 기존 페이지 이동은 취소되고 새로 실행된 페이지 이동 함수의 페이지 이동만 적용된다.
즉 페이지 이동 직전에 현재 유지하고 싶은 쿼리 파라미터가 있는지 확인하고, 있다면 기존 이동 대신 해당 쿼리 파라미터를 포함한 새로운 이동을 실행하면 되는 것이다. (없다면 기존의 페이지 이동이 실행되도록 그냥 넘어간다.)
// routeChangeStart 리스너 사용 예시
const persistKeys = ["experiment"];
const usePersistQueryParameters = () => {
const router = useRouter();
useEffect(() => {
const routeChangeStart = (url) => {
const persistQuery = persistKeys.reduce((acc, name) => {
const value = router.query[name];
if (value && !url.match(`[?&]${name}=`)) {
acc.push([name, value]);
}
return acc;
}, []);
if (persistQuery.length > 0) {
const [pathname, query] = url.split("?");
const searchParams = new URLSearchParams(query);
for (const [key, value] of persistQuery) {
searchParams.set(key, value);
}
router.push({
pathname,
query: searchParams.toString(),
});
}
};
router.events.on("routeChangeStart", routeChangeStart);
return () => {
router.events.off("routeChangeStart", routeChangeStart);
};
}, [router]);
};
이렇게 하면 useMyRouter()
, <MyLink />
처럼 기존 함수/컴포넌트들을 래핑하지 않고도, 페이지 이동 시에도 원하는 쿼리 파라미터의 값을 유지시킬 수 있다.
"더 좋은 방법"의 이슈
적용해서 사용해봤을 때 특별한 이슈는 없었다.
다만 routeChangeStart
에서 페이지 이동을 취소하고 새로운 페이지 이동을 실행하는 것이 성능 상의 이슈를 불러올 수도 있지 않을까 하는 걱정은 있다.
하지만 "페이지 이동 시에도 원하는 쿼리 파라미터의 값 유지" 기능은 실험적인 기능 사용 등 제한된 목적에 대해서만 사용할 계획이므로, 설령 성능 상에 이슈가 있다고 해도 큰 문제는 되지 않을 것이다.
당연히 (실험적인 기능 등을 사용할 일이 없는) 일반 사용자에게는 아무런 영향이 없다.