[Next.js] Pages Router 기본 개념

Woonil·약 6시간 전

Next.js

목록 보기
2/2

한 입 크기로 잘라먹는 Next.js (이정환) 강의를 듣고 학습 내용을 기록하였습니다.

Next.js의 최신 버전은 모두 App Router 방식을 취하고 있다. 이전 버전의 Pages Router는 파일 기반 라우팅, CSR 중심 구조, getServerSideProps, getStaticProps 같은 데이터 패칭 방식을 통해 React SPA의 단점을 보완하려는 첫 번째 해법이었다. App Router는 이 구조의 문제점(복잡한 데이터 패칭, 서버·클라이언트 경계 불명확, 중복 렌더링)을 해결하기 위해 나왔다. App Router의 핵심 개념들이 Pages Router에서 출발하므로, Pages Router에 대한 대략적인 이해는 충분히 도움이 될 것이다.

Pages RouterApp Router
SSR / SSGServer Components
API RoutesRoute Handlers
_app.tsx, _document.tsxlayout, template
CSR / hydrationStreaming / Suspense

Next.js의 Pages Router는 각 파일을 라우트에 대응시키는 직관적인 파일 시스템 형식의 라우팅을 제공한다. 이는 react router와 같이 페이지 라우팅 기능을 제공하는 라이브러리의 기능을 거의 유사하게 제공한다.

  • 기본 파일 구조

    • app.tsx: 리액트의 App.tsx와 동일한 역할을 하며, 전체 페이지에 공통적으로 포함되는 헤더, 레이아웃, 비즈니스 로직 등을 포함한다.
    • document.tsx: 리액트의 index.html과 비슷한 역할을 하며, 모든 페이지에 공통적으로 적용되어야 할 html을 지정한다.
    • index.tsx: 특정 경로의 페이지 진입점을 나타내며, [경로명]/index.tsx 와 같이 설정도 가능하다.
    • [id].tsx: url 파라미터에 해당하는 페이지를 반환한다.
    • [...id].tsx (catch all segment 방식): url 파라미터 내의 연속하는 모든 segment(구간)에 대응하는 페이지를 나타낼 수 있으며, 특정 경로에 대한 기본 페이지를 반환하려면 index.tsx를 따로 작성이 필요하다.
    • [[...idx]].tsx (optional catch all segment 방식): index.tsx의 역할까지 수행한다. 즉 특정 경로에 대한 기본 페이지를 반환할 수 있다.
    • 404.tsx: 없는 경로에 대한 페이지를 반환한다.
  • useRouter: next/router(page router 전용 패키지)로부터 라우팅 정보를 가져오며, 쿼리 스트링, url 파라미터와 같은 정보를 추출할 수 있다.

  • Link: <a> 태그는 기본적으로 서버에서 페이지를 불러와 네비게이팅하므로, 클라이언트 사이드에서의 네비게이팅을 지원하는 next의 Link 컴포넌트를 사용한다.

  • API Routes: Next.js에서 API를 구축할 수 있게 해주는 기능으로, api 폴더 내에 정의하면 /api/폴더명 으로 요청이 가능하다.

    import type { NextApiRequest, NextApiResponse } from "next";
    
    export default function handler(req: NextApiRequest, res: NextApiResponse) {
    	const date = new Date();
    	res.json({ time: date.toLocaleString() });
    }
  • 장점

    • 파일 시스템 기반의 간편한 페이지 라우팅을 제공한다.
    • 다양한 방식의 사전 렌더링을 제공한다.
  • 단점

    • 페이지별 레이아웃 설정이 번거롭다. (getLayout 메서드)
    • 데이터 페칭이 페이지 컴포넌트에 집중된다. 페이지 컴포넌트의 자식 컴포넌트들에 props를 계속 넘겨줘야 한다(Props Drilling).
    • 불필요한 컴포넌트들도 JS Bundle에 포함된다. 즉, 상호작용이 없는 컴포넌트들까지 클라이언트 측에서 hydration 과정을 거친다. 이들은 서버에서 한 번만 실행되면 충분하다는 뜻이다. => TTI(Time to Interaction) 증가

🤔개념

SSR (Server Side Rendering)

가장 기본적인 사전 렌더링 방식으로, 요청이 들어올 때마다 사전 렌더링을 수행한다.

getServerSideProps

컴포넌트 내에 이 함수를 선언하면 서버사이드 렌더링을 위한 컴포넌트로 지정하는 것이다. 즉, 컴포넌트보다 먼저 실행되어서 컴포넌트에 필요한 데이터를 불러오는 역할을 수행한다.

  • 특징
    • 반환값은 props 속성을 포함하는 단 하나의 객체여야 한다.
    • 사용하는 컴포넌트 측에서는 해당 props 내부의 속성들을 props로써 사용할 수 있다. 또한, props의 타입은 Next.js가 제공하는 InferGetServerSidePropsType를 사용하여 추론할 수 있다.
    • 서버측에서 실행된다.
    • context: GetServerSidePropsContext: getServerSideProps 함수는 context를 props로 받을 수 있으며, 여기에는 url파라미터나 쿼리스트링 정보를 포함한 ~정보가 담겨있다.
import { InferGetServerSidePropsType } from "next";

export const getServerSideProps = () => {
  const data = "hello";
  console.log("서버사이드props!");

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

export default function Home({
  data,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  console.log("🚀 ~ Home ~ data:", data); // 서버에서 한번, 브라우저에서 한번 실행되어 총 두 번 실행됨

  return (<></>);
}

Home.getLayout = (page: ReactNode) => {
  return <SearchableLayout>{page}</SearchableLayout>;
};

SSG (Static Site Generation)

하지만 데이터 페칭 자체가 오래걸리면 SSR은 오히려 초기 로딩을 늦출 수도 있다. 따라서 Next.js는 빌드 타입에 데이터 페칭을 미리 수행하는 SSG 등의 방법을 제공하여 이러한 우려를 해소한다. 즉, SSG는 빌드 타임에 페이지를 사전 렌더링 해둔다.

  • 장점
    • 사전 렌더링에 많은 시간이 소요되는 페이지더라도 사용자의 요청에는 매우 빠른 속도로 응답이 가능하다.
  • 단점
    • 매번 똑같은 페이지만 응답하므로 최신 데이터 반영은 어렵다.

getStaticProps

컴포넌트 내에서 이 함수를 선언하면 정적 페이지 렌더링을 위한 컴포넌트로 지정하는 것이며, 빌드 타임에 한 번만 실행된다.

import { InferGetStaticPropsType } from "next";

export const getStaticProps = async () => {
  console.log("SSG 빌드 시작"); // 미리 빌드한 페이지를 출력하므로, 빌드 타임에서만 출력됨
  const [allBooks, recoBooks] = await Promise.all([
    fetchBooks(),
    fetchRandomBooks(),
  ]);

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

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

SSR의 / 경로의 페이지 빌드 결과는 아래와 같이 Dynamic 페이지로 생성된다. 즉, 해당 페이지는 on demand(주문형)으로 불러오게 된다.

반면, SSG 적용 후 빌드 결과는 아래와 같다.

한편, 404 페이지와 같이 아무런 설정을 해주지 않은 페이지에 대해서 Next.js는 기본적으로 Static으로써 제공한다.

쿼리스트링에 접근해야 하는 경우

SSG 방식으로는 SSR과 달리 context를 통해 쿼리스트링에 접근할 수 없다. 따라서, 클라이언트 사이드에서 이를 조작해야 한다. Next.js는 기본적으로 페이지를 Static으로 제공하므로, 이 경우에는 검색바 레이아웃 등 정적으로 제공해도 되는 html의 일부만 미리 빌드해 반환한다.

export default function Page() {
  const [books, setBooks] = useState<BookData[]>([]);
  const router = useRouter();
  const q = router.query.q;

  const fetchSearchResult = async () => {
    const data = await fetchBooks(q as string); // 쿼리스트링을 통해 검색된 도서 목록을 가져옴
    setBooks(data); // 가져온 도서 목록을 상태에 저장

  };

  useEffect(() => {
    if (q) {
      fetchSearchResult();
    }
  }, [q]);

  return (
    <div>
      {books.map((book) => (
        <BookItem key={book.id} {...book} />
      ))}
    </div>
  );
}

동적 경로에 적용

[id].jsx 와 같이 동적으로 결정되는 페이지의 경우, getStaticPaths 함수 내에서 SSG를 원하는 경로에 대해 url 파라미터의 값을 미리 설정해주어야 한다. 그래야만 Next.js가 사전렌더링 해야 할 페이지로 인식하여 빌드타임에 사전렌더링을 수행할 수 있다.

// [id].tsx
import {
  GetStaticPropsContext,
  InferGetStaticPropsType,
} from "next";

export const getStaticPaths = () => {
  return {
    // 정적 생성할 경로를 지정하는 부분
    paths: [
      { params: { id: "1" } },
      { params: { id: "2" } },
      { params: { id: "3" } },
    ],
    fallback: false,
  };
};

export const getStaticProps = async (context: GetStaticPropsContext) => {
  const id = context.params!.id;
  const movie = await fetchOneMovie(Number(id));

  return {
    props: { movie },
  };
};

export default function Page({ movie }: InferGetStaticPropsType<typeof getStaticProps>) {
  return ();
}

fallback

정적 생성되지 않은 경로에 대한 fallback을 처리하며, true, false, 'blocking' 중 하나를 선택할 수 있다.

  • false: 정적 생성되지 않은 경로에 대한 fallback을 제공하지 않으며, 404 페이지로 이동시킨다.
  • 'blocking': 정적 생성되지 않은 경로에 대한 fallback을 제공하며, 페이지가 로드될 때 서버에서 데이터를 가져온다(SSR과 유사). 클라이언트에게는 페이지가 준비될 때까지 기다리게 한다. => 새로운 데이터가 계속 추가되어야 하는 상황에서 사용 가능하다.
  • true: 정적 생성되지 않은 경로에 대한 fallback을 제공하며, 페이지가 로드될 때 서버에서 데이터를 가져온다. 이때, 데이터 요청 시간이 길어질 경우 사용자 경험에 영향을 주는 것을 방지하기 위해 props가 없는 페이지를 먼저 반환한 후, props가 계산되면 props만 따로 반환하는 방식으로 동작한다. 즉, 데이터가 없는 상태의 페이지를 먼저 렌더링 해주는 것이다.
    • props 계산 중 보여줄 화면 생성
      export default function Page({
        book,
      }: InferGetStaticPropsType<typeof getStaticProps>) {
        const router = useRouter();
        if (router.isFallback) return "로딩중입니다.";
      }
    • 데이터가 없는 경우에도 404 페이지로 보내고 싶은 경우
      export const getStaticProps = async (context: GetStaticPropsContext) => {
        const id = context.params!.id as string;
        const movie = await fetchOneMovie(id);
      
        // 영화 데이터가 없으면 404 페이지로 이동
        if (!movie) {
          return {
            notFound: true,
          };
        }
      
        return {
          props: {
            movie,
          },
        };
      };

ISR (Incremental Static Regeneration)

SSG 방식으로 생성된 정적 페이지를 일정 시간을 주기로 다시 생성하는 기술

  • 특징
    • 기존 SSG 방식의 장점: 매우 빠른 속도로 응답 가능
    • 기존 SSR 방식의 장점: 최신 데이터 반영 가능

revalidate

기존 SSG 방식의 getStaticProps의 반환문에 revalidate 옵션만 추가하면 된다.

// SSG -> ISR
export const getStaticProps = async () => {
  console.log("ssg 인덱스 페이지");

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

  return {
    props: {
      allBooks,
      recoBooks,
    },
    // ISR 적용
    revalidate: 3, // 3초에 한 번 재생성
  };
};

On-demand ISR

하지만 단순히 시간 기반으로 페이지를 재성성하게 되면 시간과 관계없이 사용자의 행동에 따라 데이터가 업데이트되는 페이지에 대처하기 어렵다. 이러한 ISR 방식의 단점을 해소하기 위해 특정 경우에 재생성할 수 있게 한다.

이를 위해 api 폴더 내부에 특정 api 요청(API Route)에 대한 res에 revalidate 메서드를 지정한다.

// api/revalidate.ts
import { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    await res.revalidate("/"); // '/' 경로에 대한 재생성을 수행
    return res.json({ revalidate: true });
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: "revalidate error" });
  }
}

SEO

next/head의 Head 태그를 사용하여 기본적인 메타태그를 적용할 수 있다.

import Head from "next/head";

export default function Home({
  allBooks,
  recoBooks,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    <>
      <Head>
        <title>네스트넷 도서관</title>
        <meta property="og:image" content="/thumbnail.png" />
        <meta property="og:title" content="네스트넷 도서관" />
        <meta
          property="og:description"
          content="네스트넷 도서관에 등록된 도서들을 만나보세요."
        />
      </Head>
      <div className={s.container}>
      </div>
    </>
  );
}
  • SSG인 경우 메타태그 처리: SSG로 페이지를 불러오는 경우, fallback 상태일 때 메타태그를 불러오지 못하는 문제가 생긴다. 따라서 조건문을 통해 fallback 상태일 때 나타낼 메타태그를 지정할 필요가 있다.
    import Head from "next/head";
    
    export default function Page({
      book,
    }: InferGetStaticPropsType<typeof getStaticProps>) {
      const router = useRouter();
      if (router.isFallback) {
        // fallback 상태일 때도 메타태그를 보여줄 수 있음
        return (
          <>
            <Head>
              <title>네스트넷 도서관</title>
              <meta property="og:image" content="/thumbnail.png" />
              <meta property="og:title" content="네스트넷 도서관" />
              <meta
                property="og:description"
                content="네스트넷 도서관에 등록된 도서들을 만나보세요."
              />
            </Head>
          </>
        );
      }
    
      if (!book) return "문제가 발생했습니다 다시 시도하세요";
    
      const { id, title, subTitle, description, author, publisher, coverImgUrl } =
        book;
    
      return (
        <>
          <Head>
            <title>{title}</title>
            <meta property="og:image" content={coverImgUrl} />
            <meta property="og:title" content={title} />
            <meta property="og:description" content={description} />
          </Head>
          <div key={id} className={s.container}>
          </div>
        </>
      );
    }
profile
프론트 개발과 클라우드 환경에 관심이 많습니다:)

0개의 댓글