NextJs 작동 방식

장세진·2023년 6월 29일
2

NextJs

목록 보기
2/2
post-thumbnail

NextJs

SSR vs CSR 에서 언급했던 내용들을 복습해보자.

공식 홈페이지에서 nextjs 는 Vercel에서 유지 관리하는 웹용 풀스택 React 프레임워크라고 소개하고 있지만 FE 개발자들에게는 무엇보다도 ssr 을 지원하는 react 프레임 워크로 잘 알려져 있다.

react 는 virtual dom 을 활용한 csr 방식을 사용하여 웹 페이지를 이용하는 사람들에게 깜빡임 없는 부드러운 화면전환, 동적 ui 등 높은 퀄리티의 사용자 경험을 제공한다. 하지만 SEO, 느린 초기 로드 시간 등은 csr 만을 사용하는 react 에게 단점으로 작용이 되었으며 이를 개선하기 위해서 ReactDOMServer 라는 react 자체 라이브러리를 통해 직접 ssr 설정을 구축하는 방법을 제공하고 있다.

하지만 구성이 복잡하고 러닝 커브가 다소 높게 평가되어 사용자들이 사용에 많은 어려움을 겪었는데 이를 해결해주는 새로운 프레임워크가 등장하면서 아래와 같이 cna 명령어 하나만으로도 ssr 동작이 되는 nextjs 프레임워크 기반 프로젝트를 생성할 수 있게 되었다. (nodejs 는 당연히 설치했을거라고 생각한다.)

npx create-next-app [폴더이름] 

ssr 방식을 지원해주는 프레임워크에는 react 의 경우 nextjs, gatsby 가 있으며 vuejs 의 경우 nuxtjs 가 대표적이다. nextjs 와 nuxtjs 는 모두 vercel 에서 개발 한 프레임워크이다.

nextjs 를 사용하면 ssr 지원 뿐만아니라 프레임워크인 만큼 제공하는 기능이 상당히 많다. Router 지원, Image 최적화, vercel 배포를 통한 CI/CD 제공 등 여러 기능들이 있지만 본문에서는 Code Splitting 지원, Pre-fetching, rendering type (csr, ssr, ssg) 을 중심으로 nextjs 의 작동 방식을 적어보려고 한다.

react 를 사용하면 특별히 설정을 구축하지않는 이상 모든 페이지에서 csr 을 유지한다. nextjs 도 마찬가지로 ssr 을 지원하기 때문에 모든 페이지가 ssr 방식으로 작동할까. 그건 아니다. nextjs 는 rendering type 의 장점들을 활용해서 유연하게 페이지들을 구성할 수 있게 도와준다.

nextjs 의 작동방식을 공부하고 nextjs 를 사용하는 이유를 더 깊게 이해하는 것이 이번 블로그의 목표이다.

NextJs 작동방식

사전렌더링 (pre-rendering)

nextjs 는 브라우저에 렌더링을 할때 기본적으로 사전 렌더링을 한다. 사전렌더링이란 무엇일까

사전렌더링이란 각 페이지들을 사전에 미리 HTML 문서로 생성하여 가지고 있는 것이다. nextjs 는 빌드 시 해당하는 페이지 별로 각각의 HTML 문서를 미리 생성해서 가지고 있다가 서버로 요청이 들어올 때 알맞은 페이지를 반환해준다.

아래는 nextjs 를 빌드 했을 때 생기는 파일들의 예시이다.

HTML 파일이 여러개 보이는데 정답부터 예기하면 각 HTML 파일은 정적 페이지 렌더링(ssg) 에 사용 되는 미리 만들어진 HTML 파일이다. nextjs 는 이렇게 만들어진 파일들을 요청이 들어올 때 마다 생성하지 않고 이미 만들어진 파일을 재활용 하여 클라이언트에게 던져준다.

사전렌더링의 방식에는 크게 2가지 방식이 있는데 아래와 같다.

  1. SSG (추천) : HTML을 빌드 타임에 각 페이지별로 생성하고 해당 페이지로 요청이 올 경우 이미 생성된 HTML 문서를 반환한다.
  2. SSR : 요청이 올 때 마다 해당하는 HTML 문서를 그때 그때 생성하여 반환한다.

재밌는 사실은... nextjs 는 아무런 설정도 하지않으면 기본적으로 ssg 로 페이지를 렌더링 한다는 것이다. 만약 우리가 아무런 설정도 없이 nextjs 를 사용하고 있었다면 우리가 알고있던 ssr 은 사실 모두 ssg 였다고 봐도 무방하다. 하지만 ssg 와 ssr 은 HTML 이 만들어지는 시점의 차이지 큰 특징은 (SEO 등...) 비슷하다. 오히려 ssg 가 빌드 때 이미 만들어진 HTML 을 재활용하기 때문에 ssr 에 비해서 속도면에서 더 우수하다. 하지만 페이지 렌더링 시 지속적으로 변하는 데이터를 불러와야 하는 상황이라면 ssg 가 아닌 ssr 을 사용해야 한다. ssg 는 말그대로 정적페이지 이므로 재빌드를 하지 않는 이상 빌드 때 불러온 데이터가 계속 유지된다.

만약 ssr 로 클라이언트 요청에 따라 서버에서 HTML 문서를 생성해서 리소스로 내려주고 싶다면 다음과 같이 getServerSideProps 를 통해 설정을 해야한다.

import { useRouter } from "next/router";
import { GetServerSideProps } from "next/types";
import { useState } from "react";

interface SsrPageProps {
    parsedData: {
        activity: string;
        type: string;
        participants: number;
        price: number;
        link: string;
        key: string;
        accessibility: number;
    };
}

const Ssr = ({ parsedData }: SsrPageProps) => {
    const router = useRouter();

    return (
        <div className="w-screen h-screen flex flex-col justify-center items-center gap-5 pb-8">
            <div className="text-4xl font-bold">SSR</div>
            <p className="text-sm text-neutral-400">Fetched every render, on server side.</p>
            <ul className="text-base">
                {Object.keys(parsedData).map((key, keyIndex) => {
                    const value = parsedData[key as keyof SsrPageProps["parsedData"]];
                    return (
                        <li key={keyIndex}>
                            {key} : {value}
                        </li>
                    );
                })}
            </ul>
            <button 
                className="relative text-sm border-neutral-50 border-2 w-32 h-10 rounded-md font-bold after:content-[''] after:w-0 hover:text-blue-300 after:bg-blue-300 after:absolute after:bottom-0.5 after:left-0 after:h-0.5 hover:after:w-full after:transition-all after:rounded-md"
                onClick={() => router.back()}
            >
                Back to Home
            </button>
        </div>
    );
};

export const getServerSideProps: GetServerSideProps = async () => {
    const data = await fetch("https://www.boredapi.com/api/activity");
    const parsedData = await data.json();

    return {
        props: {
            parsedData,
        },
    };
};

export default Ssr;

우리는 ssr 또는 ssg 를 활용하여 SEO 및 초기화면 로딩속도를 개선하고 싶으면서 동시에 초기화면 로딩 이후부터는 페이지 이동 시 csr 을 활용하여 SPA 를 만들고 싶다.

이때는 nextjs 에서 제공하는 <Link> 를 활용하면 된다. <Link> 를 이용한 페이지 이동 시 nextjs 는 csr 을 하게되는데 <Link> 에 대해서는 차후 더 자세하게 설명하도록 하자.

코드분할 (code splitting)

nextjs 공식문서에 나온 코드분할은 다음과 같다.

  • 코드 분할은 애플리케이션의 번들을 각 진입점에 필요한 작은 청크로 분할하는 프로세스입니다. 목표는 해당 페이지를 실행하는 데 필요한 코드만 로드하여 애플리케이션의 초기 로드 시간을 개선하는 것입니다.
  • Next.js는 코드 분할을 기본적으로 지원합니다. 페이지/디렉토리 내의 각 파일은 빌드 단계 중에 자동으로 자체 JavaScript 번들로 코드 분할됩니다.
  • 페이지 간에 공유되는 모든 코드도 다른 번들로 분할되어 이후 탐색 시 동일한 코드를 다시 다운로드하지 않아도 됩니다.

nextjs 는 webpack 을 이용한 자바스크립트 번들을 만들게 되는데 이렇게 만들어진 자바스크립트 번들은 첫 페이지 진입 시 모두 받아지는 csr 과 다르게 페이지를 실행하는데 필요한 더 작은 청크로 분할되어 로드된다. 이렇게 되면 첫 페이지 진입 시 초기 로드 시간이 개선되는 장점이 있다.

지금까지 nextjs 에서 제공하는 사전렌더링과 코드분할에 대해서 알아봤는데 이는 SSR vs CSR 에서 언급한 SSR 의 특징을 모두 가지고 있다. nextjs 는 사용자가 사이트를 요청하면 사전 렌더링을 통해 만들어진 준비된 HTML 을 내려주기 때문에 SEO 적합하며 코드분할을 통해 csr 과 다르게 모든 js 파일을 첫 페이지 로드시 내려주지 않기 때문에 초기 로드시간을 개선해준다.

이제 csr 을 하기 위해 <Link> 에 대해서 알아보자

위 사이트는 nextjs 의 렌더링 방식 csr, ssr, ssg, isr 의 개념을 익히고 사용법을 숙지하기 위한 목적으로 만들어졌다. 추가적으로 코드가 필요하다면 github 코드를 참고하면 된다. 홈페이지의 첫 화면을 봐보자. (참고로 필자는 코드를 수정해가며 결과를 확인하기 위해서 로컬에서 코드를 수정 한 후 npm run build, npm run start 로 앱을 실행했습니다.)

위의 웹 페이지에는 첫 화면, SSG, SSR, CSR, ISR 페이지 총 5개의 페이지가 존재하며 각각의 텍스트를 클릭하면 해당 페이지로 이동한다.

각 페이지는 모두 다음과 같이 오픈 API 를 통해서 불러온 데이터들을 화면에 보여주는 형식으로 구성이 되어있다.

a 태그를 사용하여 페이지 이동을 한 경우

아래 코드는 a태그를 사용하여 페이지 이동을 하기위해 작성 된 index.tsx 이다.

import Link from "next/link";
import { useRouter } from "next/router";

export default function Home() {
    const router = useRouter();

    const typeOfNextRendering = ["SSG", "SSR", "CSR", "ISR"];

    return (
        <div className=" relative flex flex-col w-screen h-screen justify-center items-center gap-3 pb-32">
            <p className=" text-neutral-50 text-4xl font-bold">Types of Next Rendering</p>
            <p className="text-base text-neutral-400">Demo of Next.js rendring type using API</p>
            <ul className="flex flex-col gap-1 cursor-pointer">
                {typeOfNextRendering.map((type) => {
                    return (
                        <li key={type} className="relative hover:text-blue-300 after:content-[''] after:w-0 after:bg-blue-300 after:absolute after:bottom-0 after:left-0 after:h-0.5 hover:after:w-full after:transition-all">
                            <a
                                href={`./rendering/${type.toLowerCase()}`}
                            >
                                {type}
                            </a>
                        </li>
                    );
                })}
            </ul>
            <p className=" absolute text-sm text-neutral-400 bottom-20">©mobiliverse 2023 By JangSeBaRi</p>
        </div>
    );
}

첫 화면 로드 시 서버를 통해 받아지는 자원들은 다음과 같다.

다음은 ssg, ssr, csr 페이지로 이동했을 때 받아지는 자원들이다.



위의 사진들을 보며 확인할 수 있는것은 nextjs 에서 기본적으로 제공하는 사전렌더링과 코드분할이 이뤄졌다는 것이다. 페이지 이동시 받아지는 자원은 페이지를 구성하기 위한 파일들로만 구성되며 사전렌더링을 통해서 만들어진 준비 된 HTML 이 내려온다. 모든 페이지는 기본적으로 ssg 또는 ssr 를 통해 렌더링 되고 있다는것을 확인할 수 있다.

csr 페이지도 마찬가지로 오픈 API 데이터가 빠진 준비된 HTML 이 내려온다. useEffect 를 사용해서 오픈 API 데이터를 불러와 렌더링했으며 데이터를 불러오는 시점 차이 때문에 csr 페이지로 분류했지만 사전렌더링과 코드분할의 특징은 똑같이 보여주고있다.

이제 a 태그를 Link 로 바꿔보자

아래 코드는 a태그를 사용하여 페이지 이동을 하기위해 작성 된 index.tsx 이다.

import Link from "next/link";
import { useRouter } from "next/router";

export default function Home() {
    const router = useRouter();

    const typeOfNextRendering = ["SSG", "SSR", "CSR", "ISR"];

    return (
        <div className=" relative flex flex-col w-screen h-screen justify-center items-center gap-3 pb-32">
            <p className=" text-neutral-50 text-4xl font-bold">Types of Next Rendering</p>
            <p className="text-base text-neutral-400">Demo of Next.js rendring type using API</p>
            <ul className="flex flex-col gap-1 cursor-pointer">
                {typeOfNextRendering.map((type) => {
                    return (
                        <li key={type} className="relative hover:text-blue-300 after:content-[''] after:w-0 after:bg-blue-300 after:absolute after:bottom-0 after:left-0 after:h-0.5 hover:after:w-full after:transition-all">
                            <Link
                                href={`./rendering/${type.toLowerCase()}`}
                            >
                                {type}
                            </Link>
                        </li>
                    );
                })}
            </ul>
            <p className=" absolute text-sm text-neutral-400 bottom-20">©mobiliverse 2023 By JangSeBaRi</p>
        </div>
    );
}

첫 화면 로드 시 서버를 통해 받아지는 자원들은 다음과 같다.

a 태그를 사용했을 때 보다 더 많은 자원들이 받아졌다... 무슨일이 일어난걸까...

다음은 ssg, ssr, csr 페이지로 이동했을 때 받아지는 자원들이다.



서버를 통해 불러온 자원이 고작 오픈 API 의 데이터들 인데 어떻게 화면을 그릴 수 있었을까...
정답은 바로 페이지가 csr 을 통해 랜더링 되었기 때문이다.

그런데 react 의 경우 첫 화면 로드 시 모든 js 번들을 가져오기 때문에 js 를 통해 페이지 이동을 구현할 수 있지만 코드분할 때문에 js 번들 또는 청크를 모두 불러오지 않는 nextjs 가 어떻게 csr 렌더링을 할 수 있단 말인가... 이해가 안될 수 있지만 첫 화면 로드 시 서버를 통해 받아지는 자원사진을 다시보면 ssg, ssr, csr, isr 페이지를 그리기 위한 js 파일들이 서버로부터 받아진 것을 확인할 수 있다.

그렇다면 Link 를 사용하면 더 이상 코드분할(code splitting) 을 하지 않는것일까...

그건 아니다. 여기서 알아야 할 개념이 하나 더 생기는데 Link 를 사용하면 nextjs 에서 지원하는 prefetch 을 사용할 수 있다. prefetch 가 무엇일까.

prefetch

prefetch 는 Link 태그의 속성 중 하나이며 nextjs 의 설명은 다음과 같다.

  • 기본값은 true입니다. true인 경우 다음/링크는 백그라운드에서 페이지(href로 표시됨)를 미리 가져오게 됩니다. 이는 클라이언트 측 탐색의 성능을 개선하는 데 유용합니다. 뷰포트의 모든 <링크 />(처음에 또는 스크롤을 통해)는 미리 로드됩니다.
  • prefetch={false} 를 전달하여 프리페치를 비활성화할 수 있습니다. 프리페칭은 프로덕션 환경에서만 활성화됩니다.

prefetch 은 프로덕션 환경에서만 활성화가 된다는 말 때문에 npm run build, npm run start 를 통해서 앱을 실행 했었다. 아무튼 위의 얘기를 읽어보니 nextjs 는 기본적으로 코드 분할을 하지만 Link/prefetch={true} 가 뷰포트에 보일 경우 미리 페이지 이동 시 필요한 자원들을 불러온다고 한다. 때문에 내가 만든 프로젝트의 첫 화면이 로드될 때 a 태그를 사용했을 때 보다 더 많은 자원들이 불러 와 진것 같다. 첫 페이지에는 이동할 수 있는 경로가 ssg, ssr, csr, isr 4개나 되기 때문에 각 페이지를 렌더링 하기 위한 자원들을 prefetch 를 통해 미리 불러 온 것이다. 이렇게 되면 클라이언트에서 페이지 이동 시 페이지를 그리기 위한 추가적인 자원 (js, html 등...)을 따로 받아오지 않아도 되므로 클라이언트 측 탐색의 성능이 개선된다고 할 수 있다.

그렇다면 prefecth={false} 일 경우 어떻게 될까... 궁금한건 못참아~~

아래 코드는 prefetch 를 false 로 설정한 index.tsx 이다.

import Link from "next/link";
import { useRouter } from "next/router";

export default function Home() {
    const router = useRouter();

    const typeOfNextRendering = ["SSG", "SSR", "CSR", "ISR"];

    return (
        <div className=" relative flex flex-col w-screen h-screen justify-center items-center gap-3 pb-32">
            <p className=" text-neutral-50 text-4xl font-bold">Types of Next Rendering</p>
            <p className="text-base text-neutral-400">Demo of Next.js rendring type using API</p>
            <ul className="flex flex-col gap-1 cursor-pointer">
                {typeOfNextRendering.map((type) => {
                    return (
                        <li key={type} className="relative hover:text-blue-300 after:content-[''] after:w-0 after:bg-blue-300 after:absolute after:bottom-0 after:left-0 after:h-0.5 hover:after:w-full after:transition-all">
                            <Link
                                href={`./rendering/${type.toLowerCase()}`}
                                prefetch={false}
                            >
                                {type}
                            </Link>
                        </li>
                    );
                })}
            </ul>
            <p className=" absolute text-sm text-neutral-400 bottom-20">©mobiliverse 2023 By JangSeBaRi</p>
        </div>
    );
}

첫 화면 로드 시 서버를 통해 받아지는 자원들은 다음과 같다.

와우... 정말로 prefetch 가 일어나지 않았다. 그렇다면 Link 를 이용하여 csr 을 하는것이 불가능해 진 걸까... 아니면 페이지 이동 시 페이지에 필요한 리소스를 그때그때 불러오려나...

결론은 이건 또 아니다... 신기하게도 Link 태그가 달려있는 SSG 텍스트에 다음과 같이 마우스를 올렸는데

SSG 페이지를 그리기 위한 자바스크립트 파일을 다음과 같이 불러왔다.

아직 첫 화면에서 이동하지 않았기 때문에 맨 아래의 2개의 자원을 제외하고는 모두 이미 불러와 진 리소스이다. 결론은 prefetch 가 false 일 경우는 Link 가 뷰포트에 보이는 시점이 아닌 마우스가 hover 될 때 페이지 이동이 일어날 것 이라고 예상하고 이 시점에 자원을 받아온다... 결국 Link 를 사용하면 csr 로 화면을 그릴 수 있게 된다. nextjs 는 여러모로 참 대단한 프레임워크인 것 같다.

nextjs 는 ssr 를 지원하는 프레임워크이며 ssr 을 지원하기 위한 서버를 별도로 관리하고 있다. 때문에 nextjs 의 경우 아무렇게나 배포를 해버리면 nextjs 에서 ssr 를 위해 제공하는 서버를 사용할 수 없게 된다. 때문에 기본적으로 vercel 을 통해 nextjs 를 배포하곤 한다.

다음은 _OLI 님의 tistory 의 nextjs 이용한 배포방법 3가지 이다.

NextJs 배포

Vercel 배포

Vercel은 AWS lambda를 기반으로 운영되는 FaaS (Functions as a Service) 플랫폼입니다. 또한 Next JS를 개발한 회사이기 때문에 프로젝트 배포에 가장 최적화된 플랫폼입니다. Vercel의 가장 큰 장점은 기본적으로 HTTPS와 CDN이 적용된 상태로 배포됩니다. 그리고 git repository에 연결하면 변경된 코드를 메인 브랜치에 병합 시 코드를 빌드 후 배포 해주기 때문에 CI/CD 자동화에 대한 환경을 고려하지 않아도 됩니다.

사용정책: https://vercel.com/docs/platform/fair-use-policy

월 간 제한: https://vercel.com/docs/platform/limits#serverless-function-payload-size-limit

플랫폼의 모든 상업적 사용에는 Pro 또는 Enterprise 플랜이 필요합니다.

상업적 사용 기준

사이트 방문자에게 지불을 요청하거나 처리하는 모든 방법.
제품 또는 서비스 판매 광고.
사이트의 주요 목적으로 제휴 링크.
무료 버전

하루 최대 100회 배포
서버리스 함수 최대 12개 생성 가능 Next JS에서 지원하는 getStaticProps, getStaticPath, getServerSide 함수를 사용한 페이지는 최대 12개 생성 할 수 있음을 의미합니다. 위 함수를 사용하지 않은 페이지들은 자동으로 static HTML페이지로 생성되는데 이에 대한 제한은 없습니다.
https://cdn-images-1.medium.com/max/1600/0*Q00nhr4ha-aHiVMq

무료 버전에서 람다 모양의 파일이 12개가 넘으면 빌드 에러가 납니다.

무료 버전에서 람다 모양의 파일이 12개가 넘으면 빌드 에러가 납니다.
월 간 사용 가능한 대역폭 최대 100GB
월 간 서버리스 함수 실행 최대 100GB-Hrs 서버리스로 생성된 페이지는 기본 1GB가 할당됩니다. 예를 들어 이 함수가 1 초 동안 실행되는 경우 1GB-s로 요금이 청구됩니다. 즉, 1GB 메모리가 할당된 함수를 1초 동안 실행하는 프로그램을 최대 36만 회(100 x 3600) 호출할 수 있습니다. 실제 서비스에서는 서버리스 함수가 100ms이내로 호출된다는 가정 하에 최대 360만 회 호출이 가능합니다.
프로 버전

프로버전은 팀 구성원당 월 20$의 요금이 발생합니다.

하루 최대 3000번 배포 가능
서버리스 함수 최대 실행 시간 60초
월간 사용 가능 대역폭 최대 1TB
월 간 서버리스 함수 실행 최대 1000GB-Hrs
기업 버전

프로버전을 사용하다가 월 간 최대 사용량을 넘어서면 제한 설정을 문의를 해봐야 될 것 같습니다.

기업 버전 문의: https://vercel.com/contact/sales

EC2 배포

EC2 생성 후 Next JS프로젝트를 git으로 PULL 한 후 next build → next start 하면 배포됩니다. 가장 원초적인 방법이지만 서버에 대한 관리를 개발자가 직접 해야 됩니다. 간단하게 CI/CD 자동화부터 트래픽 관리까지 생각해 보면 얼마나 많은 서비스들이 추가되는지는 생략하겠습니다.

Next JS배포 자동화에 대한 글: https://velog.io/@_woogie/배포를-자동화해보자-feat.-Next-js-pm2-Nginx#빌드한-코드를-저장할-s3-만들기

S3 WebHosting 배포

next build && next export로 빌드된 next js 페이지들을 정적으로 추출하여 S3에 배포합니다. 이 방법으로 배포하게 된다면 서버리스 함수를 사용할 수 없게 되므로 next js에서 제공하는 getServerSideProps()와 getStaticPathProps()의 fallback:true기능을 사용할 수 없게 됩니다.

next js export사용 시 주의 사항: https://nextjs.org/docs/advanced-features/static-html-export

마치며

nextjs 의 작동방식을 공부하고 nextjs 를 사용하는 이유를 더 깊게 이해하는 것이 이번 블로그의 목표였는데 블로그를 적으면서 개념들을 더 명확하게 알고 가는거 같아 글로 정리해보길 잘했다는 생각이 들었다. 이번 글에는 ISR 에 대해서는 다루지 않았지만 ISR 을 적용하는 코드는 위의 github 에서 확인할 수 있으며 추가적으로 적용하는 사례등을 검색해가면서 배우면 좋을 거 같다.

혹시 글에 틀린 부분이 있다면 지적해주세요. (적극 환영합니다.)

참조

NextJs 공식사이트
[FE] SSR(Server-Side-Rendering) 그리고 SSG(Static-Site-Generation) (feat. NEXT를 중심으로)

profile
4년차 프론트엔드 개발자 장세진

0개의 댓글