Next.js의 pre-rendering 탐구하기

dahyeon·2023년 1월 21일
1

Next.js가 제공하는 강력한 기능 중 하나는 pre-rendering이다. 팀 프로젝트에 Next.js를 적용하기로 한 이유도, 블로그 서비스라는 프로젝트의 특성 상 SEO가 중요했기 때문에 Next.js의 pre-rendering 기능을 통해 SEO를 위한 SSR을 쉽게 하고자 했기 때문이다. 하지만 이 pre-rendering이 어떻게 일어나고 있는지 제대로 이해하지 못하면 예상 외의 순간에서 오류를 마주치기도 한다.

이번 글에서는 Next.js의 pre-rendering을 제대로 탐구하고, 이를 통해 프로젝트에 SSR을 적용한 경험을 소개하고자 한다.


Pre-rendering vs Client-Side Rendering

Client-Side Rendering

Next.js의 공식 문서에 표현되어 있는 그림이다. React만 사용해서 만든 어플리케이션의 경우, 브라우저는 먼저 위와 같이 빈 껍데기인 HTML 파일을 받는데, 이후 Javascript 파일을 받고 실행하여 UI를 렌더링한다. 초기 렌더링이 사용자의 디바이스에서 일어나기 때문에 이를 client-side rendering이라고 한다.

따라서 자바스크립트 사용을 차단할 경우 아래와 같이 렌더링을 할 수 없게 된다.

Pre-rendering

Next.js에서 제공하는 pre-rendering은 아래와 같이 React 컴포넌트를 HTML로 생성해서 넘겨준다.

아래와 같은 간단한 코드를 Next.js를 사용해서 작성했을 때,

import { useState } from "react";

export default function Home() {
  const [count, setCount] = useState(0);
  return <div onClick={() => setCount(count + 1)}>카운트 값은 {count} 입니다.</div>;
}

자바스크립트 사용을 차단하더라도 아래 그림과 같이 렌더링 된 컴포넌트를 확인할 수 있다.

물론 자바스크립트 사용을 차단했기 때문에 클릭하더라도 카운트 값은 올라가지 않는다.

Pre-rendering은 수행되는 시점에 따라 다음과 같이 두 종류로 나뉜다.

  • Static Site Generation(SSG)
    프로젝트를 빌드하는 시점에 HTML 파일이 생성된다. 어플리케이션이 배포되면, CDN에 HTML 파일을 저장해놓고, 매 요청마다 해당 파일을 보내주는 방식으로 배포된다.
    Next.js의 getStaticProps 함수를 통해 SSG에 필요한 데이터를 fetch 해올 수 있다.
  • Server-Side Rendering(SSR)
    런타임 시 매 요청이 올 때마다 페이지의 HTML 파일을 생성해서 넘겨준다. 클라이언트 측에서는 초기에 pre-rendering된 HTML 파일을 받고, 이후 JSON 데이터와 자바스크립트 파일을 통해 컴포넌트를 interactive하게 만드는데, 이를 hydration이라고 한다.
    Next.js의 getServerSideProps를 통해 SSR에 필요한 데이터를 fetch 해올 수 있다.

Next.js는 모든 페이지를 디폴트로 pre-rendering 한다.

더 정확히는, data fetching 없이 Static Generation을 통해 페이지를 pre-rendering 해 놓는다.

앞선 예시에 있던 코드의 경우 pre-rendering 시 data fetching이 필요없는 코드였고, 빌드 시점에 생성한 HTML 파일을 그대로 보내준 것이다.

Pre-rendering과 useState, useEffect

Next.js는 알아본 것처럼 모든 페이지를 default로 pre-rendering 해 놓는다.

  • 이 과정에서 useState를 사용했다면, useState에 명시한 초깃값으로 변수를 초기화한다. 앞서 코드에서 useState 훅을 사용해서 count 값을 0으로 초기화했는데, 초기화된 값으로 pre-rendering이 된 것을 확인할 수 있었다.
  • useEffect는 Next.js의 pre-rendering 시 실행되지 않는다. useEffect() 내부의 코드는 React에서 컴포넌트가 렌더링 된 이후에 브라우저에서 실행된다.
    따라서 Next.js 공식문서에 따르면, 클라이언트 측에서 data를 fetching 하기 위해서는 useEffect() 내부에 작성하거나 useSWR과 같은 data fetching 훅을 사용할 수 있다.

프로젝트를 진행하면서 이를 제대로 인지하지 못하고 작업을 하던 중 오류를 마주한 적이 있다.

세션 스토리지를 사용한 useSessionStorage 훅을 만들 때의 일이었는데, 해당 훅에서는 초기값을 설정할 때 sessionStorage에 해당 상태가 저장되어 있는지 확인하고, 있다면 저장된 값으로 업데이트하고 없다면 초기값으로 설정해준다.

초깃값을 설정해주는 코드는 처음에는 아래와 같이 작성했었다.

const [value, setStateValue] = useState<T>((initialValue)=>{
			const savedValue = sessionStorage.getItem(key);
			return savedValue ? JSON.parse(savedValue) : initialValue;
);

🤔 하지만 이 훅을 적용했을 때 sessionStorage is not defined라는 오류가 발생했다. 그 이유는 sessionStorage는 window의 프로퍼티인데, Next.js가 서버 사이드에서 위 코드를 실행할 때에는 window 객체가 없는 상태이기 때문이다.

따라서 아래와 같이 컴포넌트가 렌더링 된 이후 useEffect() 내부에서 세션스토리지를 확인해서 값을 바꿔주는 방식으로 구현하였다.

const useSessionStorage = <T>(key: string, initialValue: T) => {
  const [value, setStateValue] = useState<T>(initialValue);
  const [isValueSet, setIsValueSet] = useState(false);

  useEffect(() => {
    const savedValue = sessionStorage.getItem(key);
    if (savedValue) setStateValue(JSON.parse(savedValue));
    setIsValueSet(true);
  }, []);

  const setValue = (newValue: T) => {
    setStateValue(newValue);
    sessionStorage.setItem(key, JSON.stringify(newValue));
  };

  return { value, isValueSet, setValue };
};

Next.js가 pre-rendering을 디폴트로 수행하고 있기 때문에 window의 프로퍼티를 사용할 때는 이를 고려해서 코드를 작성해야 한다.


프로젝트에 SSR 적용하기

SSR을 통해 검색 엔진 최적화하기

Next.js에서 SSR을 통해 SEO를 한 경험은 해당 링크에 포스팅하였다.

여기서는 간단히 소개하자면 SSR을 통해 meta tag를 작성할 수 있다.

  1. getServerSideProps로 article 데이터 받아오기
export const getServerSideProps: GetServerSideProps = async (context) => {
  const [bookId, articleId] = context.query.data as string[];
  const article = await getArticleApi(articleId);

  return { props: { article } };
};
  • getServerSideProps는 서버에서 동작하는 코드이므로, 함수 내부에서 next/router를 사용할 수가 없다. 공식 문서에 따르면 context를 사용해야 한다. context 객체 내의 query에서 dynamic route의 query parameter를 가져올 수 있다.
  1. 가져온 데이터로 title 및 메타 태그 완성하기
import Head from 'next/head';

interface ViewerHeadProps {
  articleTitle: string;
  articleContent: string;
}

export default function ViewerHead({ articleTitle, articleContent }: ViewerHeadProps) {
  return (
    <Head>
      <title>{articleTitle}</title>
      <meta name="description" content={articleContent.slice(0, 150)} />
      <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      <meta property="og:title" content={articleTitle} />
      <meta property="og:description" content={articleContent.slice(0, 150)} />
      <meta property="og:type" content="website" />
      <meta property="og:url" content="https://www.knoticle.app" />
      <meta property="og:image" content="https://kr.object.ncloudstorage.com/j027/knoticle.png" />
    </Head>
  );
}

그 외에도 SSR을 통해 동적으로 sitemap을 생성할 수 있다.

현재 프로젝트에서 cookie에 유저의 토큰 정보가 담겨있는데, 특정 데이터를 가져올 때 이 토큰을 통해 확인할 수 있는 유저의 아이디가 필요했다. 하지만 따로 설정해주지 않고 getServerSideProps 내에서 데이터를 가져오게 되면 쿠키에 아무것도 담기지 않기 때문에, 데이터를 적절히 가져올 수 없었다.

찾아보니 getServserSideProps 내에서도 cookie에 접근할 수 있는 방법이 있었다.

  • getServerSideProps 내에서 cookie 가져오기 아래 코드와 같이 context.req.cookies로 가져올 수 있다.
    export const getServerSideProps: GetServerSideProps = async (context) => {
      const [bookId, articleId] = context.query.data as string[];
      const { access_token, refresh_token } = context.req.cookies;
      const article = await getArticleApi(articleId);
      const book = await getBookApi(bookId, { access_token, refresh_token });
    
      return { props: { article, book } };
    };
  • 가져온 cookie는 아래와 같이 요청을 보낼 때 header에 설정을 해주어야 한다.
    • Syntax

      Cookie: name=value; name2=value2; name3=value3
    • 적용 예시

      const response = await api({ url, method: 'GET', header: { Cookie: `access_token=${token.access_token}; refresh_token=${token.refresh_token}`} });

참고자료

Learn | Next.js
Cookie - HTTP | MDN
How to use cookie inside getServerSideProps method in Next.js?

profile
https://github.com/dahyeon405

0개의 댓글