Next.JS를 현명하게 사용하는 방법

우빈·2023년 6월 2일
14
post-thumbnail

필자의 경험

필자는 교내 학생이나 선생님, 여러가지 사건/사고 등을
나무위키나 위키백과 등의 문서처럼 문서화시켜 학생들이
자유롭게 문서를 수정할 수 있는 교내 위키를 개발해본 적이 있다.

처음 만들 때에는 이를 React와 TypeScript를 이용해서 작성했는데,
검색 엔진 최적화가 자동으로 되지 않는다는 단점도 있고, 그 외에도 여러가지의
작은 버그나 이슈들이 많이 있었다.

그래서 내 팀원과 함께 이를 Next.JS로 마이그레이션 하는 겸, 코드까지
같이 리팩토링해서 안정화된 위키 서비스로 업그레이드시키자고 마음 먹었다.

그러나...

그렇게 처음 SSR과 SSG에 대해 공부하고나서 막상 비주얼 스튜디오 코드를 켜서
"적용해보자!"라고 생각하고 코드를 본 순간 뇌가 멈춰버렸다.
"그런데, SSR이 더 좋을까, SSG가 더 좋을까?"라는 생각이 들며 내 뇌는
무한루프를 돌리다가 시간 초과로 메모리가 터져버렸던 적이 있다^^...

왜 공부할 때 이를 자세하게 알려주는 블로그를 찾지 못했을까, 결국 같은 학교의
선배님께 조언을 구하고 차차 적용시켜보며 프로젝트를 완성해갔었다.

위키에는 SSG를 사용했었는데, 이를 잘못되게 사용해 굉장히 애를 많이 먹었다.
서론이 조금 길었다. 이건 위키 회고록을 따로 쓴 다음 업로드하는 것으로 하고,
일단 SSR과 SSG가 왜 필요한지에 대해 이야기해보겠다.

SSR과 SSG는 왜 필요할까

성능 측면에서 Next.JS를 사용하는 이유는, 단순히 성능 개선이나
사이트 속도 개선도 있겠지만, 주목적은 검색 엔진 최적화, 즉 SEO이다.

SSR과 SSG는 공통적으로 브라우저에 컴포넌트들이 rendering 되기 전
먼저 rendering 과정을 진행 하여 빠르게 화면에 보여주는 방식을 사용한다.

여러 브라우저에는 크롤링 봇들이 떠돌아다니며 웹사이트 정보를 수집하는데,
CSR과 SSR/SSG는 정보를 수집할 때 어떤 차이가 있을까? 한번 알아보자.

크롤링 봇이 CSR 웹사이트를 수집할 때

크롤링 봇이 웹사이트 정보를 수집할 때, 만약 클라이언트 사이드 렌더링을
하는 웹사이트를 크롤링한다고 생각해보자.

만약 봇이 사이트에 접근해 정보를 다 수집한다고 생각해보자. 그 때
클라이언트 사이드 렌더링이라면, 빈 페이지가 먼저 보인 다음, 그 후에
컴포넌트들과 웹사이트의 여러가지 정보들이 로딩될 것이다.

그렇게 되면 크롤링 봇은 결국 빈 웹사이트를 수집하게 된다..
반대로 SSR과 SSG는 어떨까?

크롤링 봇이 SSR/SSG 웹사이트를 수집할 때

SSR/SSG는 Pre-render 방식을 사용하기 때문에 웹사이트를 접근했을 때
웹사이트 내에 바로 모든 정보들이 담겨있다. 따라서 크롤링 봇이 접근하면
CSR처럼 빈 웹페이지가 아닌, 모든 정보들이 들어있는 속이 꽉 찬 웹사이트를
크롤링해가는 것이고, 그렇게 되면 검색 엔진이 더욱 최적화되어 검색 결과에
더욱 상단에 위치하게 되는 원리이다!

물론 CSR이어도...

구글이나 네이버 등 여러가지 브라우저에서는 따로 Search Console 등의
검색 엔진을 최적화하는 서비스를 제공하긴 한다. 이를 통해 CSR에서도 SEO와
관련된 문제를 해결할 수는 있지만, 이는 귀찮은 작업이 하나 늘어나는 꼴이 되기도 한다.

또한 SSR/SSG로 이미 크롤링을 한번 한 사이트에서 Search Console을 통해
더 많은 웹사이트 정보를 수집한다면, CSR보다 더욱 많은 정보들이 수집되지 않을까?

이런 이유에서

SSR과 SSG를 많이 사용하고, 요즘 Next.JS가 급격히 유행을 타고 있다고 생각한다.
항상 리액트를 고집하던 내 친구도 최근에 Next.JS로 프로젝트를 만들기 시작했다.

그렇다면 SSR과 SSG가 좋은 건 알겠는데, 둘을 언제 어디서 어떻게 사용해야 할까?

SSR과 SSG의 차이

먼저 SSG는 Next App을 build할 때 모든 문서들이 static으로 생성된다.
만약 해당 페이지에 대한 요청이 발생하게 되면, 이 페이지들을 재생성하거나
새로 렌더링하여 보여주는 것이 아닌, 빌드 과정에서 이미 생성된 페이지를
사용자에게 보여주는 형태로 동작한다.

SSR은 반대로, 사용자에게 페이지를 요청할 때마다 그에 맞는 HTML 문서를
pre-render 방식으로 생성해서 사용자에게 보여주는 형태로 동작한다.

SSR과 SSG의 공통점

SSR과 SSG는 둘 다 pre-render 방식을 사용한다는 공통점이 있다.
허나 여기서 이를 정적으로 수행하는지, 동적으로 수행하는지에 있어
SSR과 SSG의 차이점이 나타나는 것이다.

이렇게 좋고 매력적인 SSR과 SSG에도 단점은 있기 마련이다.
먼저 SSG를 사용하며 미친듯이 애를 먹은 필자는 할 말이 많기 때문에,
SSR의 단점에 대해 먼저 짧게 설명하고 가겠다.

SSR의 단점

SSR은 한 페이지를 이동할 때마다 서버에서 페이지를 생성하는 방식이다.
벌써 약간 문제점이 보이지 않는가? 클라이언트 사이드 렌더링과 다르게,
서버에서 페이지를 생성하는 데에 시간이 걸리기 때문에 TTFB가 느릴 수 있다.

따라서 페이지 렌더링이 너무 무거우면, 오히려 클라이언트 사이드 렌더링의
사용자 경험보다 좋지 않은 경험을 사용자에게 제공해버리게 될 수도 있다.

또한 매 페이지를 로딩할 떄마다 서버에 요청하게 되어 서버에 부하가 올 수 있다.
결론은 꼭 SSR이라고 해서 다 좋은 것은 아니며, 제공하려는 어플리케이션의
유형에 맞게 CSR과 SSR, SSG를 적절히 사용해야 한다.

그렇다면 SSG의 단점은 무엇일까?

SSG의 단점

SSG로 구성된 작은 게시판 서비스가 하나 있다고 생각해보자.
필자는 그 게시판에 "안녕하세요!"라는 제목에, "반갑습니다~"라는 내용을
적어 그 서비스에 글을 하나 업로드했다.

허나 업로드 하고 나서, 필자가 글의 내용을 "환영합니다~"라고 바꾸고 싶어져
글 수정 버튼을 누른 다음 글의 내용을 다음과 같이 바꾸었다.

다음 시나리오에서 어떤 문제가 발생할까?
바로 SSG에서는 Next가 build를 진행할 때 페이지를 캐싱해놓은 상태이기 때문에,
페이지 내 어떤 데이터의 생성이나 수정, 삭제가 일어난다고 해도 바로 적용이 되지 않는다.

이렇게 된다면 굉장히 난감한 상황이 벌어질 수 있다..!!
이 문제를 해결하기 위해 나온 파생된 개념이 ISR이다.

ISR로 페이지 재생성하기

ISR은 SSG와 같은데, 다른 특징은 개발자가 정해놓은 시간마다
서버에서 페이지를 다시 재생성한다는 특징이 있다.
간단하게 ISR을 적용한 SSG 코드를 하나 보자.

export const getStaticProps: GetStaticProps = async () => {
  const res = await axios.get('api');

  return {
    props: { 
      data: res.data 
    },
    revalidate: 20,
  };
};

다음과 같이 값을 return할 때 revalidate라는 속성을 같이 넣어주고,
그 속성에 원하는 시간 (초 단위)를 넣어주면 ISR이 적용된다.
위 코드는 20초에 한 번씩 페이지를 재생성하는 코드가 된다.

허나 이도 그리 명쾌하진 못하다. 만약 리렌더링이 되는 시간이 지나자 마자,
한 3초 쯤에 사용자가 게시글을 업로드했다면, 결국엔 자신의 글을 확인하려면
17초를 기다려야하며, 만약 20초가 아니라 revalidate 시간이 더 길다면,
그 시간까지 계속 페이지가 리렌더링이 안된다는 문제점이 발생할 것이다.

그럼 이를 명쾌하게 해결할 방법은 없을까?
내가 공부할 때 이를 설명한 블로그는 찾지 못해서 아쉬웠는데, 한번 알아보자.

revalidate API

Next.JS에서는 프레임워크 내에서 Node.JS 코드를 작성할 수 있다.
이를 이용해서 만약 페이지가 생성/수정/삭제되었을 때만 해당 페이지를
revalidate 시켜주면 정말 이상적이지 않을까?

이를 Node.JS로 해결할 수 있다!
Next.JS의 pages/api에서 revalidate.ts(js)라는 파일을 생성해보자.

// pages/api/revalidate.ts

import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
	if (req.method !== 'POST') {
		return res.status(400).json({ error: 'Invalid HTTP method. Only POST method is allowed.' })
	}
	try {
		await res.revalidate(`/detail `)
		return res.json({ revalidated: true })
	} catch (err) {
		return res.status(500).send('Error revalidating')
	}
}

다음과 같이 next api response에는 revalidate라는 함수를 제공한다.
함수를 호출하면, 해당하는 페이지 즉 저 예제로 봤을 때 detail 페이지만
Next.JS의 서버 단에서 페이지를 재생성하게 된다.

문서를 업데이트할 때 라우터를 명시해준 다음 이 revalidate 로직을 실행하면,
굉장히 효율적으로 변화가 생긴 문서만 리렌더링할 수 있게 된다!

const onUpdatePost = () => {
 	try {
      axios.put('url');
      axios.post('/api/revalidate', {
        postId: 3
      })
    } catch (err) {
      console.log(err)
    }
}

이런 식으로 SSG를 사용할 때에 revalidate api를 통해서
ISR보다 더욱 효율적인 서비스를 사용자에게 제공할 수 있다는 장점이 있다!

사용할 때에는?

그렇다면 SSR과 SSG는 각각 어떨 때에 사용해야 할까?

SSG는 서비스하는 어플리케이션 내에서 데이터의 변동이 잘 일어나지
않을 때 많이 사용한다.

SSR은 지속적으로 변동이 일어나는 서비스를 사용자에게 제공할 때
많이 사용된다.

하지만 이들을 극단적으로 한 어플리케이션 내에서 한 렌더링 방식만
사용하지 않고, 변경이 잘 일어나지 않는 페이지는 SSG를, 잘 일어나거나
빠른 인터랙션이 필요한 페이지는 SSR과 CSR을 섞어 사용하는 Universal
렌더링을 고려하는 것도 많은 도움이 된다.

이런 렌더링 방식들은 처음에 어플리케이션을 설계할 때 적절하게 회의를 통해
같이 협업하는 팀원들과 지정을 하고 설계하면 더욱 업무가 수월해질 것 같다.
필자는 SSR과 SSG를 배움과 동시에 사용했기에 여러가지의 혼란이 많았다^^..

그럼 우리, 이제 Next.JS를 더 현명하게 사용하는 개발자가 되보도록 하자!

profile
프론트엔드 공부중

0개의 댓글