Web: Next.js Link와 Prefetch 과정 파헤쳐보기

Heechan
HcleeDev
Published in
15 min readFeb 4, 2023
Photo by Mike van den Bos on Unsplash

Next.js를 좀 사용하다보니, 이건 React처럼 Single Page Application인지 아닌지에 대해서 궁금해졌었다. 그래서 개발자 도구 네트워크 탭을 보면서, HTML이 어떻게 오는지 확인해보려고 했었다.

그런데 예시 페이지를 몇개 들어가봤는데 HTML 파일이 오는게 아니라 왠지 모를 JS 파일이 오고, Navigating이 발생해도 뭔가 새로운 HTML이 오는 것은 보이지 않았다.

이번주는 이 현상을 만드는 Next.js의 Link와 Prefetch에 대해서 알아보자.

Next.js에서의 Routing과 Single Page Application

2주 전에 작성한 Next.js 소개 글에서 Routing에 대해서 얘기하긴 했지만, 간단하게 다시 짚고 가자.

Next.js에서는 File System Based Routing을 한다. 기존의 React Router에서는 꽤 상위 컴포넌트에서 <Route path='/' element={<Main />} /> 이런 식으로 원하는 경로마다 설정해줘야 했다. 하지만 Next.js는 pages 폴더 내의 파일 구성에 따라 Routing이 만들어진다.

위처럼 각 폴더 이름으로 접근하면 index.js 파일에서 만드는 컴포넌트를 기준으로 해당 경로의 페이지가 설정이 된다. index.js 파일이 아니면 아래 Nested routes에 나온 것처럼 해당 파일의 이름으로 경로가 설정된다.

각 파일에서 export default 로 내보내지는 컴포넌트를 기준으로 페이지가 만들어진다. 다른 export 가 있어도 export default 가 기준이 된다.

Routing으로 페이지 사이의 관계를 만들었다면 그 사이에서 어떻게 움직일지도 중요하다.

웹 프레임워크에서 각 경로 사이의 움직임에 대한 처리를 따로 하는 이유는 새로운 로딩을 덜 하기 위해서다. 만약 /user 링크 화면에서 각 유저에 대한 정보를 보려고 어떤 버튼을 눌러 /user/1557 이런 링크로 넘어갔다고 생각해보자. 이때 주소가 변경되면 브라우저에서는 원래 해당 링크에 대한 HTML을 새롭게 서버에다가 요구하게 된다.

과거에 작성한 Single Page Application에 대한 글을 참고하면 도움이 될텐데, 그렇게 받아오면 또 HTML 가져오고, CSS 그리고, JS 받아오고 하는데 시간이 걸리게 된다. 그렇다보니 Single Page Application을 구현하기 위해서는, 앱 내에서 주소가 변경될 때 브라우저의 Navigate 이벤트 핸들링을 막고, 한 페이지 안에서 Router를 이용해 그에 맞는 컴포넌트를 리랜더링해서 보여준다.

Single Page Application는 사이즈가 크다는 단점이 있지만, 최초 로딩 때 그 큰 사이즈의 데이터를 다 받아오고 나면 그 이후의 랜더링, 전환은 빠르다는 장점이 있다.

요즘은 SPA의 시대인데, Next.js로 만든 애플리케이션 Single Page Application일까?

빌드된 결과물을 따져보면 그렇지는 않다. Next.js의 빌드 결과물은 기본적으로 각 페이지가 Static Generation일 경우 HTML 파일이, ServerSide Rendering의 경우에는 서버에서 런타임 중에 돌아갈 JS 파일이 만들어진다.

위 사진은 빌드 결과를 알려주는 터미널 화면과, .next 폴더 내의 빌드 결과물이다. 자동적으로 Static HTML로 만들어졌고, 가장 상위 index.html과 first-post.html이 만들어진 것을 확인할 수 있다.

다만 ‘SPA가 아니다’ 라고 할 수는 없는 것이, Next.js의 근본은 React로 되어있다. 만약 Next.js의 File System Based Routing을 사용하지 않고 최상위 index.js 하나에서 React를 사용하듯 모든 내용을 몰아넣는다면 HTML 페이지는 하나만 사용하는 것이 된다. 따라서 개발자의 의도에 따라 애플리케이션의 SPA 여부는 달라질 것이고 Next.js 사용 여부에 달린 것이 아니다.

Prefetch와 Next/Link

Next.js는 기본적으로 요청에 따라 Static HTML, 혹은 SSR의 결과 HTML을 먼저 던져주고, 브라우저가 JS를 다 다운로드하면 흔히 React가 굴러가는 방식대로 다시 DOM을 구성해 사용자에게 보여준다. 이 과정을 Hydration이라고 한다.

그러면 Next.js는 HTML을 각각 만들기 때문에 SPA가 가지고 있는 빠른 화면 전환 같은 장점을 포기한 것일까? 그냥 React에서는 처음에 큰 사이즈의 애플리케이션을 다 받아오면 그 이후는 빠르게 화면을 보여줄 수 있는데, 이렇게 하면 아무리 Static HTML을 빨리 받아온다 하더라도 결국 네트워크 요청을 타는건데 느려지는 것 아닌지 궁금했다.

그래서 Next.js의 예시 사이트를 한 번 확인해봤는데…

일단 제일 처음에 오는 HTML은 우측에서 볼 수 있듯 뭔가 CSS도 적용이 안된 쌩얼 상태로 오게 된다. 물론 이 HTML을 뜯어보면 <script> 태그로 필요한 CSS, JS 파일들을 불러온다. 저 네트워크 리스트에서 main-340bc... , _app-8e... , index-4f6d... 같은 파일들이 다 최초 HTML에 붙어있던 애들이다.

그런데 네트워크 탭을 키고 사이트를 돌아다니다보니 신기한 현상이 있었다.

링크에 마우스를 hover하니 뭔가 다운로드가 되었고, 그 내용을 살펴보니 그 링크를 타고 들어갔을 때 보이는 내용이 들어있었던 것을 확인할 수 있다.

아직 들어가지도 않았는데 들어갈 가능성이 있는 페이지의 내용을 미리 받아오는 것이다.

위 움짤에서 한 가지 더 눈치챌만한 점이 있는데, 그래서 실제로 링크를 눌러서 들어갈 때는 HTML 파일이 다시 오지는 않았다는 점이다. 새로운 HTML을 받아오지 않고 바로 여기서 랜더링을 한다는 것을 확인할 수 있다.

즉, 들어갈 가능성이 있는 페이지는 ‘미리 가져오는 작업’이 있고, 그런 경우 새롭게 HTML부터 다시 받아오는 것이 아닌 바로 랜더링을 해서 보여줄 수 있기 때문에 SPA 마냥 빠른 Routing 전환도 가능해진다.

Next.js에서는 이것을 Prefetch라고 부른다. 이 Prefetch 기능을 사용할 수 있는 방법은 next/link 에서 제공하는 Link 컴포넌트를 사용하는 것이다.

많은 Router 라이브러리에서 하이퍼링크를 만들어주는 컴포넌트를 제공하는데, 이 Link도 그런 역할이다. 위 코드에서 확인할 수 있듯 Link 컴포넌트를 이용해 Next.js 내의 Routing 기능이나 추가적인 최적화(예를 들면 지금 얘기 중인 Prefetch)를 기대할 수 있다.

Link에는 우리가 궁금해하는 Prefetch 옵션도 있다. <Link prefetch={false} /> 이런 식으로 할 수 있겠다.

공식 문서에 있는 설명인데, 기본값(true )일 경우 브라우저의 Viewport 내에 있으면 Link의 경로에 해당하는 페이지를 백그라운드에서 미리 가져오는 역할을 한다고 한다. 근데 이 기능은 false 일 때도 완전히 꺼지는 것은 아니고 링크를 hover하면 로딩한다고 한다. 그리고 개발환경에서는 확인할 수 없고 배포된 production에서만 확인할 수 있다.

Server Side Rendering 방식의 페이지는 JS를 로드할 것이고, Static Generation의 경우에는 HTML이 아닌 JSON 파일을 돌려준다고 한다.

그러면 아까 봤던 예시 블로그와 그 네트워크 탭을 다시 한 번 찬찬히 뜯어보도록 하겠다.

일단 이 블로그에서 Link 컴포넌트는 2개다. 위에 Twitter랑 our Next.js tutorial은 외부 링크라서 아니고, 밑에 각 포스트 페이지로 넘어가는 링크 2개가 있다.

각 링크가 향하는 주소는 /posts/ssg-ssr 과, /posts/pre-rendering 이다. 보아하니 ‘ssg-ssr’과 ‘pre-rendering’은 Dynamic Routing으로 들어가는 Params로 보인다. 실제 프로젝트에서 이 페이지의 경로는 /pages/posts/[id].jsx 이런 식일 것이다.

그러면 이 페이지가 랜더링되자마자 이 파일에 대한 Prefetch가 진행될 것이다. 네트워크 탭에서 한 번 찾아보았다.

%5Bid%5D-578e87ae4abe1282.js 가 해당 페이지를 랜더링하기 위한 JS 파일을 담고 있다. %5B와 %5D가 각각 [, ]인 것을 생각하면 우리가 생각한 [id].js 이런 이름의 파일이 맞음을 확인할 수 있다.

이 파일의 경로는 static/chunks 에 들어있는데, 이 파일들은 서버에서 작업 후 먼저 주는 HTML이 아니라, 그 후 Hydration을 위해 로딩하는 JS 파일이 들어있는 폴더다.

따라서 Prefetch 할 때는 SSR 방식의 페이지인 경우 전체 랜더링을 위한 JS 파일을 받아옴을 확인할 수 있다. 생각해보면 어차피 백그라운드에서 미리 가져와서 작업하는 것인데 괜히 웹 서버한테 SSR 작업의 결과 HTML을 받아올 필요는 없긴 하다.

근데 우리가 아까 본 움짤에서는 Hover 했을 때도 뭔가 가져오는 것을 확인할 수 있었다. 그 데이터는 사실은 SSR에서 getServerSideProps 같은 메서드로 미리 서버로부터 받아오는 데이터를 브라우저에서도 똑같이 불러온 작업의 결과다. 여기서는 Dynamic Routing 기반으로 데이터를 받아와야 하니 getStaticPath 같은 메서드가 아니었을까 싶다.

이름부터가 pageProps라 그런 느낌이 확 들었다

원래는 SSR 페이지의 경우 웹 서버에서 해당 페이지를 만드는 런타임 중에 해당 데이터를 가져와야 한다. 그런데 Prefetch 상황에서는 JS 파일을 브라우저가 가져온 상태이고, 이 페이지를 랜더링하기 위해서는 원래 SSR 과정에서 받아오는 Props도 필요하므로 그걸 브라우저에서 로딩해주는 절차라고 보면 되겠다.

아마 Hover 했을 때마다 가져오는건 나름 Props를 fresh하게 유지하려는 그런 작업이 아닐까 싶다.

위에 있는 pageProps의 contentHtml , date , title 은 아까 받아온 chunk JS 안에서 사용 중임을 확인할 수 있었다.

이러면 JS랑 Props는 갖춰졌으니 해당 페이지로 들어가자마자 JS를 통한 랜더링 작업이 진행되어 유저에게 보여질 것이다. 사실 SSR에서 만드는 HTML도 여기서 미리 만드나? 하는 궁금증도 있었는데, 이건 Next.js 자체 코드를 면밀히 이해해야 알 수 있을 것 같다.

SSG 경우는 직접 확인한 것은 아니지만 JSON으로 온다하니, 아마 JSON 내에 HTML 문자열이 있거나 하지 않을까… 싶다.

Next.js 코드 베이스 뜯어보기

아무래도 모든 궁금증을 풀지는 못했다보니 Next.js Github Repository를 클론 받아서 확인해보긴 했다. Next.js v13.1.6를 기준으로 했다. 지금 보고 있는 파일들은 내가 이걸 쓰는 며칠 동안에도 계속 바뀌고 있어서… 아마 미래에는 또 코드가 확 달라져있을 수도 있다.

결론부터 말하자면 prefetch해서 데이터를 로드하는 것까지는 파악했는데, 그 이후 Navigation 단계에서 어떻게 활용하는지는 제대로 이해하지 못했다. 관심있으신 분은 해당 Github를 한 번 직접 확인해보시는 것도…

일단 Link 컴포넌트부터 찾아보았다.

Link 컴포넌트 내에서 React.useEffect를 사용하고 있는 부분을 찾을 수 있었다. Production에서만 동작하고, Prefetch 기능을 끄지 않았다면 Viewport에서 볼 수 있을 시 prefetch 를 동작시키는 모습이다.

prefetch 안에 들어있는 router의 타입에 따라 뭔가 많이 달라지는데, 일단 해당 router는 NextRouter | AppRouterInstance 라는 타입인데, 클라이언트에서 정상적으로 마운트가 되었다면 NextRouter 타입으로 만들어지는 것 같다. 그래서 NextRouter 쪽 prefetch 를 살펴보려고 한다. AppRouterInstance의 prefetch도 보긴 봤는데, 우리가 봤던 블로그 예시가 돌아가는 것처럼 돌아가지 않음을 확인했다.

router.ts의 끄트머리에 pageLoaderprefetch 메서드를 부르고 있는 것을 봤다. 그걸 타고 가다보면 아래 코드가 나온다.

NextRouter의 prefetch 는 이 route-loader 파일의 prefetch 로 연결된다. 여기서는 getFilesForRoute 으로 우리가 가져오고자 하는 링크에 대한 파일 정보를 받아온 후, 해당 파일을 prefetchViaDom 이라는 메서드로 가져온다.

getFilesForRoute 에서는 어떻게 링크와 chunk 파일을 매핑할까? 다시 개발자 도구 네트워크 탭을 잘 살펴보면 최초 로딩 때 buildManifest라는 것을 로드하고 있음을 확인할 수 있다.

self.__BUILD_MANIFEST=function(s,e){
return {
"/":[s,e,"static/chunks/pages/index-4f6d89bb5c78be44.js"],
"/posts/[id]":[s,e,"static/chunks/pages/posts/[id]-578e87ae4abe1282.js"],
}
}

이것저것 생략했지만 대강 이런 내용으로, getFilesForRoute 내부에서는 Manifest에 접근해 chunk 파일 루트를 바다오고 있다.

그 후 chunk를 신기하게도 prefetchViaDom 메서드 내에서 받아오는데, 말그대로 DOM의 <head>에다가 link 태그를 추가해서 해당 파일을 받아오는 방식이다.

요소를 확인해보니 rel='prefetch' 가 달린 link 태그가 하나 있다. 우리가 원하는 chunk JS 파일을 다운로드 해줄 것이다.

이게 완료되면 loadRoute 메서드를 실행한다.

withFutureresolvePromiseWithTimeout 같은건 일단 크게 신경쓰지 않아도 되고, 중요한건 그 안에서 어떤 일이 벌어지는지다.

일단 아까 불렀던 getFilesForRoute 를 이용해 필요한 Script와 css 파일에 대한 주소를 가지고 온다. 그리고 가져온 링크에 대해 Promise.all(scripts.map(maybeExecuteScript)) 라는걸 불러주고 있다.

maybeExecuteScript 를 확인해보면 아까 <head>에 link 태그를 추가했던 것처럼 여기서는 DOM에 script 태그를 추가하는 작업을 하고 있다. appendScript 라는 메서드가 그 역할을 내부에서 수행하고 있다.

실제로 <body>를 확인해보면 해당 링크에 대해 script 가 추가되어있다. 아마 link 로 로딩한 후, 이 script 태그로 실제로 해당 JS를 구동시키는 느낌으로 확인할 수 있겠다.

이러면 JS 파일이 구동되고 그 컴포넌트가 다시 만들어지는 것이니 아까 get~Props 류 메서드로 받아오는 데이터도 계속 받아오는게 이해가 된다.

그러면 이제 실제로 Link 컴포넌트를 클릭했을 때는 이때 구동한 컴포넌트를 기반으로 어떻게 화면을 랜더링하는 것일까?

내부 메서드인 navigate 가 불린다. 여기서 NextRouter를 ‘beforePopState’ 존재 여부로 판단한다고 되어있는데, 아무튼 Router 종류에 상관없이 둘 다 replace , push 를 부르는 것 같다. 해당 메서드들을 router.ts 에서 찾고 찾아 들어가다보면 결국 change 라는 메서드에서 모인다.

아… 근데 이 부분부터 봐도 뭔가 routeInfo 에서 Component 를 꺼내서 세팅하는 것같긴 한데 그 과정을 이해하기가 너무 어려워서 이 이상은 확인을 하지 못했다. 혹시 궁금하신 분은 이 링크에 있는 파일을 중심으로 한번 prefetch , change 등의 메서드를 확인해보시면 좋을 것 같다.

결론

Single Page Application인지 아닌지 궁금해 했던 것이 이렇게 깊게까지 들어가서 파헤쳐보게 되었다. 그래도 Next.js 씩이나 하는 프로덕트다보니 사용성을 좀 더 좋게 하기 위한 방법은 다 구현해둔 것이구나 싶긴 했다.

사실 Prefetch하는 과정을 알아보는게 내 개발자 인생에 얼마나 도움될지는 모르겠지만, 그래도 굉장히 흥미로운 기능이었다. 코드를 끝까지 이해하지 못한 것은 좀 아쉬운 부분이다.

다음에 언젠가는 Next.js에서는 내부에서 React를 어떤 식으로 랜더링시키는건지 한번 확인해보고 싶기도 하다.

--

--

Heechan
HcleeDev

Junior iOS Developer / Front Web Developer, major in Computer Science