본문 바로가기

프론트엔드/Next.js

Next.js에서 SSG 와 revalidate 사용하기

  • 프로젝트: 공우 홈페이지
  • 키워드: Next.js, SSG, getStaticPaths, getStaticProps
  • 상황
    1. 공지사항 또는 뉴스(이하 포스트로 통일) 페이지(https://www.gongwoo.snu.ac.kr/notice, https://www.gongwoo.snu.ac.kr/news)에서 포스트 목록을 보여준다.
    2. 포스트를 클릭하면 news/123 같은 상세 페이지로 이동한다.
    3. 동아리 홈페이지이므로 SEO 최적화를 해야 한다. 따라서, 포스트의 상세 페이지들도 static 하게 build 해야 한다.
    4. 포스트 내용이 정적이기 때문에, 매 요청마다 html을 만드는 SSR 대신 getStaticProps를 사용하여 SSG로 구현한다.
    5. 배포 후 새로운 포스트가 만들어질 경우에도 해당 포스트에 대한 html이 자동으로 만들어져야하고, 포스트가 없다면 404페이지를 보여줘야한다.
    6. 기존의 포스트 내용이 수정되는 경우, 이미 만들어져있던 html을 그대로 쓰면 안되고 수정된 내용으로 업데이트 해야한다.
  • 해결방법
    1. 최초로 프로젝트를 빌드하는 시점에 이미 DB에 존재하는 포스트들은 미리 pre-render 해둔다.
      이 때, Next.js가 지원하는 getStaticPaths(https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation)를 사용하여 pre-render 할 페이지의 목록을 알려준다.
        export async function getStaticPaths() {
        // 뉴스(type === 1) 목록을 불러오는 graphql 쿼리
        const { data } = await client.query({
          query: gql`
            query {
              getPostsPage(input: { type: 1 }) {
                result {
                  id
                }
              }
            }
          `,
        });
      
        const paths = data.getPostsPage.result.map((news: News) => ({ params: { id: String(news.id) } }));
        return { paths, fallback: 'blocking' };
      }
      
      /** Next.js 공식문서
      export async function getStaticPaths() {
        return {
          paths: [
            { params: { ... } } // See the "paths" section below
          ],
          fallback: true, false, or 'blocking' // See the "fallback" section below
        };
      }
      */
      뉴스의 id 속성을 paths 에 넘기면 해당 id들은 build time에 pre-render 된다.

      fallback: 'blocking' 은 build time에 만들어지지 않은 페이지에 대한 요청이 들어왔을 때, 새로운 html을 만들기 전까지 페이지 이동을 blocking 하겠다는 뜻이다. (예를 들어 news/124 라는 새로운 페이지(build time에 pre-render하지 않은)에 접근했을 때, news/124 에 해당하는 html이 만들어지기 전까지는 뉴스 목록페이지에서 로딩 상태로 blocking 되어있다)

    2. path(url)가 만들어졌으므로 각 url마다 필요한 포스트 내용을 GET 한 뒤  pre-render 해야한다. Next.js가 지원하는 getStaticProps 를 사용하여 페이지에서 사용할 'props'를 return한다.(https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation)
      이 때, 포스트가 존재하지 않는다면(data.getPost === null) notFound를 return하여 404페이지로 이동시킨다.
        export async function getStaticProps({ params }: { params: { id: string } }) {
        const id = Number(params.id);
        const { data } = await client.query({
          errorPolicy: 'ignore',
          query: gql`
              query {
                getPost(id: ${id}) {
                  id
                  type
                  title
                  body
                  createdAt
                }
                getPrevPost(input: { type: 1, id: ${id} }) {
                  id
                  title
                }
                getNextPost(input: { type: 1, id: ${id} }) {
                  id
                  title
                }
              }
            `,
        });
      
        if (data.getPost === null) {
          return {
            notFound: true,
          };
        }
      
        return {
          props: {
            news: data.getPost,
            prev: data.getPrevPost,
            next: data.getNextPost,
          },
        };
      }
      
      /** Next.js 공식문서
      export async function getStaticProps(context) {
        return {
          props: {}, // will be passed to the page component as props
        }
      }
      */​
    3. 일단 html이 만들어지면 SSG 특성상 다음 요청부터는 만들어진 html을 그대로 응답한다. 하지만 배포 후 DB에서 포스트 내용이 수정될 경우, html을 갱신해야 올바른 페이지를 볼 수 있다.
        return {
        props: {
          news: data.getPost,
          prev: data.getPrevPost,
          next: data.getNextPost,
        },
        revalidate: 10, // seconds
      };
      이때 revalidate 속성을 추가하면 html을 regenerate 할 수 있다.
      html이 만들어지고 10초 동안은 사용자에게 같은 html파일을 그대로 응답한다. 10초가 지나고 GET 요청이 오면, 기존 html을 응답하면서 동시에 새로운 html을 regenerate 한다. 그리고 다음 GET 요청부터는 갱신된 html 문서를 응답한다.
    4. 포스트 상세페이지에 revalidate 옵션을 추가하여, 어드민 사이트에서 포스트 내용을 수정해도 홈페이지의 html이 자동 갱신되도록 했다!
  • Known issues
    1. 랜딩페이지는 3종류(뉴스, 공지사항, 멤버)의 큰 데이터를 getStaticProps의 응답값으로 반환한다. 이 때 위에서 기술한 내용과 똑같은 방식으로 revalidate 옵션을 적용해도 정상 작동하지 않는 버그가 있다.