Next.js는 React를 Server Side Rendering할 수 있게 해주는 프레임워크다. SSR의 장점은 첫 렌더링 화면이 빠르고 검색 엔진 최적화에 유리하므로 검색에 노출이 필요한 서비스에서 SSR은 필수적이라고 할 수 있다. 그리고 이후부터는 CSR 방식으로 동작하여 SSR의 장점과 CSR의 장점을 모두 갖고 있는 프레임워크라고 할 수 있다.
물론 React에서 SSR이 불가능한 것은 아니지만 러닝커브가 높기에 나의 경우 역시 위의 기능을 지원한 것과 더불어 Next.js로 마이그레이션하기로 결정했다. 그리고 이 외에도 Next.js에는 더 많은 기능을 지원하고 있기에 React로 프로젝트를 진행하던 중 Next.js로 마이그레이션하기로 결정했다.
React 사용하면서 lazy, Suspense를 통해 Code Splitting을 하는데 이 때 chunk 파일을 불러오기까지 로딩 컴포넌트를 보여준다.
Next.js는 Automatic Code Splitting
을 제공한다.
페이지 이동을 할 땐 목적지 페이지에 필요한 chunk만 추가 로드하여 성능을 최적화한다.
또한 LinK 컴포넌트를 이용해서 Link 컴포넌트가 viewport와 교차하면 href로 연결된 페이지의 chunk 파일을 preload하여 앞서 말한 문제점에 대해 좀 더 나은 사용자 경험을 제공할 수 있다.
앞서 작성했던 블로그 글에서 점진적 향상 기법으로 WebP 사용하기, 좋은 사용자 경험을 위해 CLS 해결하기를 작성했는데 Next.js는 이러한 문제를 바로 해결해준다.
Next/Image 컴포넌트에는 다음과 같은 기능이 있다.
lazy loading
을 통해 스크린 안에 있는 이미지만 로드하여 빠른 페이지 로드가 가능하다.이미지 사이즈 최적화
를 통해 디바이스 크기 별로 디바이스에 맞는 이미지를 다운로드할 수 있게 지원한다.placeholder
를 통해 Layout Shift를 방지하여 CLS 최적화할 수 있다.WebP
와 같은 용량이 작은 포맷으로 이미지를 변환하여 제공한다.이러한 디바이스에 맞는 이미지 사이즈를 만들고, 용량이 작은 WebP 포맷으로 변환하는 등의 작업은 Next.js 백그라운드에서 이미지 최적화가 진행되어 좋은 사용자 경험을 제공할 수 있다.
SSR은 서버에서 페이지를 렌더링해서 클라이언트에 전달해주는 방식이다. 사용자가 해당 페이지를 요청할 때마다 HTML 페이지가 생성된다. 이러한 SSR을 사용하기 위해서는 매 요청마다 서버에 의해 호출되는 getServerSideProps 함수를 export해야 한다.
const dynamicData = await fetch('https://...', { cache: 'no-store' });
로 작성해야 한다.SSG는 Next.js에서 사용자가 요청하는 시점이 아닌 빌드 시에 페이지를 미리 생성하여 제공한다. Data Fetching이 필요한 경우에 getStaticProps
라는 함수를 export 하여 함수 내에서 데이터를 받아서 리턴하면 빌드 시에 getStaticProps
가 실행되고 리턴하는 값을 컴포넌트에서 받아서 페이지를 미리 렌더링하게 된다.
SSG는 빌드 시에 HTML을 만들고 요청 시 재사용하는데 SSR은 각각의 요청 시 HTML을 만들어 렌더링된다. 기본적으로 SSG가 빌드 타임에 만들어져 높은 성능을 가지기 때문에 SSG를 사용하는 것이 좋지만 상황에 따라 잘 구분해야 사용해야할 필요가 있다.
const staticData = await fetch('https://...', { cache: 'force-cache' });
로 작성해야 한다.ISR은 빌드 시점에 페이지를 렌더링한 후, 설정한 시간마다 페이지를 새로 렌더링한다. 즉, ISR로 구분했지만 SSG에 포함된 개념으로 revalidate
값으로 설정하여 시간을 설정할 수 있다.
위의 개념과 이어지는 설명이다. Next.js는 기본적으로 Pre-render로 더 좋은 성능과 검색봇이 해당 페이지를 읽어갈 수 있어 SEO를 기대할 수 있다. 더불어 Next.js에는 Next/Head 컴포넌트를 지원한다. 여기에 title이나 image, description과 같은 og(open graph) 태그와 함께 사용하면 이 때 생성된 HTML을 통해 검색 엔진으로부터 인식이 될 수 있도록 최적화하는 작업을 할 수 있다.(React의 경우는 react-helmet 라이브러리 도움을 받을 수 있다.)
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
이제부터 npm run dev
로 개발환경으로 돌리면 에러들을 열심히 발견할 수 있다ㅎㅎ
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
useErrorBoundary: true,
},
},
});
function App() {
return (
<Provider store={store}>
<ThemeProvider theme={theme}>
<GlobalStyle />
<QueryClientProvider client={queryClient}>
<Router />
<ReactQueryDevtools />
</QueryClientProvider>
</ThemeProvider>
</Provider>
);
}
export default App;
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Layout } from 'components';
import { AppProps } from 'next/app';
import Head from 'next/head';
import { Provider } from 'react-redux';
import { store } from 'store';
import { ThemeProvider } from 'styled-components';
import GlobalStyle from 'styles/globalStyle';
import theme from 'styles/theme';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
useErrorBoundary: true,
},
},
});
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>HOW ABOUT OOTD</title>
</Head>
<Provider store={store}>
<ThemeProvider theme={theme}>
<GlobalStyle />
<QueryClientProvider client={queryClient}>
<Layout>
<Component {...pageProps} />
</Layout>
</QueryClientProvider>
</ThemeProvider>
</Provider>
</>
);
}
_app.tsx
는 Next.js에서 서버로 요청이 들어왔을 때 가장 먼저 실행되는 컴포넌트로, 페이지에 적용할 공통 레이아웃 역할을 한다.
_app.tsx
파일은 우리가 만든 모든 페이지가 초기화될 때 로딩되는 파일이기 때문에 전체 웹 애플리케이션에서 우리가 원하는 방식과 로직으로 페이지를 초기화할 수 있게 해준다. 즉, 초기화 로직을 컨트롤할 수 있게 도와주는 파일이다.
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head>
<meta charSet='utf-8' />
<link rel='icon' href='/favicon.ico' />
<meta name='theme-color' content='#000000' />
<meta name='description' content='Web site created using create-react-app' />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
_document
파일은 _app
다음에 실행된다._document
파일은 각 페이지가 초기화될 때 HTML 페이지 중 Document(<html>
이나 <body>
등) 부분에 오버 라이딩을 제공한다. 그리고 CSS-in-JS의 서버 사이드 렌더링을 위한 설정을 할 때 사용한다.styled-components가 적용되지 않은 것을 확인할 수 있다.
SSR의 경우 자바스크립트 코드가 적용이 되지 않은 페이지가 미리 렌더링되기 때문에 CSS-in-JS
로 스타일링을 하면 스타일이 적용되지 않은 HTML 마크업이 먼저 렌더링되는 문제가 발생한다.
Next.js에서는 이에 대한 해결책으로 HTML
파일에 CSS-in-JS
형식으로 작성된 스타일 요소들을 주입시켜 스타일이 뒤늦게 적용되는 문제를 해결할 수 있다.
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html lang='ko'>
<Head>
<meta charSet='utf-8' />
<link rel='icon' href='/favicon.ico' />
<meta name='theme-color' content='#000000' />
<meta name='description' content='how about ootd' />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
styled-components가 적용된 HTML을 확인할 수 있다.
import { Link } from 'react-router-dom';
import Link from 'next/link';
import Image from 'next/image
<img src={url} alt='' />
// width와 height props를 설정해주거나 fill props를 설정해줘야 한다.
<Image src={url} alt='' />
만약 위 코드처럼 react-router-dom에서 불러온 Link 태그를 사용했다면 아래처럼 바꿔주고, props도 to
에서 href
로 바꿔줘야 한다.