[Next.js] PageRouter

원정·2025년 3월 27일

Next.js

목록 보기
1/4
post-thumbnail

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs/dashboard

인프런의 <한 입 크기로 잘라먹는 Next.js> 강의를 듣고 정리한 내용입니다.


💰 페이지 렌더링


💵 vs 기존 React 렌더링 과정(CSR)

사용자가 접속 요청을 서버로 보내면 빈 HTML 껍데기(index.html)를 반환한다.
브라우저는 사용자에게 빈 화면을 렌더링한다.

서버는 HTML을 보낸 뒤 JS Bundle 파일을 브라우저로 보낸다.
JS Bundle 파일에는 해당 사이트에서 접근 가능한 모든 컴포넌트가 존재하는 리액트 앱 자체다.
브라우저는 파일을 실행하여 컨텐츠를 렌더링하고 사용자에게 보여준다.

사용자가 페이지를 이동할 경우, 요청이 서버까지 가지 않고 브라우저에서 JS 파일을 실행하여 렌더링하기 때문에 페이지 이동이 빠르다는 장점을 갖고 있다.

하지만 초기 요청으로 부터 컨텐츠가 렌더링된 화면을 보기까지의 시간이 오래걸린다.
이 시간을 FCP(First Contentful Paint)라고 한다.
FCP는 웹 성능을 대표할 정도로 중요한 지표다.

FCP에 따른 이탈률

  • 3초 이상: 32% 증가
  • 5초 이상: 90% 증가
  • 6초 이상: 105% 증가
  • 10초 이상: 123% 증가

정리하면 리액트 앱들은 CSR을 사용하여 초기 접속 이후에 일어나는 페이지 이동은 빠르고 쾌적한 반면, FCP가 늦어지게 된다는 치명적인 단점이 존재한다.

💵 Next.js 사전 렌더링

사용자가 페이지에 접속하면, 서버는 컴포넌트를 렌더링하여 HTML을 보낸다. 브라우저는 이 HTML을 즉시 화면에 보여주지만, 이 시점에는 JS가 아직 로드되지 않았기 때문에 사용자 상호 작용을 할 수는 없다.

서버는 리액트와 동일하게 JS Bundle 파일을 후속으로 보내준다.
브라우저는 JS Bundle 파일을 실행하여 HTML과 연결한다.
이 과정을 Hydration이라 부른다.

JS Bundle 파일이 연결된 뒤부터는 사용자와 상호 작용할 수 있다.
사용자와 상호 작용할 수 있게 되는 시간까지를 TTI(Time To Interactive)라고 부른다.
페이지 이동을 하게 되면 서버까지 갈 필요없이 브라우저에서 JS 파일을 실행하여 페이지를 교체한다.

정리하면 서버에서 렌더링된 HTML 파일을 보냄으로써 FCP 시간을 줄이고 초기 접속 이후에 일어나는 페이지 이동에 대해서는 CSR 방식과 같이 효율적으로 페이지를 이동한다.

💵 Pre-fetching

Next.js는 사용자가 보고 있는 페이지에서 이동할 가능성이 있는 모든 페이지들을 미리 불러온다.

Next.js는 앱에 작성된 모든 컴포넌트들을 자동으로 페이지별로 분리해서 미리 저장한다.
초기 접속 요청할 때 받는 JS Bundle 파일은 리액트처럼 앱 내의 모든 컴포넌트 코드가 아닌 현재 페이지에 해당하는 컴포넌트 코드들만 전달한다.
왜냐하면 초기 접속 요청이 있을 때마다 모든 페이지에 해당하는 코드들을 매번 번들링해서 전달하게 되면 파일의 용량이 커져서 다운로드 속도도 느려지고 Hydration 과정도 오래 걸려 TTI가 늦어지는 문제가 발생하기 때문이다.

하지만 현재 페이지에 해당하는 코드들만 보내주면 페이지 이동을 CSR 방식으로 처리할 수 없다.
현재 페이지에 대한 코드들만 존재하기 때문에 다른 페이지를 이동할 때 페이지 코드를 추가로 불러와야 하는 상황이 생기기 때문이다.

프리패칭은 페이지 이동이 느려지는 문제를 방지한다.
페이지 이동이 이뤄지기 전에 프리패칭이 발생하여 현재 페이지와 연결된 모든 페이지들의 코드를 미리 불러온다.
따라서 추가적인 데이터를 서버에 요청할 필요없이 CSR 방식의 빠른 페이지 이동이 가능하다.

💰 데이터 패칭


💵 기존 리액트의 데이터 패칭

export default function Page() {
  const [state, setState] = useState();
 
  const fetchData = async () => {
    const response = await fetch("...");
    const data = await response.json();
 
    setState(data);
  };
 
  useEffect(() => {
    fetchData();
  }, []);
 
  if (!state) return "Loading ...";
 
  return <div>...</div>;
}

기존 리액트의 처리 방식은 서버로 부터 불러온 데이터가 화면에 나타나기까지 오랜 시간이 걸린다는 단점이 있다.
데이터 요청 자체가 컴포넌트가 마운트된 이후에 실행되기 때문이다.

느린 FCP를 거치고 데이터를 요청하기 때문에 데이터 로딩이 완료되기까지 추가적인 시간이 필요하다.

💵 Next.js의 데이터 패칭


Next.js는 서버에서 사전 렌더링을 진행하는 과정에서 서버로부터 현재 페이지에 필요한 데이터를 미리 불러오도록 설정해줄 수 있다.
리액트의 데이터 패칭보다 빠른 시점에 데이터를 요청하고 받은 데이터를 HTML에 렌더링해주기 때문에 사용자는 FCP가 끝나고 추가적으로 기다릴 필요가 없다.

하지만 사전 렌더링 과정에서 데이터를 받아올 때 데이터의 용량이 크거나 서버의 상태가 좋지 못해 데이터를 받기까지 오래 걸린다면 사용자는 아무런 화면도 볼 수 없는 문제가 있다.
Next.js는 사전 렌더링이 오래 걸릴 것으로 예상되는 페이지의 경우 빌드 타임에 사전 렌더링 마처두도록 설정할 수도 있다.

Next.js의 사전 렌더링 방식으로 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 증분 정적 재생성(ISR)을 제공한다.

💵 서버 사이드 렌더링 (SSR)

export default async function fetchData(): Promise<Data[]> {
  let url = "...";
 
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error();
    }
 
    return await response.json();
  } catch (e) {
    console.error(e);
    return [];
  }
}
 
export const getServerSideProps = async (q?: string) => {
  // 서버 사이드에서 실행되는 코드기 때문에 브라우저에서 조회 불가, 터미널에서 확인 가능
  console.log("서버사이드프롭스!!!");
 
  const data = await fetchData();
 
  return {
    props: {
      data
    },
  };
};
 
export default function Page({
  data,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <div>...</div>
  );
}

SSR 방식은 가장 기본적인 사전 렌더링 방식으로 요청이 들어왔을 때 사전 렌더링을 진행하는 방식이다.

컴포넌트 바깥에 약속된 이름인 getServerSideProps 함수를 만들고 export로 내보내주면 SSR이 동작하도록 설정된다.
getServerSideProps 함수는 사전 렌더링할 때 컴포넌트 보다 먼저 실행돼서 필요한 데이터를 불러오는 기능을 한다.
반환 데이터는 반드시 props라는 객체를 요소로 갖고 있는 객체여야만 한다.
반환 객체의 props를 읽어와서 페이지 컴포넌트에 전달하기 때문에 꼭 준수해야 한다.

getServerSideProps 함수는 서버 측에서만 실행되기 때문에 콘솔에 출력한 내용은 브라우저가 아닌 서버 터미널에서 확인할 수 있다.

같은 이유로 getServerSideProps 함수 내부에서 브라우저 환경에서만 사용할 수 있는 window 객체를 사용하면 오류가 발생한다.

페이지 컴포넌트 또한 서버에서 한 번, 브라우저에서 한 번 실행되기 때문에 아무런 조건없이 window 객체를 사용하면 오류가 발생한다.
만약 페이지 컴포넌트에서 window 객체를 사용하고 싶으면 가장 쉬운 방법은 useEffect를 사용하는 방법이다.
useEffect는 컴포넌트가 마운트된 이후에 실행되기 때문에 서버에서 실행되지 않는다.

InferGetServerSidePropsTypegetServerSideProps의 반환 타입을 자동으로 추론해주는 기능이다.
컴포넌트에서 매개 변수 타입은 InferGetServerSidePropsType<typeof getServerSideProps>를 통해 정의할 수 있다.

export const getServerSideProps = async (
  context: GetServerSidePropsContext
) => {
  const q = context.query.q;
  const books = await fetchData(q as string);
 
  return {
    props: { books },
  };
};

만약 쿼리 스트링을 사용해야 한다면 context: GetServerSidePropsContext를 매개 인자로 받아서 context.query.쿼리 스트링 이름과 같은 방식으로 사용할 수 있다.

💵 정적 사이트 생성 (SSG)

SSG 방식은 SSR의 단점을 해결하는 사전 렌더링 방식으로, 빌드 타임에 실행된다.
사용자가 접속 요청을 보내면 빌드 타임에 만들어둔 페이지를 지체없이 응답할 수 있다.
SSG 방식은 사전 렌더링 과정에서 데이터를 불러오는 과정이 오래 걸려도 사용자의 경험에는 아무런 영향을 미치지 않는다.
사전 렌더링에 많은 시간이 소요되도 요청에 빠른 응답을 하는 장점을 갖고 있다.

하지만 빌드 타임 이후에는 새롭게 페이지를 생성하지 않기 때문에 사용자가 언제 접속 요청을 보내더라도 매번 같은 페이지만 응답한다.
따라서 최신 데이터 반영이 어려워 최신 데이터가 빠르게 반영되어야 하는 페이지보다는 데이터가 자주 업데이트 되지 않는 정적 페이지에 적합한 사전 렌더링 방식이다.

export const getStaticProps = async () => {
  console.log("인덱스 페이지");
 
  const data = await fetchData();
 
  return {
    props: {
      data,
    },
  };
};
 
export default function Page({
  data,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  return <div>...</div>;
}

getStaticProps라는 이름을 갖는 함수를 만들고 반환값은 동일하게 props를 요소로 갖는 객체를 반환해준다.
SSG 방식으로 동작하기 때문에 getStaticProps 함수 내부에 콘솔을 출력하면 빌드 시에 터미널에 한 번만 출력된다.

  • 빌드 명령어를 실행해보면 Generating static pages라는 메세지가 나오면서 SSG로 동작하는 페이지들이 생성되고 있다고 나온다.
  • 이 과정에서 인덱스 페이지라는 메세지가 출력되는 걸 미뤄보아 getStaticProps 함수가 실행된 걸 확인할 수 있다.
  • Route (pages) 아래 쪽을 보면 SSG 방식으로 사전 렌더링한 페이지는 흰색 동그라미가 앞에 붙어있다.
  • 다른 페이지들은 f라는 function 기호가 붙어있는데, 기호의 의미는 메세지 최하단에서 확인할 수 있다.
  • 빈 동그라미는 prerendered as static content라고 나와있고 기본값으로 설정된 SSG 페이지라는 뜻이다.
  • Next.js는 getServerSideProps, getStaticProps와 같은 메서드를 사용하지 않고 아무런 설정도 안 했을 경우, 정적 페이지로 빌드 타입에 사전 렌더링하도록 설정해준다.
export const getStaticProps = async (
  context: GetStaticPropsContext
) => {
  const q = context.query.q;
  const books = await fetchData(q as string);
 
  return {
    props: { books },
  };
};

쿼리 스트링을 사용하기 위해 SSR 방식과 같은 방법으로 사용하면 오류가 발생한다.

getStaticProps 함수에 전달되는 contextquery 속성이 존재하지 않는 이유는 빌드 타임에 한 번 실행되기 때문에 사용자의 동작에 의해 전달되는 쿼리 스트링을 알 수 없기 때문이다.

동적 경로를 갖는 페이지 컴포넌트에 SSG를 적용시켜 보면,

export const getStaticPaths = async () => {
  const datas = await fetchDatas();
 
  const paths = datas
    .map((data) => data.id.toString())
    .reduce(
      (acc: { params: { id: string } }[], cur) => [
        ...acc,
        { params: { id: cur } },
      ],
      []
    );
 
  return {
    paths,
    fallback: false,
  };
};

페이지 컴포넌트 바깥에 getStaticPaths라는 이름으로 함수를 만들고 export로 내보내 준다.
getStaticPathspathsfallback 속성을 갖는 객체로 반환해줘야 한다.

paths는 어떤 경로들이 존재할 수 있는지를 객체 배열로 반환해줘야 하며 URL Parameter를 의미하는 params라는 값으로 설정해준다.

fallback은 만약 브라우저에서 paths의 값으로 설정한 URL에 해당하지 않는 경로로 접속 요청을 할 경우 대비책을 설정하는 역할로 3가지 옵션이 존재한다.

1. fallback: false

false로 설정할 경우 존재하지 않는 경로의 요청에 대해 NotFoundPage를 반환한다.

npm run build 명령어를 실행하면 빌드된 산출물 안에서 직접 확인할 수 있다.
만약 브라우저가 /book/1로 요청하게 되면 지체없이 HTML 파일을 보여줄 수 있다.

2. fallback: "blocking"

"blocking"을 넣어주면 존재하지 않는 경로에 대해 SSR 방식으로 사전 렌더링하여 보여준다.
빌드 타입 이후에 생성된 페이지는 Next 서버에서 자동으로 저장되기 때문에 청므 요청 시에 SSR 방식으로 동작하여 조금 느릴 수 있으나, 이후 요청에 대해서는 새롭게 생성할 필요가 없이 빠른 속도로 렌더링된다.

따라서 SSR과 SSG가 결합된 형태로 동작한다.
동적인 페이지를 구현할 때 빌드 타임에 모든 데이터를 불러오기 어려운 상황이거나 새로운 데이터가 추가되어야 하는 상황에서 사용할 수 있다.

하지만 존재하지 않았던 페이지를 SSR 방식으로 생성할 때 사전 렌더링 시간이 길어지면 브라우저에게 서버가 아무런 응답도 하지 않기 때문에 페이지 크기에 따라 오랜 시간을 기다려야 하는 문제가 있다.

3. fallback: true

true로 설정할 경우 존재하지 않는 페이지 요청을 받았을 때 getStaticProps의 반환값인 props가 없는 페이지를 먼저 반환한다.
이후 페이지에 필요한 데이터인 props만 따로 계산하여 완료되면 브라우저에게 보낸다.
UI만 먼저 렌더링하고 데이터는 나중에 전달하게 되는 것이다.

export default function Page({
  data,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  const router = useRouter();
 
  if (router.isFallback) return "로딩중입니다.";
  if (!book) return "문제가 발생했습니다. 다시 시도하세요.";
 
  const { id, title, subTitle, description, author, publisher, coverImgUrl } =
    data;
 
  return <div>...</div>;
}

페이지 컴포넌트가 아직 getStaticProps의 계산 결과를 props로 받지 못한 상황을 fallback 상태라고 부른다.
useRouter.isFallback를 사용하여 fallback 상태에 따른 분기 처리를 할 수 있다.

export const getStaticProps = async (context: GetStaticPropsContext) => {
  const id = context.params!.id;
  const data = await fetchOneData(Number(id));
 
  if(!data){
    return {
      notFound: true,
    }
  }
 
  return {
    props: { data },
  };
};

추가적으로 만약 존재하지 않는 데이터를 요청하는 페이지로 들어왔을 경우 Not Found Page를 보여주고 싶다면, getStaticProps 함수에서 { notFound: true }를 반환해주면 된다.

💵 증분 정적 재생성 (ISR)

SSG 방식으로 생성된 정적 페이지를 일정 시간을 주기로 다시 생성하는 방식이다.
앞서 SSG 사전 렌더링 방식은 빌드 타임 이후에는 다시 생성하지 않기 때문에 매번 같은 페이지만 보여주는 문제가 있었다.
하지만 ISR 방식을 이용하면 SSG 방식으로 빌드 타입에 생성된 정적 페이지에 유통 기한을 설정할 수 있다.

60초로 설정했다면, 60초 전에는 빌드 타입에 생성한 페이지를, 후에는 원래 갖고 있는 페이지를 반환하고 새로운 페이지를 생성한다.

ISR 방식은 기본적으로 이미 만들어진 페이지를 반환하기 때문에 빠른 속도로 응답한다는 SSG 방식의 장점과 주기적으로 페이지를 업데이트 해줌으로써 최신 데이터를 반영해줄 수 있는 SSR 방식의 장점까지 갖고 있는 강력한 렌더링 전략이다.

export const getStaticProps = async (context: GetStaticPropsContext) => {
  const id = context.params!.id;
  const data = await fetchOneData(Number(id));
 
  if(!data){
    return {
      notFound: true,
    }
  }
 
  return {
    props: { data },
    revalidate: 3,
  };
};

getStaticProps의 반환값에 revalidate: 유통 기한을 넣어 반환하면 ISR 방식이 적용된다.

💵 On-Demand ISR

ISR 방식은 SSG와 SSR 방식의 장점을 모두 갖고 있기 때문에 ISR 방식을 최대한 이용하는 것이 좋다.
하지만 시간과 관계없이 사용자의 행동으로 업데이트된 페이지는 ISR 방식을 적용하기 어렵다.

게시글 수정이나 삭제 등의 사용자 행동에 따라서 즉각적으로 업데이트가 필요한 게시글 페이지의 경우, ISR 방식으로 렌더링한다면 유통 기한 전에 수정한 경우 수정 전 페이지를 보게 된다.
추가로 60로 설정했지만 24시간 이후에 게시글을 수정한다면 불필요하게 페이지를 재생성하는 과정 또한 발생한다.

SSR로 해결한다면 요청마다 새롭게 렌더링하여 응답 시간이 느려지고 동시에 접속자가 많이 몰리면 서버 부하가 커진다.

Next.js는 기존의 ISR 방식이 아닌 요청을 기반으로 페이지를 업데이트 시킬 수 있는 새로운 ISR 방식을 제공한다.
요청을 받을 때마다 페이지를 다시 생성하는 방식을 On-Demand ISR 방식이라고 한다.

사용자가 게시글을 수정할 때마다 페이지 Revalidate 요청을 보내 페이지를 다시 생성할 수 있다.
On-Demand ISR 방식을 이용하면 대부분의 페이지를 최신 데이터로 유지하면서 정적 페이지로 처리할 수 있다.

export const getStaticProps = async (context: GetStaticPropsContext) => {
  const id = context.params!.id;
  const data = await fetchOneData(Number(id));
 
  if(!data){
    return {
      notFound: true,
    }
  }
 
  return {
    props: { data },
  };
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    await res.revalidate("/");
    return res.json({ revalidate: true });
  } catch (err) {
    res.status(500).send("Revalidation Faild");
  }
}

getStaticProps 함수를 작성하고 Revalidate 요청을 처리할 새로운 핸들러를 만들어 준다.
핸들러 함수 내부에서 response.revalidate 메서드의 인자로 재생성할 페이지 경로를 전달한다.
요청이 성공하면 { revalidate: true }를 반환하여 재생성이 완료되었음을 알려준다.

On-Demand ISR 방식은 거의 대부분의 케이스를 커버할 수 있는 사전 렌더링 방식이기 때문에 Next.js로 구축된 웹 서비스들에서 활발히 사용되고 있다고 한다.

profile
https://wonjung-jang.github.io/ 로 이동했습니다!

0개의 댓글