강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

seong2님의 프로필 이미지
seong2

작성한 질문수

한 입 크기로 잘라먹는 Next.js(v15)

2.16) 빌드 후 실행한 환경에서 네트워크 탭에서의 Json 요청

해결된 질문

작성

·

80

·

수정됨

0

안녕하세요. 좋은 강의 잘 듣고 있습니다. "2.16) SSG 4. 폴백옵션 설정하기" 강의를 듣다가 궁금한 점이 생겨서 질문남깁니다!


홈 경로는 getStaticProps 함수를 사용해 빌드 타임에 사전 렌더링이 이루어지고 있습니다. Next App을 빌드 후에 홈 / 경로에서 네트워크 탭을 살펴보다가 현재 화면 내에서 인덱스에 해당하는 Json 응답을 받은 것을 보았습니다.

image.png

하나를 열어보니 아래와 같습니다.

// 6.json?id=6

{
    "pageProps": {
        "book": {
            "id": 6,
            "title": "프론트엔드 성능 최적화 가이드",
            "subTitle": "웹 개발 스킬을 한 단계 높여 주는",
            "description": "...",
            "author": "유동균",
            "publisher": "인사이트",
            "coverImgUrl": "https://shopping-phinf.pstatic.net/main_3562758/35627588630.20230704084326.jpg"
        }
    },
    "__N_SSG": true
}

제가 이해한 바로는 / 경로에 진입해서 초기 요청이 이루어지면 서버는 사전에 렌더링 된 HTML 페이지를 응답한 이후 현재 페이지에 필요한 모든 자바스크립트 파일을 JS Bundle로 전달합니다.

네트워크 탭에서 localhost HTML 문서를 확인하고 이후에 http://localhost:3000/_next/static/chunks/pages/book/%5Bid%5D-4bfc2418ccf58b5f.js JS Bundle을 응답받은 것을 확인했습니다.

그리고 현재 페이지에서 이동 가능성이 있는 페이지들에 한해 사전에 요청이 이루어집니다. js 파일이 전달되어야 하지만 북(/book/[id])페이지가 SSG 방식으로 사전 렌더링이 이루어지므로 데이터를 뿌려주기만 하면 되니까 json 형식으로 응답이 온 것이 아닐까 생각했습니다. (북페이지를 SSR 방식으로 바꾸니 북페이지의 js 파일이 전달되었습니다.)

이후 Link 컴포넌트인 도서 목록 중 하나를 클릭하면 자바스크립트 코드를 실행시켜 컴포넌트를 교체하는 CSR 방식으로 페이지를 이동합니다.

Q. SSG 방식에서는 사전 렌더링의 응답으로 Json 형식으로 전달받아 새로운 페이지를 보여주는 제가 이해하고 있는 방식이 맞는지 궁금합니다.


다른 하나는, 현재 화면에서 네트워크 요청(예: 6.json?id=6)이 무수히 많이 이루어진 것을 보았습니다. 그래서 Link 컴포넌트의 prefetch 값을 false로 설정하여 각 컴포넌트에 마우스를 호버했을 때 프리페칭이 이루어지도록 했습니다. 그러자 마우스를 호버하면 프리페칭이 이루어졌지만 마우스를 호버할 때마다 요청이 이루어져 의문이 생겼습니다.

Q. 잦은 요청이 서버에 부하를 주는 것은 아닌지, 이를 개선할 수 있는 방법이 있는지 궁금합니다.

 

긴 글 읽어주셔서 감사합니다.

아래 코드 첨부하였습니다!

📄/pages/index.tsx

import SearchableLayout from "@/components/Searchable-layout";
import BookItem from "@/components/Book-item";
import { InferGetStaticPropsType } from "next";
import fetchBooks from "@/lib/fetch-books";
import style from "./index.module.css";
import fetchRandomBooks from "@/lib/fetch-random-books";

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

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

export default function Home({
  allBooks,
  recoBooks,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    <div className={style.container}>
      <section>
        <h3>지금 추천하는 도서</h3>
        {recoBooks.map((book) => (
          <BookItem key={book.id} {...book} />
        ))}
      </section>
      <section>
        <h3>등록된 모든 도서</h3>
        {allBooks.map((book) => (
          <BookItem key={book.id} {...book} />
        ))}
      </section>
    </div>
  );
}

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

📄/pages/book/[id].tsx

/* eslint-disable jsx-a11y/alt-text */
/* eslint-disable @next/next/no-img-element */
import fetchBook from "@/lib/fetch-one-book";
import style from "./[id].module.css";
import { GetStaticPropsContext, InferGetStaticPropsType } from "next";

export const getStaticPaths = () => {
  return {
    paths: [
      { params: { id: "1" } },
      { params: { id: "2" } },
      { params: { id: "3" } },
    ],
    fallback: "blocking",
  };
};

export const getStaticProps = async (context: GetStaticPropsContext) => {
  const { id } = context.params!;
  const book = await fetchBook(Number(id));

  return {
    props: {
      book,
    },
  };
};

export default function Page({
  book,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  if (!book) return "문제가 발생했습니다. 다시 시도해주세요.";

  const { title, subTitle, description, author, publisher, coverImgUrl } = book;

  return (
    <div className={style.container}>
      <div
        className={style.cover_img_container}
        style={{ backgroundImage: `url('${coverImgUrl}')` }}
      >
        <img src={coverImgUrl} />
      </div>
      <div className={style.title}>{title}</div>
      <div className={style.subTitle}>{subTitle}</div>
      <div className={style.author}>
        {author} | {publisher}
      </div>
      <div className={style.description}>{description}</div>
    </div>
  );
}

답변 2

1

이정환 Winterlood님의 프로필 이미지
이정환 Winterlood
지식공유자

안녕하세요 이정환입니다.

수준급 질문 감사합니다! 순서대로 답변드릴게요 😃

 

1) SSG 페이지의 프리페칭

SSG 환경에서도 book/[id] 페이지의 JS Bundle이 프리페칭 됩니다. 그러나 여러개의 book/[id] 페이지를 프리페칭 한다고 해서 여러번 JS Bundle을 불러오지는 않습니다. 딱 한번만 불러오게 됩니다.

image.png

이는 동적 페이지의 동작 구조가 동일한 JS Bundle에서 JSON 데이터만 달라지는 방식으로 동작하기 때문입니다. 쉽게 말해 book/[id] 경로의 여러 페이지들은 모두 동일한 JS Bundle을 공유한다는 것이죠

 

2) 잦은 프리페칭 요청이 서버에 부하를 주는 것은 아닐까?

네 맞습니다. 프리페칭 요청이 너무 많을 경우 서버에 충분히 부하를 줄 수 있습니다.
이에 Next.js는 프리페칭을 선택적으로 해제 할 수 있는 기능과 캐싱 전략을 추가로 활용하고 있습니다.

아래 그림에 보시면 프리페칭되는 요청의 Response Header에 X-Next.js-Cache 라는 필드가 있는걸 보실 수 있는데요 이처럼 Next.js는 자체적인 캐시 전략을 통해 프리페칭되는 데이터가 너무 자주 요청되는 상황에 최대한 효율적으로 대응하고 있습니다.

image.png

여기에 더불어 앱 라우터 버전부터는 클라이언트 라우트 캐시 라는 기능을 통해 프리페칭된 데이터를 브라우저의 캐시에 보관하는 전략도 취하고 있습니다.

seong2님의 프로필 이미지
seong2
질문자

1번의 답변을 보고 제가 프리페칭은 페이지를 이동하려고 할 때 해당 페이지의 js코드를 가져오는 것이라는 이해가 부족했던 것 같습니다. 초기 요청의 응답으로 JS Bundle이 온 것처럼 프리페칭의 결과도 이동할 페이지의 JS Bundle이며 SSG 환경에서 동적 페이지는 JSON 데이터만 달라진다.. 이해했습니다!

(SSG, SSR 환경을 번갈아가며 테스트했고, Link의 prefetch 속성값을 false로 설정해두어서 마우스 호버 전에는 JS Bundle이 오지 않은 것을 착각했습니다 ㅠㅠ)

2번의 답변은 제가 몰랐던 부분이네요! 알려주신 키워드로 좀 더 알아보겠습니다.

답변 감사합니다!

0

안녕하세요, 인프런 AI 인턴입니다. seong2님!

Next.js에서 SSG와 프리패칭에 관련된 궁금증들을 잘 짚어주셨습니다. 질문하신 내용을 바탕으로 각 부분에 대해 자세히 설명 드리겠습니다.

1. SSG 방식에서의 JSON 응답

SSG(Static Site Generation)는 빌드 타임에 HTML 파일을 한 번 생성하고, 페이지 요청 시 이미 생성된 HTML을 응답합니다. 그런데, Next.js는 사전 렌더링된 페이지에서도 필요한 데이터를 JSON 형태로 제공합니다. 실행중인 JS 파일이 이러한 JSON 데이터를 사용하여 클라이언트 측에서 페이지를 완성합니다.

따라서, 초기 진입 시 서버는 연관된 JSON 파일을 함께 전송하고, 브라우저는 이 JSON을 사용해 컴포넌트를 그립니다. 페이지 이동 시에는 SSR과 달리, 서버가 HTML을 새로 만드는 작업 없이 이미 존재하는 JSON을 사용하여 CSR 방식으로 페이지를 교체합니다.

  • nextjs 프리패칭 답변을 참조하시면 프리패칭과 관련된 더 깊이 있는 설명을 확인할 수 있습니다.

2. 프리패칭과 요청 최소화

Link 컴포넌트의 prefetch 기능은 기본적으로 true로 설정되어 있습니다. 이로 인해 사용자가 링크를 클릭하기 전에 JS 번들을 사전에 로드하여 더 빠르게 페이지를 전환할 수 있게 도와줍니다. 이 점이 SSG/SSR에서 성능을 향상시키는 요소입니다. 하지만 prefetch가 많은 요청을 유발하여 서버 부하를 걱정하신다면, 해당 기능을 필요에 따라 제한할 수 있습니다.

프리패칭을 최소화하기 위한 방법:
- Link 컴포넌트의 prefetch 옵션을 false로 설정하여 사전 로드를 방지할 수 있습니다.
- 프리패칭이 꼭 필요한 페이지 외에는 lazy loading이나 dynamic import를 활용하여 페이지를 로드할 수도 있습니다.

프리패칭을 사용하면서 성능과 유저 경험을 적절히 밸런스 해외려면, 페이지 이동 전 필요한 최소한의 리소스를 불러오도록 최적화해야 합니다. Next.js는 자체적으로 캐싱 전략을 지니고 있어 304(Not Modified) 상태로 디바이스의 캐시를 재사용하기도 하므로, 이 측면에서도 효율성을 제공하고 있습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

seong2님의 프로필 이미지
seong2

작성한 질문수

질문하기