ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Bug Fix] Next.js 14 App Router 버전, useSearchParams 빌드 Error와 Suspense
    Front-end 개발 2024. 11. 25. 00:25

     

    프로젝트를 배포하기 위해서 npm run build 명령어를 수행한 결과 빌드 에러가 뙇!

    build Error: 정적 페이지를 생성하면서 useSearchParams를 사용한 컴포넌트는 suspense로 감싸줘야 한다.

     

    Problem


    Suspense 경계 없이 useSearchParams를 통해 매개변수를 읽으면 전체 페이지가 클라이언트 측 렌더링으로 선택된다. 왜냐하면 클라이언트 사이드에서 정적 렌더링 동안에 가장 가까운 Suspense boundary에서 렌더링을 하려고 하고, React는 useSearchParams 사용을 발견하면 가장 가까운 Suspense 경계까지 fallback으로 표시하기 위해 렌더링을 일시 중단한다. Suspense 설정이 없으면 타고타고 올라가서 전체 페이지가 클라이언트 측 렌더더링이 된다. 이로 인해 클라이언트 측 JavaScript가 로르될 때까지 페이지가 비어 있을 수 있기 때문에 빌드 에러가 발생한다.

    이로 인해 가장 큰 문제는 Vercel에서 빌드가 실패해서 배포가 불가능한 상황이다.

     

    Solution


    useSearchParams를 사용한 컴포넌트는 외부에서 Suspense 컴포넌트로 씌워줘야 한다. 다시 말해, Suspense 컴포넌트를 경계로 useSearchParams가 호출되도록 래핑해 준다.

    'use client'
     
    import { useSearchParams } from 'next/navigation'
    import { Suspense } from 'react'
     
    function Search() {
      const searchParams = useSearchParams()
     
      return <input placeholder="Search..." />
    }
     
    export function Searchbar() {
      return (
        // You could have a loading skeleton as the `fallback` too
        <Suspense>
          <Search />
        </Suspense>
      )
    }

     

    Suspense 태그로 감싸주고 fallback 속성 (스켈레톤 UI이나 로딩 바 같은 스피너 컴포넌트를 넣어주는 속성)을 추가하지 않아도 일단 해당 문제를 해결할 수 있다. 하지만 Data fetching이 있어서 동적 페이지 생성(SSR 방식)에 지연이 발생한다면 대체 UI를 넣어주는 것이 사용자 경험 향상 측면에서 좋다. 나의 경우에도 추후 로딩을 표시하는 스피너를 넣어줄 생각이다.

    ⚠️ 주의할 점은 useSearchParams를 호출하는 컴포넌트 자체를 Suspense로 감싸줘야 한다. return 문에서 일부 JSX 요소를 Suspense로 감싸주는 것은 의미가 없다. 나의 프로젝트에서 적용한 경우에는 예시 코드를 보면 보다 쉽게 이해할 수 있을 것이다. PaymentsComplete 페이지 컴포넌트에서 useSearchParams가 사용되는 컴포넌트를 분리하고 그 컴포넌트 자체를 Suspense로 감싸준 코드는 아래과 같다.

     

    function PaymentsInfo() {
      const searchParams = useSearchParams();
      const title = searchParams.get('model')?.toUpperCase() || '';
      const price = Number(searchParams.get('price')) || 0;
      return (
        <>
          <div className="flex flex-col">
            <h1 className="text-[60px] text-center text-white border-b-[1px] border-white max-[1366px]:text-[32px]">
              {title || ''}{' '}
            </h1>
            <h2 className="text-[40px] text-center mt-[0px] max-[1366px]:text-[30px]">
              결제에 성공했습니다!
            </h2>
          </div>
    
          <div>
            <h3 className="text-[20px] mb-2 text-right ">
              <span className="text-[25px] text-left max-[1366px]:text-[20px]">구입 가격 : </span>
              {price.toLocaleString() || ''}원
            </h3>
          </div>
        </>
      );
    }
    
    export default function PaymentsComplete() {
      return (
        <main>
          <div className="h-[calc(100vh-420px)] bg-black text-white p-4 flex flex-col items-center justify-center gap-y-[40px] max-[1366px]:h-screen">
            <Suspense fallback={<div>로딩 중 ...</div>}>
              <PaymentsInfo />
            </Suspense>
            <Link
              href="/"
              className="btnBasic border-[white] px-[40px] py-[15px] mt-[40px] hover:bg-white hover:text-black"
            >
              홈으로 돌아가기
            </Link>
          </div>
        </main>
      );
    }

     

    참고로, next.config.mjs 파일에 useSearchParams와 관련된 규칙을 비활성화 하는 것은 권장되지 않지만 가능하다. missingSuspenseWithCSRBailout 옵션을 false로 설정하면 비활성화되어 npm run build 명령어 실행 시에 에러를 표시가 Warning 수준으로 변경된다. 하지만 Vercel 배포에서는 빌드 에러인 것은 마찬가지라서 의미가 없다.

     

    // next.config.js
    module.exports = {
      experimental: {
        missingSuspenseWithCSRBailout: false,
      },
    }

     

    Reference


    참고 1. Next.js 공식 Docs, Misssing Suspense boundary with useSearchParams

Designed by Tistory.