회사에서 Next.js를 쓰는 가장 큰 이유가 SSR 때문이라고 한다.
사이드 프로젝트에서 내가 만든 React.js를 보면 첫 로딩 시간이 너무 느리긴 했다.
그 당시에는 중요한 이슈가 아니었어서 스치듯이 문서만 본 적이 있는데, 이번 기회에 확실히 정리하고자 한다.
결론부터 말하자면 둘 중 하나가 탁월히 좋다는 것이 아니다.
각각의 장단점이 있기 때문에 프로젝트에 맞는 렌더링을 선택하는 것이 맞다고 생각한다.
우선 렌더링이란 서버에 요청해서 받은 내용을 브라우저 화면에 표시하는 것이다.
과정은 다음과 같다.
SSR / CSR은 서버에서 렌더링을 하냐, 클라이언트에서 렌더링 하냐의 차이가 있다.
SSR은 Server Side Rendering의 약어로, 사용자가 페이지를 이동할 때마다 새로운 페이지를 요청한다.
사용자가 받을 템플릿(html 등)은 서버에서 렌더링하고 완성된 페이지 형태로 응답을 준다.
특히 단순한 텍스트와 정적인 이미지만 표시하던 과거에 SSR은 큰 문제가 없었다.
하지만 현재의 복잡한 웹 사이트의 경우 사용자가 특정 기능을 실행하기 위해 단 한번의 클릭을 했어도 사이트 전체가 다시 로딩되어야 하는 문제에 생긴다.
장단점은 다음과 같다
CSR은 Client Side Rendering의 약어로, 브라우저에서 첫 요청 시 한 페이지만 불러온다.
일반적으로 브라우저에서 자바스크립트를 사용하여 콘텐츠를 렌더링하는 것을 의미한다.
사용자 인터렉션으로 변화되는 내용만 AJAX를 통해 서버에 요청하고 데이터를 받음으로써 사용 가능한 렌더링 방식이다.
SPA와 항상 같이 나오는 개념이라고 생각해도 된다.
React.js와 같은 경우 index.js
(또는 .ts
)에서 모든 페이지 내용을 가지고 있고, 사용자는 index.js
만 받아서 화면에 표시한다.
장단점은 다음과 같다
Remix, Next.js, Gatsby 세 개 다 써보지 않아서 정확한 비교는 내가 정리하는 것보다는 https://www.youtube.com/watch?v=RP8nvTeurbQ 여길 보는 게 나을 것 같다.
다만 이러한 프레임워크를 사용하는 이유는 CSR / SSR을 넘어서 그냥 React로만 프로젝트를 구성하는 것보다 다양한 이점이 있기 때문이다.
CSR, SSR에 대한 것만 정리하자면 세 가지 프레임워크 모두 React의 CSR을 극복할 수 있다.
Remix는 SSR만 제공하는 것 같다.
Next.js는 CSR, SSR을 모두 제공할 수 있다.
Gatsby는 정적 사이트 생성(SSG)을 위해 사용하는 프레임워크로 SSR, CSR, lazy loading 등의 기능도 지원한다. 작은 서비스에 좋은 수단이 될 수 있다.
~SSR과 SEO는 생략~
yarn add typescript @types/node @types/react
로 설치 가능.yarn add -D sass
로만으로 scss 파일 사용 가능.page.ts
라는 파일이 있다면 domain:[port]/page
로 라우팅됨. 파일 이름을 [id].ts
라고 지었다면 domain:[port]/id
로 접근이 가능하며 id는 동적으로 변화가 가능함. 해당 컴포넌트(정확하게는 페이지)에서는 useRouter
를 이용한 뒤 router.query
를 통해 id를 얻을 수 있음.// [id].ts
import { useEffect } from 'react';
import { useRouter } from 'next/router';
export default () => {
const router = useRouter();
useEffect(() => {
console.log(router.query.id);
});
};
https://stackoverflow.com/questions/65086108/next-js-link-vs-router-push-vs-a-tag 를 참고했다.
Next에서는 라우팅을 하는 방법이 \<Link> 를 이용한 방법, useRouter().push()(이하 router.push) 를 이용한 방법, \<a> 를 이용한 방법이 있다.
하지만 \<a>를 이용한 라우팅은 보통 사용되지 않는데, 두 가지 단점이 있기 때문이다.
<a> tag without using next/link's <Link> creates a standard hyperlink which directs end user to the url as a new page
라고 한다.router.push 와 \<Link> 의 차이는 주로 SEO 와 관련되어 있다.
router.push는 window.location과 비슷하게 동작하여 \<a>를 생성하지 않는다.
이 말은 연관된 링크가 크롤러에 의해 수집되지 않아 SEO에 불리하다.
반면, \<Link>는 \<a>를 생성하여 연관된 링크가 크롤러에 의해 수집이 될 수 있다.
스택 오버플로우에 따르면 Endusers will still navigate with without reloading the page, creating the behavior of a Single Page App
라고 한다.
cf) 같은 page에서 다른 컴포넌트로 router 이동 시 history가 쌓이지 않기 때문에 뒤로가기 버튼을 누르면 이전 화면이 뜨지 않고 이전전 page 화면이 뜬다.
이를 해결하고 싶다면 next.js에서 쿼리스트링으로 같은 page 내에서 window.history 쌓기를 참고.
기본값은 true
로 <Link />
가 렌더링될 때 href
옵션으로 연결되어 있는 모든 항목이 미리 로드되는 것을 말하며 production 레벨에서만 이루어진다.
공식 홈페이지에 있는 그림을 통해 보면 한 눈에 알 수 있다.
pre-rendering의 방식에는 두 가지가 존재한다.
static generation
build time에 HTML을 만드는 메소드로, 한 번 만들어진 뒤에는 매 request 마다 해당 HTML이 재사용된다.
server-side rendering
매 request 마다 HTML을 만드는 메소드로, static generation과 달리 해당 HTML이 재사용되지 않는다.
루트 디렉터리 안에는 component, pages, global 등의 디렉터리가 위치된다.
해당 디렉터리 안에 들어가지 않는 파일에는 index.ts, _app.ts, _document.ts가 있다.
다만 index.ts가 반드시 존재하는 것은 아니다.
index.ts는 domain:[port]/ 와 같이 루트 페이지에 접근할 때, 상황에 따라(로그인 여부 등) 화면을 보여주거나 redirect 시키는 등의 기능을 주로 넣어두는 파일이다.
// _app.ts 예시
function App({ Component, pageProps }) {
~~~
return <Component {...pageProps} />
}
Component
와 pageProps
가 있다.Component
는 요청한 페이지이다. 'GET /' 을 보냈다면, Component
에는 /pages/index.ts
파일이 props로 전달된다.pageProps
는 getInitialProps를 통해 내려 받는 props들을 말하며 자세한 설명은 아래에 있다.console.log
실행시 서버, 클라이언트 모두에서 콘솔 결과를 볼 수 있다.// _document.ts 예시
import Document, { Html, Head, Main, NextScript } from "next/document"
export default class MyDocument extends Document {
render() {
return (
<Html>
<Head>
// 모든페이지에 아래 메타(웹 타이틀, ga 같은 것)가 head에 들어감
// 루트파일이기에 가능한 적은 코드만 넣음
<meta ~ />
</Head>
<body>
<Main />
</body>
<NextScript />
</Html>
)
}
}
console.log
실행시 서버에서만 보인다.componentDidMount
같은 훅도 실행되지 않으며 static한 상황(로직)에만 사용된다.공식문서에서 각 사용법에 대해 나와 있으므로 해당 내용을 참고해도 된다.
material-ui(mui) 사용법
yarn add @emotion/react @emotion/styled @mui/icons-material @mui/material @mui/styles
실행// document.ts
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '@mui/styles';
export default class MyDocument extends Document {
static getInitialProps = async (ctx) => {
const materialSheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
materialSheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: <>{initialProps.styles}</>,
};
};
}
// app.ts
import type { AppProps } from 'next/app';
import CssBaseline from '@mui/material/CssBaseline';
const App = ({ Component, pageProps }: AppProps) => {
return (
<>
<CssBaseline />
<Component {...pageProps} />
</>
);
};
export default App;
styled component 사용법
yarn add -D babel-plugin-styled-components
실행{
"presets": ["next/babel"],
"plugins": [
[
"babel-plugin-styled-components",
{ "fileName": true, "displayName": true, "pure": true, "ssr": true } // 옵션보고 설정
]
]
}
// ~~
import { ServerStyleSheet } from "styled-components";
// ~~
export default class MyDocument extends Document {
static getInitialProps = async (ctx) => {
const sheet = new ServerStyleSheet();
// ~~
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props =>
sheet.collectStyles(materialSheets.collect(<App {...props} />))
});
// ~~~
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
)
// ~~~
}
Next v9 이상에서는 getInitialProps
대신 getStaticProps, getStaticPaths, getServerSideProps
을 사용하도록 가이드하지만 편의상 getInitialProps
라고 사용했다.
react, vue같은 Client Side Rendering (CSR)의 경우는 useEffect, created 함수를 이용하여 data fetching을 한다.
서버사이드에서 실행하는 Next에서는 getInitialProps를 이용하여 data fetching 작업을 한다.
1. 하나의 페이지에서는 하나의 getInitialProps가 실행된다.
next에서는 _app -> page component 순으로 페이지가 렌더링된다.
만약 _app에 getInitialProps
를 정의했다면, 하위 컴포넌트에서는 getInitialProps
가 실행되지 않는다.
하위 컴포넌트에서도 getInitialProps
값을 반영하기 위해서는 아래와 같이 변경이 필요하다.
// app.ts
function MyApp({ Component, pageProps }) {
return <Component ponent {...pageProps} />;
}
MyApp.getInitialProps = async ({ Component, ctx }) => {
let pageProps = {};
// 하위 컴포넌트에 getInitialProps가 있다면 추가 (각 개별 컴포넌트에서 사용할 값 추가)
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
// _app에서 props 추가 (모든 컴포넌트에서 공통적으로 사용할 값 추가)
pageProps = { ...pageProps, extra: { id: 1234, property: 'what' } };
return { pageProps };
};
export default MyApp;
2. getInitialProps는 서버에서 실행된다
Web API(setTimeout, window.~, document.~)는 해당 함수 내에서 실행되지 않는다.
getStaticProps
getStaticProps
라는 함수를 export 하면 Next는 getStaticProps
에 의해 return되는 props를 build 타임에 pre-render한다.
해당 값은 빌드 시 고정되며, 빌드 이후에는 값 변경이 불가능하다.
다음과 같은 상황에서 보통 사용된다.
// code example
// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li>{post.title}</li>
))}
</ul>
);
}
// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do direct database queries.
export async function getStaticProps() {
const res = await fetch('https://.../posts'); // any data fetching library. ex) swr
const posts = await res.json();
return {
props: {
posts,
},
};
}
export default Blog;
getStaticPaths
dynamic routing을 사용하는 페이지에서 getStaticProps
라는 함수를 export 하면 Next는 빌드시 return된 모든 path를 정적으로 pre-render한다.
이곳에서 정의하지 않는 하위 경로는 접근해도 페이지가 뜨지 않는다.
다음과 같은 상황에서 보통 사용된다.
// code example
export async function getStaticPaths() {
return {
paths: [
{ params: { ... } }
],
fallback: true // false or 'blocking'
};
}
getServerSideProps
getServerSideProps
라는 함수를 export 하면 Next는 getServerSideProps
에 의해 return되는 props를 build 타임에 pre-render한다.
데이터를 미리 렌더링할 필요가 없는 경우나 데이터가 자주 변경되는 경우에는 클라이언트 측에서 데이터를 가져오는(useEffect 사용) 것을 고려해야 한다.
다음과 같은 상황에서만 사용된다.
You should use getServerSideProps only if you need to render a page
whose data must be fetched at request time.
This could be due to the nature of the data or properties of the request
(such as authorization headers or geo location).
Pages using getServerSideProps will be server side rendered at request time and
only be cached if cache-control headers are configured.
If you do not need to render the data during the request, then you should consider fetching data on the client side or getStaticProps.
// code example
function Page({ data }) {
// Render data...
}
// This gets called on every request
export async function getServerSideProps() {
const res = await fetch(`https://.../data`);
const data = await res.json();
// Pass data to the page via props
return { props: { data } };
}
export default Page;
// 캐시하는 방법
export async function getServerSideProps({ req, res }) {
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
);
return {
props: {},
};
}
getInitialProps
가 있다면 실행한다.getInitialProps
가 있다면 실행하고, pageProps들을 받아온다.getInitialProps
가 있다면 실행하고, pageProps들을 받아온다.https://oneroomtable.tistory.com/entry/%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EA%B3%BC-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94
https://velog.io/@ash3767/%EC%84%9C%EB%B2%84%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81
https://nextjs.org/learn/foundations/about-nextjs?utm_source=next-site&utm_medium=nav-cta&utm_campaign=next-website
https://kyounghwan01.github.io/blog/React/next/basic/