Low Love - King Gnu

cd

obvoso

star

Suspense와 useSearchParams

Next.js의 app router와 hydrate

개발
Nextjs
React
thumbnail

서론

next/navigationuseSearchParams훅을 사용하는 컴포넌트에 ‘use client’ 지시어를 붙여 RCC로 만든 후, RootLayout에서 호출을 하니 아래와 같은 에러가 발생했습니다.

useSearchParams() should be wrapped in a suspense boundary at page "/main”

왜 이런 에러가 발생했는지, useSearchParams를 사용하는 컴포넌트를 Suspense 로 감싸주어야 하는지에 대해 찾아보며 정리한 글입니다.

오늘 글을 작성해 올해의 기술부채를 모두 청산하려 합니다. (ㅋㅋ) 새해 복 많이 받으세요.

문제의 코드

 //layout.tsx
 
 <html lang="en">
    <body>
      <Header />     //client component, using useSearchParams()
      {children}     //server component
      <BotttomTab /> //client component, using useSearchParams()
    </body>
  </html>

Root Layout에서 <Header><BottomTab> 컴포넌트를 호출합니니다.

두 컴포넌트는 내부적으로 next/navigationuseSearchParams훅을 사용하기 때문에 ‘use client’ 지시어를 최상단에 명시하여 만들었습니다.

하지만 빌드시 두 컴포넌트를 <Suspense>로 감싸주지 않았다고 에러가 발생했습니다.

제가 알고있기론, RSC에서 RCC를 import해서 사용하는거에는 문제가 없어야 했습니다.
기본적으로 스트리밍을 지원해 문제 없이 빌드가 되야하는 거 아닌가.. 하는 의문점이 들었지만 개발 기한을 맞추기 위해 우선 무지성으로 <Suspense>로 감싼 후 머지를 했습니다. 이렇게 쌓인 기술부채로 부자 될 겁니다.

Suspense

Suspense는 컴포넌트의 렌더링을 일시 중지하고 데이터 로딩을 기다릴 수 있게 해주는 React의 기능입니다.

컴포넌트가 비동기 데이터를 로딩하는 동안 <Header/>렌더링을 일시 중지하고 fallback UI를 보여주다가, 데이터가 준비되면 <Header/>를 정상적으로 렌더링하도록 해줍니다.

<Suspense fallback={<Loading />}>
  <Header/>
</Suspense>

<Suspense> 구문으로 감싸면, 해당 RCC가 클라이언트에서 필요한 데이터를 준비하기 전까지 UI를 어떻게 처리할지 명시적으로 React에게 알려줍니다.

RSC → RCC로 이어지는 경계에서 Suspense를 통해 클라이언트 측 데이터가 준비되지 않았을 때는 로딩 화면 혹은 fallback을 보여줄 수 있도록 처리하는 것입니다.

Suspense의 동작

  1. 컴포넌트가 렌더링을 시도

    컴포넌트가 특정 비동기 자원에 의존하고 있다면, 해당 자원이 아직 준비되지 않은 상태일 수 있습니다.

  2. 비동기 자원이 준비되지 않았다면 (Promise 상태라면)

    React는 이 컴포넌트의 렌더링을 잠깐 멈추고, 가장 가까운 Suspense boundary까지 찾아 올라가서 fallback UI를 표시합니다.

    즉, 해당 컴포넌트에 대한 본 렌더링 자체를 완전히 스킵하고 fallback UI로 대체하는 것입니다.

  3. 비동기 자원이 준비(완료)되면

    React는 멈췄던 렌더링(본 렌더링)을 재개하고, 실제 컴포넌트 UI를 화면에 표시합니다.

    이때, 기존 fallback UI가 사라지고 진짜 컴포넌트가 화면에 등장합니다.

React는 동시성(Concurrent Rendering)기반으로 동작합니다. 앱 전체의 렌더링 시도와 fallback UI 렌더링, 비동기 데이터 페칭이 병렬로 진행됩니다.

Next App router와 Suspense

Next.js 13+ App Router에서 Suspense boundary가 있으면, 다음 시나리오가 가능합니다.

  1. 서버 사이드 스트리밍

    서버 컴포넌트(RSC) 부분(HTML)은 빠르게 스트리밍됩니다.

    RSC 내부에서 RCC가 필요한 부분은 rsc payload에서Suspense fallback 또는 placeholder로 처리됩니다.

  2. 클라이언트 하이드레이션

    클라이언트가 초기화되며, 서버가 내려준 정보와 동기화를 시작합니다.

    RCC가 서버측 정보를 가져올 준비가 되면, Suspense fallback UI을 걷어내고 실제 UI로 전환합니다.

useSearchParams

useSearchParamsnext/navigation에서 제공하는 훅입니다. URL 정보를 클라이언트에서 읽어오는 기능을 담당합니다.

useSearchParams는 내부적으로 Next.js의 새로운 app/router 시스템(서버 라우팅 + 클라이언트 라우팅)을 통해 동작합니다.
단순히 클라이언트 전역의window.location.search나 클라이언트 라우터 로직에 의해 동기화 된 URLSearchParams를 사용하는 것이 아니라고 합니다.

Next.js에서는 서버 레이어의 라우트 정보와 클라이언트 레이어의 동적 라우팅이 서로 협업해서 어떤 URL을 보여줄지를 결정합니다.

Next.js App Router는 서버 렌더링 시점에도 URL을 인식하고, 이를 props 형태로 내려주거나 클라이언트 하이드레이션에서 동일한 URL 상태를 재사용합니다.

Suspense로 감싸주어야 하는 이유

결국 useSearchParams는 현재 URL의 query string을 Next.js Router가 어떻게 인식하고 있는가?에 기반하여, React에서 상태로서 받게 됩니다.

RSC 단계에서 URL을 읽을 수도 있지만, RCC가 그 값을 클라이언트 측에서 직접 접근하려면 결국 클라이언트 번들에서 해당 데이터가 준비될 때까지 대기해야 합니다.
준비되기 전 접근하려고 할 수 있기 때문에 에러메세지가 표시된 것입니다.

만약 Suspense boundary가 없으면, Next.js는 “아직 클라이언트 라우터 정보가 준비되지 않았는데, useSearchParams 쪽에서 접근하려 하네?” → “로딩 처리가 안 되면 에러”를 낼 수밖에 없습니다.

즉, useSearchParams가 포함된 RCC를 서버 컴포넌트에서 직접 import하면, Next.js는 서버 → 클라이언트 스트리밍 과정 중 해당 컴포넌트를 하이드레이션해야 할 시점을 정확히 알아야 하고, 그래서 Suspense 경계가 필요하다고 에러를 내는 것입니다.

use client의 코드 스플리팅

만능인 줄 알았던 use client 지시어를 붙였음에도 에러가 발생한 이유가 있었습니다.

use client를 붙이면 자동으로 lazy loading이 되는 줄 알았습니다. 하지만 해당 지시어는 컴포넌트를 rcc로 만들어 클라이언트가 실행해야 할 코드로 번들링이 될 뿐이었고, 렌더링이 될 때 무조건 lazy loading을 하라는 건 아니었습니다.

use client는 단지 클라이언트에서 실행되도록 설정할 뿐, lazy loading을 보장하지 않습니다. Lazy Loading을 확실히 구현하려면 React.lazy 또는 Next.js의 dynamic을 활용해야 합니다.

그렇지만 클라이언트 컴포넌트가 준비될 때까지 fallback UI를 표시하므로, lazy loading과 비슷한 사용자 경험을 제공할 수 있습니다.

경계를 설정해주어야 하는 이유

스트리밍 또는 파셜 렌더링 시점에서 RCC에 필요한 searchParams는 서버에서 준비될 때 또는 클라이언트에서 최종적으로 hydrate될 때가 다를 수 있으므로, 이 로직을 React에서 Suspense로 경계를 만들어 처리해주어야 합니다.

그렇기 때문에 useSearchParams를 사용하는 RCC가 <Suspense>로 감싸져 있을 경우,

Next.js는 여기서부터는 클라이언트가 쓰는 데이터가 아직 준비되지 않았을 수도 있다, 따라서 이 부분만큼은 Suspense FallBack으로 처리하고, 준비가 되면 hydrate 시켜야 한다. 라는 것을 명시적으로 알 수 있게 됩니다.

이것이 useSearchParams()가 “Suspense 경계 안에서 사용되어야 한다”는 에러 메시지의 근본적인 이유입니다…

왜 useSearchParams만 이런 에러 메세지가 발생하는가?

다른 클라이언트 훅들, 예를 들면useState, useEffect, useRef 등은 서버에서 받아와야 할 추가 정보 없이도 바로 동작합니다. 그냥 브라우저 JS에서 실행되면 끝이니까요.

하지만 useSearchParams, usePathname, useRouter 같은 Next.js 고유의 라우트 정보에 의존하는 훅들은, 서버데이터(서버 라우트 정보)와 클라이언트 라우터 상태가 동일하게 준비되어야만 정상 동작합니다.

그리고 이걸 부분적으로 늦게(hydration 시점에) 로딩해야 수도 있기 때문에, React Suspense를 통해 경계를 만들어주어 알려주어야하는 구조입니다.

결국, useSearchParams가 Next.js 라우터 상태를 동기화 한다는 점이 포인트이고, 이는 일반적인 단순 클라이언트 훅과 다르게 서버-클라이언트 협력이 필요하기 때문에 Suspense로 감싸줘야 하는 것입니다.

회고

오늘도 역시 공식문서에 답이 있었습니다. 그냥 블로그 이름을 RTFM로 변경할까 싶습니다.

Next.js가 새로운 라우팅을 도입하면서 새로 알아가야 할 부분이 여전히 많은 거 같습니다. useSearchParams이 내부적으로 window.location.search를 사용하는 줄 알았는데 아니라니… use client가 만능이 아니라니…

모르는 게 많다는 건 아직 성장할 부분이 많이 남았다는 거겠죠. 이게 긍정적으로 생각해야 하는 것인지 해탈한 것인지 감은 안 오지 만요.

프론트엔드 공부를 시작한지 곧 2년이 됩니다.
2년동안 했으면서 이것도 몰라? 싶겠지만 젠인가 상전술식인 십종영법술을 타고난 메구미도 식신 6종 밖에 못 다루고 명문 암살가 집안인 조르딕가 삼남 키르아도 넨에 대해 몰랐으니... 저도 그래도 되는 거 아닐까요. 사람마다 속도는 다른 법이죠…

블로그에 들러주시는 모든 분들(0명) 모두 새해 복 많이 받으시고, 등따시고 배부른 한 해 되시길 바라겠습니다.
올해는 취업하고 싶습니다. 많은 직·간접적 도움 감사합니다. 틀린 부분이 있다면 역시 지적 부탁드립니다.

레퍼런스

star

이전 포스트

List Virtualization 목록 가상화

react-window를 사용하여 가상리스트 구현하기

2024년 12월 22일