Next.js Router에 대한 고찰
그런데 이제 Next.js 소스코드를 곁들인...

🚧이 글은 next.js의
next/router모듈에 대해서 다루고 있는 글입니다.
이 글에서 나오는 next.js의 소스코드는v12의 major 버전의 가장 마지막 버전인v12.3.4을 기본으로 참고하고 있습니다.
Next.js에서 현재 route에 접근하거나 다른 route로 이동하는 등 라우터에 대한 접근이 필요할 때 next/router 패키지를 사용하게 된다. 보통 3가지 방법으로 이 모듈을 사용할 수 있다.
default로 export되는
Router싱글톤 객체를 사용하기 (이하Router싱글톤 사용으로 통칭)
이렇게 3가지 방법이 있는데, 현재 Next.js 문서에서는 Router 싱글톤을 사용하는 방법에 대해선 찾아볼 수 없는 상태다. Next.js는 일반적으로 router를 사용할 때 useRouter를 이용하는 것을 권장한다.
To access the
routerobject in a React component you can useuseRouterorwithRouter.
In general we recommend usinguseRouter.React 컴포넌트에서
router객체에 접근하려면useRouter나withRouter를 사용할 수 있다.
일반적으로useRouter를 사용하는 것을 권장한다.Next.js 문서에서 발췌..
이 글에서는 각각 방식의 차이와 Next.js에서 useRouter를 사용하는 것을 권장하는 이유까지 알아보도록 하겠다. 이에 대해 알아보기 전에 Next.js가 동작하는 전반적인 구조를 먼저 파악해보자.
Next.js가 동작하는 방식
기본적으로 Next.js는 모든 페이지를 pre-render하는데 pre-render에는 2가지 방식이 있다.
Static Generation: 빌드 타임에 HTML이 생성되고 이 HTML은 매 요청마다 재사용된다.
SSR: 매 요청마다 HTML이 생성된다.
중요한 점은 Next.js에서는 모든 페이지가 기본적으로 서버에서 빌드된다는 점이다. 그래서 Next.js의 모든 페이지는 SSR을 사용하지 않더라도 SSR과 유사하게 동작한다.
빌드 타임(Static Generation) 혹은 요청이 오는 시점(SSR)에 서버 측에서 HTML을 render한다.
브라우저에서 HTML을 다운로드한다.
- HTML이 도착한 시점에는 JS가 로드되지 않았기 때문에 페이지는 Interactive하지 않다.
이후 JS가 로드되고 실행되면서 페이지가 Interactive해지게 된다.(이 과정을 Hydration이라고 한다. React 문서에서는 서버에서 이미 render된 HTML에 React를 붙이는 과정이라고 일컫는다.)
이를 실제 Next.js 코드를 보면서 전체적인 흐름을 알아보자. 먼저 서버에서 pre-render하는 코드를 보자.
Next.js에서 pre-render 코드의 동작
Next.js에서는 pre-render할 때 renderToHTML 함수를 실행한다.
renderToHTML에서는 pre-render될 때 _document.tsx, _app.tsx, render하려는 페이지가 통합되어 render된다. 이렇게 통합되어 render된 페이지는 브라우저에 전달된 이후에 hydrate되고 interactive한 상태로 바뀌게 된다.
document를 render하는 renderDocument 내부를 좀 더 파보면 React에서 SSR을 위해 제공하는 ReactDOMServer.renderToString 사용하고 있다. (v12.3.4 기준.) 하지만 ReactDOMServer.renderToString API는 React18에서 새로 추가된 Streaming SSR을 지원하지 않기 때문에 Next.js v13.0.0 버전에서는 ReactDOMServer.renderToReadableStream API를 사용하는 것을 확인할 수 있다.
pre-render 이후의 동작
앞서 말했다시피 SSR된 HTML을 브러우저에서 fetch한 이후에 SSR된 HTML과 React를 다시 연결하는 Hydration이 필요하다.
Hydration 과정은 다음과 같은 순서로 진행된다.
hydrate->render->doRender->renderReactElement
Next.js의 client-side 초기화 코드(next/client/next.js) 과정을 보면 initialize가 완료된 이후에 hydrate가 실행된다. 그리고 render와 doRender를 거쳐 renderReactElement에서 hydration이 진행되게 된다.
그럼 Next.js의 전반적인 동작과 코드에 대해서 이해했으니 이제 이 글의 원래 목적인 Router에 대해서 자세히 알아보도록 하자.
Router 사용법 비교
앞서 Router를 사용하는 3가지 방법에 대해서 알아봤는데, Next.js는 useRouter, withRouter, Router 싱글톤 방식 모두 같은 Router 객체를 사용하기 때문에 각 방식마다 사용하는 값의 차이는 없다.
먼저 useRouter와 withRouter를 먼저 보자면, 이 둘은 동일하다고 봐도 무방하다. 코드를 보면 이해가 쉽다.
위 코드를 보면 withRouter는 사실상 useRouter를 사용하지 못하는 class 컴포넌트를 위한 유틸 코드에 불과하다는 것을 알 수 있다. 이 두 코드는 완전히 동일한 동작을 한다고 봐도 무방하다.
또한 useRouter는 RouterContext에서 값을 가져오는 걸 확인할 수 있다. RouterContext는 Router 싱글톤으로 초기화되는데, 이 Router 싱글톤은 앞서 Next.js의 동작 과정을 보면서 알아본 hydrate 함수 내에서 초기화가 이뤄진다.
hydrate 에서 createRouter가 호출되고 여기서 Router 싱글톤의 초기화가 이뤄진다. 그 이유는 Next.js 앱이 client-side에서 hydration되면서 초기화되는 과정에서 router 상태가 변할 때마다 앱이 re-render되도록 router에 리스너를 등록해야 하기 때문에 Router 싱글톤 초기화가 hydrate에서 이뤄진다.
위 발췌한 코드에서 좀 이상하게 느껴질 수도 있는 부분이 Router 싱글톤을 관리하기 위한 전역 객체가 2개가 있다는 점인데(packages/next/client/index.ts의 router와 next/client/router.ts의 singletonRouter), packages/next/client/index.ts와 packages/next/client/router.ts에서 각각 Router 싱글톤을 사용하기 위한 변수를 따로 선언했기 때문이라고 이해하면 된다.
여기서 createRouter의 주석을 보면 서버에서 사용되면 안된다라는 주석이 달려있는데, 이 부분을 기억해두도록 하자.
그래서 이렇게 초기화된 singletonRouter가 바로 우리가 import Router from 'next/router'를 했을 때 사용할 수 있는 Router 싱글톤이다.
그렇다면 여기서 잠깐 돌아가서 아까 앞서 알아봤던 useRouter가 의존하는 RouterContext의 초기화가 이뤄지는 곳으로 가보자
useRouter가 의존하는 RouterContext의 값은 makePublicRouterInstance로 만들게 되는데, 여기서 의존하는 router 객체가 바로 아까 hydrate함수에서 createRouter의 리턴 값으로 초기화된 Router 싱글톤 객체다.
결국 결론은 useRouter와 Router 싱글톤 방법 모두 결국 Router 싱글톤을 사용하니까 이 둘 사이엔 아무런 차이가 없는 것일까?
결론부터 말하자면 CSR 시점에선 차이가 없다. 이 말은 즉슨 SSR 시점에서 차이가 있다는 뜻이다. 아까 언급했던 createRouter에 남겨져있던 서버에서 사용되면 안된다 주석을 다시 떠올려보자.
SSR 시점에서의 Router
SSR 시점에서는 CSR 시점에서 사용하던 Router 싱글톤과 다르게 ServerRouter를 사용한다.
위 과정에서 보면 알 수 있듯이 Router 싱글톤을 초기화하는 동작은 없고 오로지 RouterContext.Provider에 ServerRouter를 주입하는 코드만 있다. 뿐만 아니라 Router 싱글톤은 createRouter에 의해서 초기화되는데, SSR에선 이 초기화가 진행되지 않는다.
바로 이 점 때문에 SSR 시점에 Router 싱글톤에 접근하게 되면 No router instance found. 에러가 발생하게 된다. (이 포스트는 Next.js v12.3.4를 기준으로 하지만 이 포스트를 작성하는 시점 가장 최근 버전인 v13.3.0에서도 에러가 발생한다.)
SSR이 아니라 SSG를 사용하고 있는 곳이더라도 pre-render 되는 환경은 서버이기 때문에 상황은 동일하다. 다만 차이가 있다면 SSG는 빌드 타임에서 에러가 발생하고, SSR은 런타임에서 에러가 발생하게 된다.
useRouter, Router 싱글톤 정리
이렇게 Router 싱글톤과 useRouter에 대해서 알아봤다. 그럼 Router를 사용할 때 무엇을 언제 사용하면 좋을지 그리고 장단점에 대해서 알아보도록 하자.
useRouter 사용
SSR safe하다.
router가 변경될 때 훅을 사용하는 컴포넌트가 re-render된다.
Next.js 문서에서 권장하는 방식이다.
Router 싱글톤 사용
SSR safe하지 않다.
router를 변경하더라도 싱글톤을 사용하는 컴포넌트가 re-render되지 않는다.
hook이 아니기 때문에 react 바깥에서 사용하기가 상대적으로 용이하다.
더 이상 Next.js 문서에 공개되지 않는 API이어서 추후에 deprecated될 위험이 있음.
- 뿐만 아니라 Next.js v13에서 공개된
app디렉토리 구조에서 router를 사용할 때next/navigation모듈을 사용해야 하는데, 이 모듈에서는Router싱글톤을 export하고 있지 않고 있어서 더욱 지양할 필요가 있음.
- 뿐만 아니라 Next.js v13에서 공개된
컴포넌트가 Router 상태에 의존하여 re-render 할 필요가 없거나 Router 상태 변경에 의한 re-render 성능 최적화가 적극적으로 요구되는 경우라면 Router 싱글톤의 사용을 검토해볼 필요가 있으나 SSR safe하지 않다는 치명적인 단점과 deprecated될 위험이 있다는 점에서 useRouter 사용을 권장한다. 아마 이런 이유에서 Next.js에서 useRouter 사용을 권장한다고 생각한다.
글을 마치며...
사내 코드에서 Next.js Router를 사용하는 방식에 useRouter와 Router 싱글톤을 사용하는 방식 2가지가 있었는데, 이 두 가지 방식의 차이를 알아보다가 이 글을 쓰게 됐다. Next.js Router의 동작 원리 그리고 Next.js 어플리케이션의 동작 방식에 대한 전체적인 구조를 알게 되어 유익했던 시간이었다. 다음 번엔 Next.js 어플리케이션의 동작 방식을 좀 더 파보는 포스트를 작성해볼까 한다.
마치면서 이번 포스트를 쓰면서 알게 됐던 사실 몇 가지를 적어본다.
Next.js 문서에서
Router싱글톤 사용이 제거된 문서는 9.5.0 버전이다. 마지막으로 싱글톤 방식에 대해 볼 수 있는 문서는 9.4.4 버전이다.- 이 부분이 삭제된 PR은 해당 PR인데, 문서에서
Router싱글톤을 제거하게 된 명확한 이유는 없다.
- 이 부분이 삭제된 PR은 해당 PR인데, 문서에서
useRouter에서 사용하는RouterContext가 원래packages/next/server폴더에 있어서packages/next/client에서RouterContext를 사용할 때 server package에 의존성이 생기는 이상한 구조였는데, 이런 공통 로직이packages/next/shared로 들어가게 되면서 이 이상한 구조가 개선됐다. - shared 디렉토리 개선 PRLink컴포넌트는RouterContext를 사용하기 때문에useRouter를 사용하는 것과 성능은 큰 차이가 없을 것이다.



