사전 렌더링과 데이터 패칭 (with. SSR, SSG, ISR)

정호성·2025년 2월 15일

Next.js

목록 보기
5/18
post-thumbnail

기존 React App에서의 Data Fetching

export default function Page() {
  // 1. 데이터를 보관할 state 생성
  const [state, setState] = useState();
  
  // 2. 데이터 패칭 함수 생성
  const fetchData = async () => {
    const response = await fetch("...");
    const data = await response.json();
    
    setState(data);
  };
  
  // 3. 컴포넌트 마운트 시점에서 데이터 패칭 함수 호출
  useEffect(() => {
    fetchData();
  }, []);
  
  // 4. 데이터가 로딩 중일 때 예외처리
  if(!state) return "Loading...";
  
  return <div>...</div>;
}

위와 같은 기존의 리액트 앱에서의 데이터 패칭 방식은 큰 단점이 있다.
바로 백엔드 서버로부터 불러온 데이터가 화면에 나타나기까지 걸리는 시간이 매우 오래 걸린다는 것인데,
이유는 백엔드 서버에 보내는 데이터 요청이 컴포넌트가 마운트된 이후에나 발생하기 때문에 데이터 요청 자체를 늦게 시작해 불러오는 속도도 당연히 느려지게 되는 것이다.

결국 사용자 입장에서는 가뜩이나 느린 리액트 앱의 FCP이후에 또 한 번 데이터를 기다려야되는 시간이 추가되는 꼴이다.

Next.js에서의 Data Fetching

Next.js는 사전 렌더링을 통해 서버에서 렌더링된 HTML을 브라우저에 전달해주는데 이 과정에서
백엔드 서버로부터 현재 페이지에 필요한 데이터를 미리 불러오도록 설정할 수 있다.

따라서 리액트 앱의 데이터 패칭보다 훨씬 더 빠른 타이밍에 백엔드 서버에서 데이터를 요청하고 받아오기 때문에 서버가 브라우저에게 전달하는 HTML파일에는 서버 데이터가 포함되어 있으니 결국 사용자에게 데이터 패칭이 이미 완료된 페이지를 추가적인 로딩 없이 한 방에 바로 보여줄 수 있다는 장점을 가지고 있다.

요약

React App의 데이터 패칭Next App의 데이터 패칭
- 컴포넌트 마운트 이후에 발생함
- 데이터 요청 시점이 느려지게 되는 단점 발생
- 사전 렌더링중 발생함(당연히 컴포넌트 마운트 이후에도 발생 가능)
- 데이터 요청 시점이 매우 빨라지는 장점 있음

Next.js의 다양한 사전 렌더링 : SSR, SSG, ISR

그런데 만약에 Next.js에서 데이터 패칭이 진행될 때 백엔드 서버 상태가 매우 좋지 않다거나
불러와야할 데이터의 용량이 엄청 크다거나 하는 등의 이슈로 사전렌더링 중 데이터 요청이 너무 오래걸리게 된다면,
사용자는 아무런 화면도 볼 수 없는게 아니냐는 의문이 들 수도 있다.
Next.js는 이러한 문제점을 해결하기 위해 사전렌더링이 오래걸릴 것으로 예상되는 페이지는
빌드 타임(build time)에 미리 사전렌더링을 진행하는 등의 다양한 사전 렌더링 방식을 제공한다.

서버 사이드 렌더링 (SSR): Server Side Rendering

  • 가장 기본적인 사전 렌더링 방식
  • 요청이 들어올 때마다 사전 렌더링을 진행함
  • getServerSideProps() 라는 함수를 통해 구현할 수 있다.

SSR의 장점과 단점

장점 : 페이지 내부의 데이터를 항상 최신으로 유지할 수 있다.
단점 : 데이터 응답 속도가 느려지면 모든 것이 늦어지게 된다.


페이지 역할을 하는 파일 안에 getServerSideProps()라는 Next.js에서 제공하는 함수를 작성하고
export하면 이 페이지는 SSR로 동작하도록 설정된다.

// SSR을 적용하고 싶은 페이지
export const getServerSideProps = () => {
  const data = "백엔드에서 불러온 데이터";

  return {
    props: {
      data,
    },
  };
};

export default function Page({ data }: InferGetServerSidePropsType<typeof getServerSideProps>) {
  console.log(data);

  // ...
}

getServerSideProps()는 해당 페이지에 접근했을 때 페이지 컴포넌트보다 먼저 실행되어
해당 페이지에 필요한 데이터를 불러오는 역할을 한다. 데이터를 불러온 다음 페이지 컴포넌트가 실행된다.

  • getServerSideProps()에서 props라는 속성을 가진 객체를 반환해주면 Page 컴포넌트로 해당 props를 전달해준다.
  • getServerSideProps()의 return값은 반드시 props라는 객체 프로퍼티를 포함하는 단 하나의 객체여야 한다.
  • getServerSideProps()를 통해 전달받은 props의 타입은 Next.js가 제공하는 내장타입 중에 InferGetServerSidePropsType라는 타입으로 명시하고 제네릭으로
    <typeof getServerSideProps>을 넣어준다. 이 타입은 getServerSideProps()의 반환값 타입을 자동으로 추론해주는 기능을 하는 타입이다.

또 한가지 주의할 점

페이지 역할을 하는 Page 컴포넌트는 사전 렌더링 과정에서 서버에서 먼저 한번 실행된 후 브라우저에서 js번들 형태로 전달될 때, 즉 하이드레이션 과정이 진행될 때 한번 더 실행된다.
따라서 Page 컴포넌트 안에서 어떠한 조건도 없이 window.location() 같은 코드를 작성하면 서버 측에서 실행될 때 window객체를 찾을 수 없기 때문에 오류가 발생한다.


이 때 useEffect를 사용하면 컴포넌트의 마운트 이후 실행되기 때문에 브라우저단에서만 사용할 수 있는 window객체를 사용할 수 있다.

getServerSideProps() 함수 안에서 query나 params 등 브라우저로부터 받은 요청에 대한 정보는 context 매개변수를 사용하면 된다. 이 때 타입은 GetServerSidePropsContext로 정의한다.

export const getServerSideProps = async (
    context: GetServerSidePropsContext,
) => {
    console.log(context.query);
    
    return {
        props: {},
    };
};

정적 사이트 생성 (SSG): Static Site Generation

  • SSR의 단점인 데이터 응답 속도가 느릴 경우를 보완하는 Next.js의 디폴트 사전 렌더링 방식
  • 빌드 타임에 페이지를 미리 사전렌더링 해 둠
  • 데이터가 자주 업데이트 되지 않아도 되는 정적인 페이지에 적합한 방식이다.
  • getStaticProps() 라는 함수를 통해 구현할 수 있다.

SSG의 장점과 단점

장점 : 사전 렌더링에 많은 시간이 소요되는 페이지더라도 사용자의 요청에는 매우 빠른 속도로 응답이 가능하다.
단점 : 매번 똑같은 페이지만 응답하기 때문에 최신 데이터를 반영하기 어렵다.
그러나 이 단점은 클라이언트 측 데이터 패칭을 사용하지 않고 서버측에서 모든 데이터를 불러오는 상황에만 국한된다.


SSG는 정적 사이트 생성이라는 뜻으로 빌드 타임에 미리 페이지를 사전 렌더링하는 특징이 있다.
따라서 빌드가 완료된 이후에 Next.js앱이 실제로 가동되었을 때 브라우저가 접속 요청을 보내게 되면 Next서버는 빌드 타임에 미리 만들어 두었던 페이지를 지체없이 매우 빠른 속도로 응답하기 때문에 사용자 또한 데이터 패칭이 완료된 페이지를 매우 빠르게 볼 수 있다.

이 이후의 과정은 기존 SSR의 방식과 동일하게 진행된다.
브라우저는 서버로부터 받은 HTML 페이지를 화면에 렌더링하고 Hydration을 위해 Next서버가 JS번들을 후속으로 전달하면 브라우저에서 이를 실행해서 Hydration을 완료 함으로써 상호작용이 가능한 완성된 페이지로 거듭나게 된다.

SSR에서 사용했던 getServerSideProps()와 마찬가지로 props라는 속성을 가진 객체를 반환해주면 Page 컴포넌트로 해당 props를 전달해준다. 이 때, 함수 이름은 getStaticProps로 명시한다.

props 타입 또한 동일한 방법인데 대신 InferGetStaticPropsType을 사용해주면 된다.

export const getStaticProps = async () => {
  console.log("인덱스 페이지")

  const [allBooks, recoBooks] = await Promise.all([fetchBooks(), fetchRandomBooks()]);

  return {
    props: {
      allBooks,
      recoBooks,
    },
  };
};

export default function Home({ allBooks, recoBooks }: InferGetStaticPropsType<typeof getStaticProps>) {
  // ...
}

위 코드 중 console.log부분은 개발모드에서는 개발 편의성을 위해서 코드 수정 결과를 화면에 바로 바로 반영하도록 하기 때문에 설정해둔 사전 렌더링 방식이 SSG든 SSR이든 요청을 받을 때마다 계속해서 새롭게 페이지를 사전 렌더링하게 된다.

실제로 SSG가 잘 동작하고 있는지 확인하기 위해서 프로덕션 모드로 실행하기 전에 빌드를 먼저 해보면,

Generating Static Pages와 함께 콘솔에 찍어뒀던 "인덱스 페이지"가 잘 출력된다.

동시에 페이지별 빌드 결과를 빌드 결과 최하단에서 확인할 수 있다.

  • ○(Static): prerendered as static content, SSG와 동일한 정적 페이지인데 getStaticProps를 설정해 두지 않았기 때문에 기본값으로 설정된 SSG페이지
  • ●(SSG): prerendered as static HTML (uses getStaticProps), HTML로 사전 렌더링된 페이지
  • ƒ(Dynamic / SSR): server-rendered on demand, 서버 측에서 페이지를 요청할 때마다 실시간으로 렌더링

빌드 결과에서 알 수 있듯이 페이지에 getServerSidePropsgetStaticProps를 설정하지 않으면 기본적으로 SSG페이지로 설정된다.

그런데, SSR을 적용하지 않은 /api/hello/api/time같은 API Routes에도 function 기호가 붙어있는데 이는 Next.js가 기본적으로 모든 API Routes들을 다이나믹하게, 즉 SSR로 작동하도록 설정해 두었기 때문이다.

또 한가지 주의할 점

getStaticProps()함수는 빌드 시점에 한 번만 실행되기 때문에 브라우저 환경에서 제공하는 queryprams는 context로 받을 수 없다.

따라서 서치 페이지 등 브라우저에 대한 정보가 필요한 페이지는 SSG방식으로 동작시킬 수가 없다.
더 엄밀히 말하자면, 쿼리스트링을 꺼내올 수 없기 때문에 검색 결과를 서버로부터 불러오는 동작은 수행할 수가 없다.
그래도 이런 페이지를 SSG로 동작시키길 원한다면 데이터를 불러오는 과정을 브라우저에서 실행되는 컴포넌트 안에서 작성해야한다.

그러나, 위와 같은 방식이 유리한 점도 있다.

SSR은 데이터 로딩 전까지 아무것도 화면에 표시되지 않아 사용자가 빈 화면을 보게 되고,
SSG는 최신 데이터를 브라우저에 반영할 수 없는 단점이 있다.

SSG + 클라이언트 사이드 데이터 패칭은 페이지 레이아웃과 로딩 바 등을 먼저 표시한 뒤, 클라이언트에서 데이터를 불러와 사용자에게 더 나은 로딩 경험을 제공한다.
이는 마치 식당에서 물과 밑반찬을 먼저 주는 것과 같아, 메인 메뉴가 늦어도 기다리는 경험을 덜 지루하게 만든다.

따라서 해당 페이지의 검색 결과가 CSR로 불러와지면 안 되는 상황이 아니라면 (SEO를 고려해야 한다거나 등...) SSG + CSR 방식을 차용하는게 가장 좋을 수 있다

동적인 페이지를 SSG로 사전렌더링 하기 위해서는 어떤 경로들이 존재할 수 있는지 설정하는 과정이 필요하다.
이 역할을 하는 함수가 바로 getStaticPaths()다.
paths 속성에 존재할 수 있는 모든 경로를 작성해주고, fallback 속성에는 paths에 작성한 이외의 경로로 접근했을 때 어떻게 처리할지 설정해줄 수 있다.

export const getStaticPaths = () => {
  return {
    paths: [
      { params: { id: "1" } },	// params의 값은 반드시 문자열
      { params: { id: "2" } }, 
      { params: { id: "3" } },
    ],
    fallback: false,	// 존재하지 않는 페이지는 not found 처리
  };
};

export const getStaticProps = async (context: GetStaticPropsContext) => {
  // ...
};

export default function Page({ book }: InferGetStaticPropsType<typeof getStaticProps>) {
  // ...
}

미리 설정한 1, 2, 3 id params를 가진 페이지들이 빌드타임에 사전렌더링 되는지 확인해보기 위해 빌드를 돌려보면

빌드 산출물이 들어있는 .next폴더 아래 server/pages/book경로에 잘 생성되어있는 것을 확인할 수 있다.

Fallback 옵션 설정 (없는 경로로 요청시)

  • false: 404 Not Found 반환
  • blocking: 즉시 생성 (Like SSR)
  • true: 즉시 생성 + 페이지만 미리 반환

false
없는 경로로 요청시 404 Not Found 반환

blocking
없는 경로로 요청시 즉시 생성 (Like SSR)

  • 빌드 타임에 모든 동적 경로를 불러오기 어려운 상황일 경우, 새로운 데이터가 계속 추가되어야 하는 경우 등에 사용 가능.
  • 첫번째 요청 때는 SSR 방식으로 페이지를 새롭게 생성해서 신규 데이터를 반영, 그 이후 동일한 경로는 캐싱되어 SSG 방식으로 빠른 접근.
  • 사용자는 서버가 응답을 줄 때까지 기다려야 함

true
즉시 생성 + 페이지만 미리 반환

bloking 옵션을 사용할 때 존재하지 않았던 페이지를 새롭게 생성하면서 페이지 생성 시간이 길어지면 브라우저에게 아무 응답도 주지 않기 때문에 로딩이 발생하게 된다.
이런 문제를 해결하기 위해 사용하는 옵션이 fallback: true이다.

true 옵션을 사용하면 props가 없는 페이지, 즉 getStaticProps로부터 받은 데이터가 없는 페이지를 먼저 반환해서 사용자에게 보여준다.

이 때, 페이지 컴포넌트가 아직 서버로부터 데이터를 전달받지 못한 상태를 "fallback 상태"라고 한다.

fallback 상태일 때 로딩 등의 처리를 해주고 싶다면 useRouter()의 isFallback 속성을 통해 fallback 상태인지 확인하여 원하는 동작을 처리해줄 수 있다.

export const getStaticPaths = () => {
  return {
    paths: [...],
    fallback: true,
  };
};

export const getStaticProps = async (context: GetStaticPropsContext) => {
  // ...
};

export default function Page({ book }: InferGetStaticPropsType<typeof getStaticProps>) {
  const router = useRouter();

  if (router.isFallback) return "로딩 중입니다."; // fallback 상태일 때
  if (!book) return "문제가 발생했습니다. 다시 시도하세요."; // 뭔가 데이터에 문제가 있어서 데이터를 받지 못했을 때

  // ...
  };


만약 사전 렌더링시에 받아오는 데이터에 문제가 있을 때 404페이지로 이동시키고 싶다면 getStaticProps안에서 조건문을 만들어 return {notFound: true}로 처리하면 데이터가 없을 때 404페이지로 자동으로 전환하게 된다.


증분 정적 재생성 (ISR): Incremental Static Regeneration

기존 SSG의 한계를 극복하기 위한 기능으로,
SSG방식으로 생성된 정적 페이지를 일정 시간을 주기로 다시 생성하는 사전 렌더링 방식.

기본적으로는 이미 만들어져 있는 페이지를 반환하기 때문에 빠른 속도로 브라우저에게 응답이 가능하다는 SSG의 장점과 주기적으로 페이지를 업데이트 해줄 수 있기 때문에 최신 데이터를 반영할 수 있다는 SSR 방식의 장점까지 함께 가지고 있는 강력한 기능이다.

만약 60초 주기로 설정했다면 60초가 지나기 전 까지는 빌드타임에 생성된 페이지를 그대로 반환하다가
60초 이후에 접속 요청이 발생하게 되면 일단 원래 가지고 있던 페이지를 반환 후 서버에서 새로운 페이지를 재생성하게 된다. 이 시점부터 최신 데이터가 반영된 페이지를 렌더링하게 됨.

기존에 SSG를 적용하기 위해 만들어 두었던 페이지 내 getStaticProps()함수 리턴문에 props와 함께 revalidate를 전달해 주면 적용할 수 있다. 이 때 value는 데이터를 몇 초 단위로 재검증 할 것인지 전달하면 된다.

export const getStaticProps = async () => {
  	const [allBooks, recoBooks] = await Promise.all([
        fetchBooks(),
        fetchRandomBooks(),
    ]);

    return {
        props: {
            allBooks,
            recoBooks,
        },
        revalidate: 3, // 3초 단위로 데이터 재검증
    };
};

주문형 재검증 (On-Demand ISR)

하지만 시간 기반의 ISR을 적용하기 어려운 페이지도 존재하기 마련이다.
예를 들어, 시간과 관계없이 사용자의 행동에 따라 데이터가 업데이트 되는 커뮤니티 사이트의 게시글 페이지는 사용자가 게시글을 수정한 시점에서 해당 내용으로 업데이트가 되어야 하는데

ISR 방식을 사용하면 재생성 시점이 지나야 업데이트가 적용되므로 최신 데이터를 즉각적으로 반영하기 어렵다.
또, 아래 그림 처럼 데이터 변경 시점과 재검증 시간의 갭이 크면 불필요한 페이지 재생성이 일어나게 된다.

그럼 이런 페이지는 그냥 SSR로 처리하면 안 되나? 🤔

SSR의 경우는 브라우저가 요청할 때마다 매번 새롭게 페이지를 사전 렌더링 하기 때문에 응답 시간도 많이 느려지고, 동시에 접속자들이 몰리게 될 때는 서버의 부하까지 커져 버리게 된다.

따라서 시간을 기반으로 페이지를 업데이트 시키는 기존의 ISR방식이 아닌 요청을 기반으로 페이지를 업데이트 시킬 수 있는데, 이 기능이 바로 주문형 재검증 (On-Demand ISR)이라고 한다.

On-Demand ISR 방식을 사용하면 거의 대분의 페이지를 최신 데이터 반영이 가능한 정적 페이지로 처리할 수 있다.

먼저, revalidate 요청을 처리할 API 라우트를 하나 만들어 준다.

// pages/api/revalidate.ts

import { NextApiRequest, NextApiResponse } from "next";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    await res.revalidate("/"); // 인수는 revalidate할 페이지 경로
    
    return res.json({ revalidate: true });
  } catch (err) {
    res.status(500).send("Revalidation Failed");
  }
}

이제 /api/revalidate라는 주소로 접속 요청을 하게 되면 위 handler()가 실행되며 인수로 전달한 "/"경로를 revalidate할 것이다.

정리하자면, 사용자의 행동에 따라 혹은 특정 조건에 따라 데이터가 업데이트되어야하는 페이지를
정적 페이지로 유지하고 싶다면 On-Demand ISR 방식을 사용할 수 있다.

On-Demand ISR은 대부분의 케이스를 커버할 수 있는 강력한 사전 렌더링 방식이라
많은 Next.js로 구축된 웹 서비스가 이 방식을 사용하고 있다고 한다.

✅ SSR, SSG, ISR, On-Demand ISR 비교표

방식장점단점
SSR (Server-Side Rendering)- 최신 데이터 즉시 반영
- SEO 최적화 가능
- 응답 시간이 느릴 수 있음 (백엔드 응답 대기)
- 높은 서버 부하
SSG (Static Site Generation)- 빌드 시 정적 파일 생성 → 빠른 로딩 속도
- 서버 부하 없음
- SEO 최적화
- 빌드 시점 이후 데이터 변경 반영 불가
- 실시간성이 필요한 경우 부적합
ISR (Incremental Static Regeneration)- SSG처럼 빠른 페이지 로딩 속도 제공
- 주기적인 데이터 갱신 가능
- 서버 부하 적음
- 갱신 주기 동안은 이전 데이터를 제공
On-Demand ISR- ISR처럼 빠르면서도 데이터 변경 즉시 반영 가능
- 특정 요청 시에만 캐시를 갱신 → 불필요한 재생성 방지
- 캐시 갱신을 트리거하는 API를 따로 구현해야 함

📌 언제 어떤 방식을 선택해야 할까?

방식SEO 최적화 수준페이지 로딩 속도서버 부하데이터 최신성적합한 사례
SSR✅ (보통)❌ (서버 응답 필요)❌ (서버 부담 큼)✅✅✅ (최신 데이터 반영)검색 페이지, 대시보드
SSG✅✅✅ (최고)✅✅✅ (가장 빠름)✅✅✅ (서버 부담 없음)❌ (재빌드 필요)블로그, 문서, 랜딩 페이지
ISR✅✅ (좋음)✅✅ (빠름)✅✅ (낮음)✅ (시간 단위 업데이트)뉴스, 상품 목록 페이지
On-demand ISR✅✅✅ (최고)✅✅ (빠름)✅✅ (낮음)✅ (즉시 반영)사용자 요청 시 데이터 갱신 필요 페이지

💡 선택 기준

  • 즉시 최신 데이터 반영 필요?SSR
  • 최대한 빠른 페이지 로딩이 중요?SSG
  • 어느 정도 정적이지만, 주기적인 업데이트 필요?ISR
  • 관리자가 데이터 변경 시, 즉시 반영 필요?On-Demand ISR
profile
👨🏻‍💻 Front-End Developer

0개의 댓글