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 Generation
과 Server-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 Rendering
을 Static 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
되어 보이는 상태이다.
레퍼런스
'프레임워크 > next.js' 카테고리의 다른 글
Next.js 에서 Hydration 이란 무엇인가? (0) | 2022.06.23 |
---|---|
Next.js 의 빌트인 태그인 next/head 는 무엇일까? (0) | 2022.06.20 |
Next JS 에서 스타일링을 적용하는 다양한 방법 (0) | 2022.06.18 |
Next.js APP _app.js 의 모든 것 (0) | 2022.06.18 |
next.js 의 라우팅은 일반적인 페이지의 라우팅과 무엇이 다른가? (0) | 2022.06.14 |