...loading
2025-02-27
최근들어, NextJs를 잘 이해하고 사용하고 있는지 자문해본다. 이 블로그도 NextJs로 개발한 프로젝트이지만, 기능 구현에 급급해서 정작 NextJs를 잘 이해하고 활용하지 못한 것 같다. 특히 NextJs의 서버사이드 렌더링과 서버 컴포넌트의 활용에 대해 이해가 필요한 것 같다. 따라서 NextJS의 렌더링을 중점으로 포스팅을 진행하고자 한다.
NextJS는 서버사이드 렌더링을 채택하고 있는 프레임워크로, 초기 HTML을 서버에서 생성하여 클라이언트에게 전달한다.
서버사이드 렌더링은 서버에서 완성된 HTML을 브라우저에 전달하는 방식이다. 따라서 NextJS 프로젝트에서 렌더링 되는 컴포넌트는 서버 측에서 미리 완성되어 전달된 HTML파일이다. 그리고 브라우저는 이미 완성된 HTML파일을 전달받았기 때문에, 클라이언트에게 빠르게 컨텐츠를 페인팅하여 보여줄 수 있다.
NextJS의 렌더링을 보다 구체적으로 이해하기 위해 기존 리액트의 CSR(클라이언트사이드 렌더링)과 비교할 수 있다. 두 렌더링은 그 과정에서 큰 차이가 존재한다.
: 서버에서 React 컴포넌트를 실행하여, 완성된 HTML문자열을 만든다.
: 브라우저는 콘텐츠가 채워진 HTML을 받는다. 따라서 HTML에 포함된 JS파일을 다운로드하지 않아도 초기 콘텐츠를 표시할 수 있다.
: 브라우저는 JS파일을 실행하여 기존의 HTML과 연결하여 활성화한다. 이를 하이드레이션이라 한다. 하이드레이션은 정적인 HTML을 컴포넌트 로직, 상태 관리, 이벤트 핸들러 등의 React의 코드와 연결하는 것이다.
: 브라우저는 서버에게 비어있거나 최소한의 HTML파일을 받는다.
: 브라우저는
: 브라우저는 JS파일을 파싱하고 React 컴포넌트들이 호출되며 가상 DOM이 생성된다.
: 가상 DOM을 반영하여 브라우저의 렌더링이 진행된다.
SSR과 CSR의 과정을 살펴보았다. 두 렌더링 과정 차이에서 가장 중요한 포인트는 브라우저가 초기 서버로부터 완성된 HTML파일을 받느냐의 여부이다. NextJS는 완성된 HTML파일을 받기에 리액트 코드를 브라우저가 실행하기 이전, 하이드레이션이 진행되기 이미 이전부터 정적인 콘텐츠를 페인팅하여 사용자에게 보여줄 수 있따. 그리고 이 지점으로부터 성능적인 이점을 경험할 수 있다.
특히 컴포넌트들이 많고 복잡한 페이지일수록 SSR이 성능적으로 유리할 수 있다. CSR의 경우 브라우저가 JS를 파싱하여 렌더링하기 전까지 사용자는 빈 화면을 보게 되지만, SSR의 경우 서버로부터 전달받은 이미 완성된 정적인 컨텐츠들을 미리 볼 수 있기 때문이다.
NextJS에서는 'use client'구문을 통해 클라이언트 컴포넌트를 사용할 수 있다. 그렇다면 클라이언트 컴포넌트는 CSR의 방식이 적용되기 때문에 SSR 적용이 안되는 것일까? 기본적으로 아니라 할 수 있다. 부모 컴포넌트가 서버 컴포넌트라면, 클라이언트 컴포넌트의 정적인 콘텐츠 역시 서버에서 HTML로 렌더링된다. 다만, useEffect와 같은 클라이언트 전용 JS 로직은 JS 번들로 전달되어 하이드레이션을 거쳐 기존 HTML과 연결된다.
NextJS의 루트 컴포넌트는 기본적으로 서버 컴포넌트다. 따라서 그 하위의 컴포넌트들은 서버/클라이언트 컴포넌트인지를 떠나서 SSR이 적용된다고 볼 수 있다. 하지만 만약 루트 컴포넌트를 클라이언트로 전환한다면 이야기가 달라질 것이다. 최상위 컴포넌트가 클라이언트 컴포넌트이기에 프로젝트 전체가 CSR 방식으로 전환된다. 클라이언트 컴포넌트에서도 SSR의 이점이 적용될 수 있는 전제 조건은 부모 컴포넌트가 서버 컴포넌트이어야 함이다.
NextJS의 렌더링(SSR)을 살펴보면 의문이 한 가지 생긴다. NextJs의 렌더링에서 하이드레이션이 상당히 중요한 매커니즘인데, 브라우저의 렌더링과 모순되는 부분이 있는 것 같다. 브라우저 렌더링의 기본적인 과정을 생각해보면 의문이 생긴다. 브라우저는 HTML을 파싱하는 과정에서 JS파일을 만나면 HTML 파싱을 중단한다. 그런데 NextJS는 어떻게 하이드레이션 이전에 HTML 내용을 페인팅하여 사용자에게 보여줄 수 있을까?
그 이유는
<script src="/_next/static/chunks/main.js" defer></script>
해당 속성을 사용하면, HTML 파싱을 중단하지 않고 파싱이 완료된 후 JS파일을 로드하게 된다. NextJS에서는 자동으로 defer 속성이 사용되도록 설정되어 있다. 따라서 하이드레이션 이전에 초기화면을 대한 렌더링하여 사용자에게 보여줄 수 있는 것이다.
NextJS는 기본적으로 SSR의 방식을 지원하고 있다. 하지만 클라이언트 컴포넌트를 사용하여 일부 컨텐츠에 대해서는 리액트의 CSR의 방식을 지원하여 개발자에게 여러 옵션을 제공한다. NextJS는 이러한 두 가지 렌더링 옵션 이외도 SSG(Static Site Generation)을 제공한다. SSG는 정적인 내용의 페이지를 한번만 생성하여, CDN에 저장해두어 재사용하는 방식의 렌더링 옵션이다. 즉, SSG는 본 블로그의 랜딩 페이지처럼 매번 최신화가 필요없거나 정적인 페이지에 적합하다. 이러한 페이지는 매번 서버/클라이언트 사이드에서 렌더링 과정을 거칠 필요가 없다. SSG를 통해 정적 페이지의 HTML을 빌드 시점 혹은 설정된 주기에만 생성하여 효율적으로 사용할 수 있다.
++ NextJS는 빌드 시에만 HTML을 생성하는 SSG의 특성을 확장하여, ISR(Incremental Static Regeneration)이라는 업그레이드 된 SSG방식을 제공한다. 이는 Revalidation 주기를 설정할 수 있도록 하여, 주기마다 HTML을 재생성하여 보다 최적화된 페이지를 제공하는 것이다. 본 포스팅에서는 편의상 ISR 또한 SSG로 통합하여 작성한다.
NextJS에서는 캐싱 옵션을 통해 손쉽게 활용할 수 있는 SSG 옵션을 제공한다. cache: "force-cache"
또는 next: { revalidate: N }
을 사용하여 SSG를 페이지에 적용할 수 있다. 즉, 캐싱을 강제하거나 캐싱 주기를 설정하여 SSG를 적용하는 것이다. 이는 정적인 HTML 컨텐츠를 리턴하는 서버컴포넌트에 적용 가능하며, 서버 컴포넌트에서 호출하는 데이터 요청 API에도 적용 가능하다.
코드예시와 같이 서버컴포넌트의 최상단에 간단히 설정하는 것으로 SSG 적용이 가능하다.
export const revalidate = 3600; // SSG 적용 export default function ServerComponent() { return ( <div> <h1>Blog Posts</h1> <ul> <li>Post 1 - Static Content</li> <li>Post 2 - Static Content</li> <li>Post 3 - Static Content</li> </ul> </div> ); }
아래 코드는 본 블로그에서 사용하는 API 요청 함수다. revalidate 설정을 통해 SSG를 적용하고 있다. 중요한 포인트는 해당 요청을 호출하는 컴포넌트가 서버컴포넌트이어야 한다는 점이다.
export async function fetchPostList(): Promise<PostDataProps[]> { const response = await fetch( `${process.env.PUBLIC_URL}/api/post/get-all-posts`, // 서버주소 { next: { revalidate: STALE_TIME } } // SSG 적용 ); if (!response.ok) { throw new Error("Failed to fetch data"); } return await response.json(); }
이상으로 NextJS의 렌더링에 대하여 알아보았다. 프로젝트를 구현에 급급하여 잘 알지 못하고 넘어갔던 부분들이 확실히 많았던 것 같다. 특히 NextJS의 렌더링 방식에 대해 잘 알지 못했던 찝찝함이 조금은 해소되었다. 그리고 포스팅을 마치니 블로그 코드의 리팩토링 방향성도 잡아나갈 수 있을 것 같다. 곧 있을 리팩토링의 목표는 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 사용하여 SSR 및 SSG의 장점을 잘 녹여내는 방향이 될 것 같다.
추가적으로 NextJS의 장점에 대해 더 명확하게 학습했다, 또한 NextJS가 렌더링에 대해서 다양한 옵션을 제공함으로써, 개발자 및 사용자의 편의성을 증대시키고 또한 개선해나가고 있다는 인상을 받는다. 추후 프로젝트에 있어서도 특별한 이유가 아니라면, NextJS를 사용할 것 같다. 내가 느끼기에 NextJS는 미니멀한 풀스택 구현이 가능하고 배포가 쉽다는 점이 매력적이고, 다양한 렌더링 옵션을 선택할 수 있다는 것도 큰 장점이다.
어쩌다보니 포스팅에서 NextJS찬양자가 된 것 같지만.. NextJS는 개발자가 다양한 상황들을 고려하면서도 개발에 집중할 수 있도록 만들어주는 프레임워크같다. 그리고 이러한 과정에서 개발자로 한걸음 더 성장한 것 같다.
Comments