프레임워크/next.js

Next.js 가 지원하는 Pre-rendering 이란 무엇일까?

Jake Seo 2022. 6. 19. 16:53

Next.js Pre-rendering 개요

Pre-rendering 이란, Next.js 가 각 페이지에 대한 HTML 을 생성하는 것을 말한다.

기존의 문제: SPA 의 Client-side 렌더링 문제

원래 React 같은 SPA(Single Page Application) 는 보통 .html 파일 내부에 Root 로 사용될 <div/> 태그 하나만 존재한다.

html 파일에는 아무것도 존재하지 않고, 브라우저를 통해 URL 에 접근하면, 자바스크립트에 의해 Client-side 렌더링이 시작된다. 이렇게 Client-side 자바스크립트 렌더링 방식은 느린 인터넷 환경을 이용하는 사람에게는 잠시동안 Blank page 를 보여주기도 하며, 검색엔진 최적화가 어렵다는 단점이 있다.

문제 해결: Next.js 의 Pre-rendering

Next.js 는 Pre-rendering 을 통해 HTML 페이지를 생성하여 이러한 문제를 해결한다.

Pre-rendering 의 장점은 퍼포먼스가 뛰어나고 SEO(Search Engine Optimization) 에 뛰어나다는 것이다.

만들어진 HTML 페이지는 최소한의 자바스크립트 코드만 연결되어 있다. 페이지가 브라우저에 의해 로드되었을 때, 자바스크립트 코드가 동작하여 페이지를 완전히 인터렉티브하게 만들어준다. 이 과정을 hydration 이라는 용어로 부른다.

만일, 자바스크립트를 비활성화하면 HTML 은 pre-rendering 에 의해 생성되어 초기 화면은 보이지만, 자바스크립트를 이용한 동적인 부분은 작동하지 않는다. hydration 이 작동하지 않았기 때문이다.

Next.js 가 지원하는 두가지의 Pre-rendering 방식

Static GenerationServer-side Rendering 이 존재한다. 두 방식의 차이는 '언제 HTML 을 생성하냐' 에 있다.

  • Static Generation (Recommended): 빌드 타임에 HTML 이 생성되어, 매 요청마다 재사용된다.
    • 빌드 타임에 생성된다는 것은 next build 명령어를 실행했을 때 생성되는 것을 의미한다.
    • CDN 에 의해 캐싱도 가능하다.
    • 데이터를 포함하는 HTML 페이지 생성도 가능하다.
  • Server-side Rendering: 매 요청마다 HTML 이 생성된다.

Next.js 는 한가지 방법을 강요하지 않는다. 하이브리드로 매 페이지마다 원하는 방법을 쓸 수도 있다.

성능상의 이유로 Next.js 에서는 Static Generation 을 추천한다. Static Generation 으로 만들어진 페이지는 추가적인 설정 없이도 CDN 에 의해 캐싱이 되어 퍼포먼스 향상을 노릴 수 있다. 하지만 특정 상황에서는 Server-side Rendering 이 꼭 필요할 때도 있긴 하다.

원한다면, Client-side RenderingStatic Generation 혹은 Server-side Rendering 과 함께 사용할 수도 있다. 더 자세히 알아보고 싶다면, Data Fetching 문서 를 참고하자.

Next.js 렌더링과 일반 React App 의 소스코드 현실 비교

일반 React App

<!DOCTYPE html>
<html lang="ko">
   <head>
      <script>
         window.trackKakaoPixel = function () {};
      </script>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
      <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=3.0, minimum-scale=1, user-scalable=yes"/>
      <link rel="stylesheet" type='text/css' href="https://cdn.banksalad.com/fonts/noto/NotoSansKR.css">
      <link rel="stylesheet" type='text/css' href="https://cdn.banksalad.com/fonts/jua/style.css">
      <link rel="stylesheet" href="/dist/v2.bundle.css">
      <link rel="stylesheet" href="/dist/v2.vendor.bundle.css">
      <title>금융을 내 편으로 | 뱅크샐러드</title>
      <meta name="description" content="카드, 예적금, 보험, 투자, 대출, 연금, 실물자산까지! 500만이 선택한 내 돈 관리 앱 뱅크샐러드로 새로운 돈 관리를 시작해보세요."/>
      <meta name="keywords" content="뱅크샐러드, 돈관리, 돈관리앱, 카드, 카드추천, 신용카드추천, 체크카드추천, 적금추천, 예금추천, CMA추천, CMA통장추천, 보험비교, 보험비교사이트, 금리계산기, 금리비교사이트, 혜택많은신용카드, 신용카드비교, 체크카드비교, 뱅샐, 샐러드뱅크, 뱅셀, banksalad"/>
      <meta name="og:title" content="금융을 내 편으로 | 뱅크샐러드"/>
      <meta name="og:description" content="카드, 예적금, 보험, 투자, 대출, 연금, 실물자산까지! 500만이 선택한 내 돈 관리 앱 뱅크샐러드로 새로운 돈 관리를 시작해보세요."/>
      <meta property="og:url" content="https://www.banksalad.com/">
      <meta property="og:image" content="https://cdn.banksalad.com/app/meta/introduce-a/og_banksalad.png" />
      <meta name="theme-color" content="#16c89b" />
      <meta name="naver-site-verification" content="6e174e47ce861cc70846d4bf91a36904b89f730e" />
      <meta name="google-site-verification" content="EZ3Ngy-eg4XxWQFF8Y7PwPX2cJYbPi4-h6gcis0S0iA" />
      <meta name="google-signin-client_id" content="602590083136-pg65trppq4if6383rfufkug9q367l1lh.apps.googleusercontent.com">
      <meta name="apple-mobile-web-app-title" content="Banksalad">
      <link rel="apple-touch-icon-precomposed" href="https://cdn.banksalad.com/app/meta/introduce-a/favicon_120x120.png">
      <link rel="apple-touch-icon-precomposed" sizes="196x196" href="https://cdn.banksalad.com/app/meta/introduce-a/favicon_196x196.png">
      <link rel="shortcut icon" type="image/x-icon" href="https://cdn.banksalad.com/app/meta/introduce-a/favicon.ico">
      <link rel="stylesheet" href="https://cdn.banksalad.com/content/style/contents.min.css" />
      <link rel="canonical" href="https://www.banksalad.com/" />
      <!--[if IE 9 ]>
      <script src="https://cdn.banksalad.com/lib/IE9.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/selectivizr/1.0.2/selectivizr-min.js"></script>
      <![endif]-->
      <!-- Page hiding snippet -->
      <style>.async-hide { opacity: 0 !important} </style>
      <script>
         (function(a,s,y,n,c,h,i,d,e){s.className+=' '+y;
             h.end=i=function(){s.className=s.className.replace(RegExp(' ?'+y),'')};
             (a[n]=a[n]||[]).hide=h;setTimeout(function(){i();h.end=null},c);
         })(window,document.documentElement,'async-hide','dataLayer',4000,{'GTM-NVB8B6P':true});
      </script>
      <!-- End Page hiding snippet -->
      <!-- Google Tag Manager -->
      <script>
         (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
                 new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
             j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
             'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
         })(window,document,'script','dataLayer','GTM-NVB8B6P');
      </script>
      <!-- End Google Tag Manager -->
      <script>
         (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
                     (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
                 m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
         })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

         ga('create', 'UA-44896653-4', 'auto');
         ga('require', 'GTM-TC3TM64');
         ga('require', 'urlChangeTracker');
         ga('require', 'outboundLinkTracker');
         ga('require', 'cleanUrlTracker', {
             stripQuery: true,
             trailingSlash: 'remove'
         });
         ga('set', 'appName', 'banksalad2:Web');
         ga('send', 'pageview');
      </script>
      <script async src="https://cdnjs.cloudflare.com/ajax/libs/autotrack/2.4.1/autotrack.js"></script>
      <script>
         !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
                 n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
             n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
             t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
                 document,'script','//connect.facebook.net/en_US/fbevents.js');

         fbq('init', '147489888961817');
         fbq('track', "PageView");
      </script>
      <noscript><img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=147489888961817&ev=PageView&noscript=1" /></noscript>
      <script type="text/javascript" charset="UTF-8" src="//t1.daumcdn.net/adfit/static/kp.js"></script>
      <script type="text/javascript">
         var kakaoPixelInit = function(id) {
           try {
             var _kakaoPixel = window.kakaoPixel(id);
             return function(eventName, tag) {
               _kakaoPixel[eventName](tag);
             };
           } catch(e) {
             return function() {};
           }
         }
         window.trackKakaoPixel = kakaoPixelInit('2074218455202443754');
         window.trackKakaoPixel('pageView');
      </script>
      <script>
         window.namespaceEnv = 'production';
         window.apiHost = 'https://api.banksalad.com';
         window.gatewayHost = 'https://api.banksalad.com/v1/lightweightgateway';
      </script>
   </head>
   <body>
      <!-- Google Tag Manager (noscript) -->
      <noscript>
         <iframe
            src="https://www.googletagmanager.com/ns.html?id=GTM-NVB8B6P"
            height="0"
            width="0"
            style="display:none;visibility:hidden"
            ></iframe>
      </noscript>
      <!-- End Google Tag Manager (noscript) -->
      <div id="wrap"></div>
      <div class="g-signin2" style="display: none;"></div>
      <script>
         window.fbAsyncInit = function() {
             FB.init({
                 appId      : '250069005407221',
                 autoLogAppEvents : true,
                 xfbml            : true,
                 version          : 'v9.0'
             });
         };
      </script>
      <script>
         window.kakaoInit = function () {
             Kakao.init('ae341654dec07eb62b8c46edb32646c0');
         };
      </script>
      <script async defer crossorigin="anonymous" src="https://connect.facebook.net/en_US/sdk.js"></script>
      <script async defer onload="kakaoInit()" src="//developers.kakao.com/sdk/js/kakao.min.js"></script>
      <script src="//cdn.banksalad.com/resources/protocol/protocol.min.js"></script>
      <script src="/dist/v2.vendor.js"></script>
      <script src="/dist/v2.bundle.js"></script>
      <script>
         console.log(
             " /$$                           /$$                           /$$                 /$$\n" +
             "| $$                          | $$                          | $$                | $$\n" +
             "| $$$$$$$   /$$$$$$  /$$$$$$$ | $$   /$$  /$$$$$$$  /$$$$$$ | $$  /$$$$$$   /$$$$$$$\n" +
             "| $$__  $$ |____  $$| $$__  $$| $$  /$$/ /$$_____/ |____  $$| $$ |____  $$ /$$__  $$\n" +
             "| $$  \\ $$  /$$$$$$$| $$  \\ $$| $$$$$$/ |  $$$$$$   /$$$$$$$| $$  /$$$$$$$| $$  | $$\n" +
             "| $$  | $$ /$$__  $$| $$  | $$| $$_  $$  \\____  $$ /$$__  $$| $$ /$$__  $$| $$  | $$\n" +
             "| $$$$$$$/|  $$$$$$$| $$  | $$| $$ \\  $$ /$$$$$$$/|  $$$$$$$| $$|  $$$$$$$|  $$$$$$$\n" +
             "|_______/  /\\_______/|__/  |__/|__/  \\__/|_______/  \\_______/|__/ \\_______/ \\_______/"
         )
      </script>
   </body>
</html>

위는 뱅크셀러드의 예인데, 메인 페이지에 <body/> 태그 안쪽 내용에 html 요소가 거의 없는 것을 알 수 있다. <div id="wrap"></div> 와 그 밑에 div 태그 1개정도가 더 있을 뿐이다.

Next.js App

<!DOCTYPE html>
<html>
   <head>
      <style data-next-hide-fouc="true">body{display:none}</style>
      <noscript data-next-hide-fouc="true">
         <style>body{display:block}</style>
      </noscript>
      <meta charSet="utf-8"/>
      <meta name="viewport" content="width=device-width"/>
      <meta name="next-head-count" content="2"/>
      <noscript data-n-css=""></noscript>
      <script defer="" nomodule="" src="/_next/static/chunks/polyfills.js?ts=1655624731846"></script><script src="/_next/static/chunks/webpack.js?ts=1655624731846" defer=""></script><script src="/_next/static/chunks/main.js?ts=1655624731846" defer=""></script><script src="/_next/static/chunks/pages/_app.js?ts=1655624731846" defer=""></script><script src="/_next/static/chunks/pages/index.js?ts=1655624731846" defer=""></script><script src="/_next/static/development/_buildManifest.js?ts=1655624731846" defer=""></script><script src="/_next/static/development/_ssgManifest.js?ts=1655624731846" defer=""></script><script src="/_next/static/development/_middlewareManifest.js?ts=1655624731846" defer=""></script>
      <noscript id="__next_css__DO_NOT_USE__"></noscript>
   </head>
   <body>
      <div id="__next" data-reactroot="">
         <nav class="jsx-faaba1baeedd438e"><a class="jsx-faaba1baeedd438e active" href="/">Home</a><a class="jsx-faaba1baeedd438e " href="/about">About</a></nav>
         <div>
            <h2>Index Page</h2>
         </div>
      </div>
      <script src="/_next/static/chunks/react-refresh.js?ts=1655624731846"></script><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/","query":{},"buildId":"development","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script>
   </body>
</html>

body 태그 내부에 div 태그 등 다양한 내용이 pre-rendered 되어 보이는 상태이다.

레퍼런스

반응형