NextJS13 Rendering(Server Components)

puka·2023년 10월 21일

이 글을 읽기 전에 React 서버 컴포넌트의 작동 방식을 읽어보시는 걸 추천드립니다.


NextJS13에서 공식문서 Rendering 부분을 정리하는 글입니다.
Server Components, Client Components, Composition Patterns를 설명해보도록 하겠습니다.

React 서버 컴포넌트를 사용하면 서버에서 렌더링하고 선택적으로 캐시할 수 있는 UI를 작성할 수 있습니다. Next.js에서는 렌더링 작업을 경로 세그먼트별로 더 분할하여 스트리밍 및 부분 렌더링을 가능하게 하며, 세 가지 서버 렌더링 전략이 있습니다

Server Components

서버 렌더링의 이점

서버에서 렌더링 작업을 수행하면 다음과 같은 몇 가지 이점이 있습니다

  • 데이터 가져오기(Data Fetching)
    서버 컴포넌트를 사용하면 데이터 가져오기를 데이터 소스에 더 가까운 서버로 옮길 수 있습니다. 이렇게 하면 렌더링에 필요한 데이터를 가져오는 데 걸리는 시간과 클라이언트가 요청해야 하는 양을 줄여 성능을 향상시킬 수 있습니다.

  • 보안(Security)
    서버 컴포넌트를 사용하면 토큰 및 API 키와 같은 민감한 데이터와 로직을 클라이언트에 노출할 위험 없이 서버에 보관할 수 있습니다.

  • 캐싱(Caching)
    서버에서 렌더링하면 결과를 캐시하여 후속 요청 및 사용자 전체에서 재사용할 수 있습니다. 이렇게 하면 각 요청에서 수행되는 렌더링 및 데이터 가져오기 양을 줄여 성능을 개선하고 비용을 절감할 수 있습니다.

  • 번들 사이즈(Bundle Sizes)
    서버 컴포넌트를 사용하면 이전에는 클라이언트 자바스크립트 번들 크기에 영향을 미쳤던 대규모 종속성을 서버에 유지할 수 있습니다. 클라이언트가 서버 컴포넌트용 JavaScript를 다운로드, 구문 분석 및 실행할 필요가 없으므로 인터넷 속도가 느리거나 성능이 낮은 기기를 사용하는 사용자에게 유용합니다.

  • 초기 페이지 로드 및 첫 번째 콘텐츠 페인트(Initial Page Load and First Contentful Paint (FCP))
    서버에서는 클라이언트가 페이지를 렌더링하는 데 필요한 JavaScript를 다운로드, 구문 분석 및 실행할 때까지 기다릴 필요 없이 사용자가 즉시 페이지를 볼 수 있도록 HTML을 생성할 수 있습니다.

  • 검색 엔진 최적화 및 소셜 네트워크 공유 가능성(Search Engine Optimization and Social Network Shareability)
    렌더링된 HTML은 검색 엔진 봇이 페이지 색인을 생성하는 데 사용하고 소셜 네트워크 봇이 페이지에 대한 소셜 카드 미리보기를 생성하는 데 사용할 수 있습니다.

  • Streaming(스트리밍)

    스트리밍 방식에서는 서버가 HTML 문서를 조각조각 나누어 조금씩 전송합니다. 이렇게 하면 클라이언트는 첫 번째 조각을 받자마자 브라우저가 화면에 표시할 수 있으므로 사용자 경험이 향상됩니다.

    다만, 모든 상황에서 스트리밍이 유용하지는 않습니다. 예를 들어, 큰 데이터 세트를 다루거나 복잡한 계산 작업이 필요한 경우에는 전통적인 SSR 방식이 더 효율적일 수 있습니다.

    스트리밍은 기본적으로 Next.js 앱 라우터에 내장되어 있습니다. 이를 통해 초기 페이지 로딩 성능은 물론 전체 경로의 렌더링을 차단하는 느린 데이터 가져오기에 의존하는 UI를 개선할 수 있습니다. 예를 들어 제품 페이지의 리뷰가 있습니다.

loading.js와 UI 컴포넌트를 사용하여 route segments 스트리밍을 시작할 수 있습니다. Loading UI and Streaming 아래에서 설명

Next.js에서 서버 컴포넌트 사용

기본적으로 Next.js는 서버 컴포넌트를 사용합니다. 이를 통해 추가 구성 없이 서버 렌더링을 자동으로 구현할 수 있으며, 필요한 경우 클라이언트 컴포넌트를 사용하도록 선택할 수 있습니다(클라이언트 컴포넌트 참조).

서버 컴포넌트는 어떻게 렌더링되나요?

서버에서 Next.js는 React의 API를 사용해 렌더링을 orchestrate합니다. 렌더링 작업은 개별 경로 세그먼트와 서스펜스 바운더리에 따라 청크로 분할됩니다.

각 chunk는 두 단계로 진행됩니다.

  1. React는 서버 컴포넌트를 React 서버 컴포넌트 페이로드(RSC 페이로드)라는 특수 데이터 포맷으로 렌더링합니다.
  2. Next.js는 RSC 페이로드와 클라이언트 컴포넌트 자바스크립트 명령어를 사용해 서버에서 HTML을 렌더링합니다.

그런 다음 클라이언트에서:

  1. HTML은 경로의 빠른 비대화형 미리보기를 즉시 표시하는 데 사용되며, 이는 초기 페이지 로드에만 사용됩니다.
  2. React 서버 컴포넌트 페이로드는 클라이언트 및 서버 컴포넌트 트리를 조정하고 DOM을 업데이트하는 데 사용됩니다.
  3. 자바스크립트 명령어는 클라이언트 컴포넌트를 채우고 애플리케이션을 대화형으로 만드는 데 사용됩니다.

💡 리액트 서버 컴포넌트 페이로드(RSC)란 무엇인가요?

RSC 페이로드는 렌더링된 React 서버 컴포넌트 트리의 압축된 바이너리 표현입니다.
클라이언트에서 React가 브라우저의 DOM을 업데이트하는 데 사용됩니다.
RSC 페이로드에는 다음이 포함됩니다.

  • 서버 컴포넌트의 렌더링 결과
  • 클라이언트 컴포넌트가 렌더링될 위치에 대한 플레이스홀더와 해당 자바스크립트 파일에 대한 참조
  • 서버 컴포넌트에서 클라이언트 컴포넌트로 전달된 모든 프로퍼티

서버 렌더링 전략

서버 렌더링에는 세 가지 하위 집합이 있습니다: 정적, 동적, 스트리밍입니다.

정적 렌더링 Default (Static Rendering)

정적 렌더링을 사용하면 빌드 시 또는 데이터 재검증 후 백그라운드에서 경로가 렌더링됩니다. 결과는 캐시되어 콘텐츠 전송 네트워크(CDN)로 푸시될 수 있습니다. 이 최적화를 통해 사용자와 서버 요청 간에 렌더링 작업 결과를 공유할 수 있습니다.

정적 렌더링은 정적 블로그 게시물이나 제품 페이지와 같이 경로에 사용자에 맞춤화되지 않고 빌드 시점에 알 수 있는 데이터가 있는 경우에 유용합니다.

동적 렌더링(Dynamic Rendering)

동적 렌더링을 사용하면 요청 시점에 각 사용자에 대한 경로가 렌더링됩니다.

동적 렌더링은 경로에 사용자에게 맞춤화된 데이터가 있거나 쿠키 또는 URL의 검색 매개변수와 같이 요청 시점에만 알 수 있는 정보가 있는 경우에 유용합니다.

즉, "경로가 동적으로 렌더링되는 경우"란 웹 페이지가 클라이언트에서 생성되지 않고 서버에서 실시간으로 생성된다는 것을 의미합니다

💡 캐시된 데이터가 있는 동적 경로
대부분의 웹사이트에서 경로는 완전히 정적이거나 완전히 동적인 것이 아니라 다양한 스펙트럼을 가지고 있습니다.
예를 들어, 일정 간격으로 재검증되는 캐시된 제품 데이터를 사용하는 이커머스 페이지와 캐시되지 않은 개인화된 고객 데이터가 있는 이커머스 페이지가 있을 수 있습니다.

Next.js에서는 캐시된 데이터와 캐시되지 않은 데이터가 모두 포함된 동적으로 렌더링된 경로를 가질 수 있습니다. 이는 RSC 페이로드와 데이터가 별도로 캐시되기 때문입니다. 따라서 요청 시 모든 데이터를 가져올 때 성능에 미치는 영향에 대해 걱정할 필요 없이 동적 렌더링을 선택할 수 있습니다.

전체 경로 캐시데이터 캐시에 대해 자세히 알아보세요.

동적 렌더링으로 전환

렌더링 중에 동적 함수 또는 캐시되지 않은 데이터 요청이 발견되면 Next.js는 전체 경로를 동적으로 렌더링하도록 전환합니다. 다음 표에는 동적 함수와 데이터 캐싱이 경로가 정적으로 렌더링되는지 동적으로 렌더링되는지에 미치는 영향이 요약되어 있습니다.

  1. 동적 함수가 없고 데이터가 캐시되어 있는 경우: 페이지는 정적으로 렌더링됩니다.
  2. 동적 함수가 있지만 데이터가 캐시되어 있는 경우: 페이지는 동적으로 렌더링됩니다.
  3. 동적 함수 없이 데이터가 캐시되지 않은 경우: 페이지는 동적으로 렌더링됩니다.
  4. 동적 함수와 캐시되지 않은 데이터 모두 있는 경우: 페이지는 역시나 동적으로 렌더링됩니다.

위 표에서 경로가 완전히 정적이 되려면 모든 데이터가 캐시되어야 합니다. 그러나 캐시된 데이터 가져오기와 캐시되지 않은 데이터 가져오기를 모두 사용하는 동적으로 렌더링된 경로를 가질 수 있습니다.

Next.js는 사용되는 기능과 API에 따라 각 경로에 가장 적합한 렌더링 전략을 자동으로 선택하므로 개발자는 정적 렌더링과 동적 렌더링 중 하나를 선택할 필요가 없습니다. 대신 특정 데이터를 캐시하거나 재검증할 시기를 선택하고 UI의 일부를 스트리밍하도록 선택할 수 있습니다.

동적 기능(Dynamic Functions)

동적 함수는 사용자의 쿠키, 현재 요청 헤더 또는 URL의 검색 매개변수와 같이 요청 시점에만 알 수 있는 정보에 의존합니다. Next.js에서 이러한 동적 함수는 다음과 같습니다.

  • cookies() 과 headers()
    서버 컴포넌트에서 이를 사용하면 요청 시 전체 경로가 동적 렌더링으로 선택됩니다.

  • useSearchParams()
    클라이언트 컴포넌트에서는 정적 렌더링을 건너뛰고 대신 클라이언트에서 가장 가까운 부모 Suspense 경계까지 모든 클라이언트 컴포넌트를 렌더링합니다.
    useSearchParams()를 사용하는 클라이언트 컴포넌트를 Suspense 경계로 감싸는 것이 좋습니다. 이렇게 하면 그 위에 있는 모든 클라이언트 컴포넌트가 정적으로 렌더링될 수 있습니다

useSearchParams에서 말한 Suspense의 예시코드

Static Rendering
경로가 정적으로 렌더링되는 경우, useSearchParams()를 호출하면 가장 가까운 서스펜스 경계까지의 트리가 클라이언트 측에서 렌더링됩니다.

이렇게 하면 페이지의 일부가 정적으로 렌더링되는 동안 searchParams를 사용하는 동적 부분이 클라이언트 측에서 렌더링됩니다.

Suspense 경계에서 useSearchParams를 사용하는 컴포넌트를 래핑하여 클라이언트 측에서 렌더링되는 경로의 일부를 줄일 수 있습니다.

// 파일 경로 app/dashboard/search-bar.tsx
'use client'
 
import { useSearchParams } from 'next/navigation'
 
export default function SearchBar() {
  const searchParams = useSearchParams()
 
  const search = searchParams.get('search')
 
  // 정적 렌더링을 사용할 때 서버에 기록되지 않습니다.
  console.log(search)
 
  return <>Search: {search}</>
}
// 파일 경로 app/dashboard/page.tsx
import { Suspense } from 'react'
import SearchBar from './search-bar'
 
// 이 컴포넌트는 서스펜스 경계에 대한 폴백으로 전달됩니다.
// 초기 HTML에서 검색창 대신 렌더링됩니다.
// 리액트 하이드레이션 중에 값을 사용할 수 있는 경우 폴백
// `<SearchBar>` 컴포넌트로 대체됩니다.
function SearchBarFallback() {
  return <>placeholder</>
}
 
export default function Page() {
  return (
    <>
      <nav>
        <Suspense fallback={<SearchBarFallback />}>
          <SearchBar />
        </Suspense>
      </nav>
      <h1>Dashboard</h1>
    </>
  )
}

Dynamic Rendering

경로가 동적으로 렌더링되는 경우 클라이언트 컴포넌트의 초기 서버 렌더링 중에 서버에서 useSearchParams를 사용할 수 있습니다.

💡 알아두면 좋은 내용 - 동적 경로 세그먼트 구성 옵션을 force-dynamic으로 설정하면 동적 렌더링을 강제할 수 있습니다. 즉, 특정 웹 페이지가 항상 동적으로 랜더되도록 설정할 수 있다는 것입니다.

// app/dashboard/search-bar.tsx
'use client'
 
import { useSearchParams } from 'next/navigation'
 
export default function SearchBar() {
  const searchParams = useSearchParams()
 
  const search = searchParams.get('search')
  
  // 초기 렌더링 중에는 서버에 기록되고
  // 이후 탐색 시에는 클라이언트에 기록됩니다
  console.log(search)
 
  return <>Search: {search}</>
}
// app/dashboard/page.tsx
import SearchBar from './search-bar'
 
export const dynamic = 'force-dynamic'
 
export default function Page() {
  return (
    <>
      <nav>
        <SearchBar />
      </nav>
      <h1>Dashboard</h1>
    </>
  )
}
  • searchParams
    searchParams를 사용하면 요청 시 페이지가 동적 렌더링으로 전환됩니다.
    즉, searchParams를 활용하는 웹페이지(즉, URL의 쿼리 문자열에 따라 내용이 달라지는 웹페이지)가 서버 사이드에서 실시간으로 생성되어 제공된다는 의미입니다.

이러한 함수를 사용하면 요청 시 전체 경로가 동적 렌더링으로 전환됩니다.

Loading UI and Streaming

특수 파일 loading.js를 사용하면 React Suspense로 의미 있는 로딩 UI를 만들 수 있습니다. 이 규칙을 사용하면 경로 세그먼트의 콘텐츠가 로드되는 동안 서버에서 즉각적인 로딩 상태를 표시할 수 있습니다. 렌더링이 완료되면 새 콘텐츠가 자동으로 교체됩니다.

즉시 로딩 상태(Instant Loading States)

즉시 로딩 상태는 탐색 시 즉시 표시되는 폴백 UI입니다. 스켈레톤, 스피너와 같은 로딩 표시기를 미리 렌더링하거나 표지 사진, 제목 등과 같이 작지만 의미 있는 부분을 향후 화면에 표시할 수 있습니다. 이를 통해 사용자가 앱이 반응하고 있음을 이해하고 더 나은 사용자 경험을 제공할 수 있습니다.

폴더 내에 loading.js 파일을 추가하여 로딩 상태를 생성합니다.

//app/dashboard/loading.tsx
export default function Loading() {
  // You can add any UI inside Loading, including a Skeleton.
  return <LoadingSkeleton />
}

같은 폴더에서 loading.js는 layout.js 안에 중첩됩니다. page.js 파일과 그 아래의 모든 하위 파일은 자동으로 경계로 감싸집니다.

💡 알아두면 유용해요

  • 서버 중심 라우팅을 사용하더라도 내비게이션이 즉시 실행됩니다. 즉, "서버 중심 라우팅을 사용하더라도 내비게이션이 즉시 실행됩니다."는 서버에서 페이지를 생성하고 로드하는 서버 사이드 렌더링(Server Side Rendering, SSR) 방식을 사용해도, 사용자가 다른 경로(페이지)로 이동하는 것이 즉시 이루어진다는 것을 의미합니다.
  • 내비게이션은 중단 없이 진행되므로 경로를 변경할 때 다른 경로로 이동하기 전에 - 경로의 콘텐츠가 완전히 로드될 때까지 기다릴 필요가 없습니다. 새 페이지를 로드하는 동안에도 내비게이션이 멈추지 않으며, 따라서 사용자는 새 페이지의 모든 컨텐츠가 로드될 때까지 기다릴 필요가 없다는 것을 의미합니다. 이것은 웹사이트의 반응성과 사용자 경험(UX)을 향상시킵니다.
  • 새 경로 세그먼트가 로드되는 동안 공유 레이아웃은 대화형 상태로 유지됩니다.
    새 페이지를 로딩하는 동안에도 공유된 레이아웃(예: 헤더, 푸터, 사이드바 등)은 그대로 유지되며 계속해서 상호작용할 수 있다는 것입니다

Streaming with Suspense

loading.js 외에도 자체 UI 컴포넌트에 대한 Suspense 바운더리를 수동으로 생성할 수도 있습니다. 앱 라우터는 Node.js 및 Edge 런타임 모두에 대해 Suspense를 사용한 스트리밍을 지원합니다.

0개의 댓글