Next.js 서버컴포넌트에서 searchParams 접근하기

독립적인 서버 컴포넌트에서 url 쿼리 파라미터 접근의 필요성

dukjjang

dukjjang

2025년 1월 9일

Next.js

서버 컴포넌트에서 searchParams가 필요한 상황

Next.js 애플리케이션을 개발하다 보면, 페이지 컴포넌트가 아닌 독립적인 서버 컴포넌트(예: 모달, 재사용 UI 요소)에서 현재 URL의 쿼리 파라미터(searchParams)에 접근해야 할 때가 있다.
클라이언트 컴포넌트라면 useSearchParams 훅으로 바로 쿼리 파라미터를 가져올 수 있는데, 서버 컴포넌트는 브라우저 환경이 아니니까 이 훅을 사용할 수 없다.

“그렇다면 서버 컴포넌트에서는 도대체 어떻게 사용자가 접속한 URL의 pathname이나 searchParams에 접근해야 할까?”

왜 서버 컴포넌트에서는 searchParams에 직접 접근할 수 없을까?

서버 컴포넌트는 서버에서 실행되기 때문이다. 브라우저에서 돌아가는 게 아니라는 말이다. 즉, 브라우저의 URL 자체에 직접 접근할 수 없는 환경이다. 그렇다고 해서 “서버 컴포넌트가 요청 URL 정보를 전혀 못 쓴다”는 건 아니지만, 브라우저가 보내는 데이터를 어떻게 받을지 별도의 방식을 고민해야 한다.

1차 해결 방법: 페이지 컴포넌트에서 Prop으로 전달하기

가장 먼저 떠올릴 수 있는 방법은 페이지 컴포넌트로부터 searchParams를 Prop으로 넘겨받는 것이다. 페이지 컴포넌트는 기본적으로 searchParams를 prop으로 받을 수 있기 때문에, 그걸 하위 컴포넌트(서버 컴포넌트 포함)로 넘겨주면 된다.

// pages/products.tsx (페이지 컴포넌트)
import ProductDetails from '@/components/ProductDetails';

export default function ProductsPage({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) {
  return (
    <div>
      <h1>Products</h1>
      <ProductDetails searchParams={searchParams} />
    </div>
  );
}

// components/ProductDetails.tsx (서버 컴포넌트)
export default async function ProductDetails({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) {
  const productId = searchParams.productId;
  // ... productId를 사용해서 상품 정보 조회 ...
  return (
    <div>
      {/* ... 상품 상세 정보 ... */}
    </div>
  );
}

이 방식은 단순하고 직관적이라 대부분의 상황에서 잘 작동한다. 그런데 문제는 Prop Drilling재사용성 저하다.

문제 1) Prop Drilling

만약 서버 컴포넌트가 페이지 구조의 깊은 곳에 있다면, searchParams가 필요하지도 않은 여러 중간 컴포넌트들을 거쳐야 한다. 이건 불필요한 코드가 늘어나고, 가독성도 떨어진다.

문제 2) 재사용성 저하

searchParams를 받아야 하는 서버 컴포넌트가 여러 곳에서 재사용된다면, 그 위치마다 Prop으로 넘겨줘야 한다. 역시 귀찮고, 중복도 생긴다.

결국 여기서 **“필요한 데이터는 사용하는 컴포넌트에서 바로 가져오는 게 낫다”**는 교훈을 얻게 된다.

최종 해결 방법: 미들웨어로 헤더를 추가해서 전달하기

그래서 고민한 방법이 미들웨어를 활용하는 것이다.
미들웨어라면 Next.js 서버에 요청이 들어왔을 때, 브라우저 URL(pathname, searchParams 등)을 참조할 수 있다. 이걸 이용해서, searchParams를 헤더에 담아서 서버 컴포넌트 쪽으로 넘겨주면 된다.

동작 순서는 대략 이렇다:

  1. 사용자 요청: 사용자가 브라우저에서 Next.js 앱에 접속한다.
  1. DNS 및 서버 도달: DNS 등을 거쳐 Next.js 서버에 요청이 들어온다.
  1. 미들웨어 실행: Next.js 서버의 middleware.ts가 실행된다. 이 시점에 request.nextUrl.searchParams 등 요청 정보를 확인할 수 있다.
  1. 헤더에 searchParams 추가: 미들웨어에서 searchParams를 문자열로 바꿔서 x-search-params 같은 커스텀 헤더에 담는다.
  1. 서버 컴포넌트 렌더링: 요청이 서버 컴포넌트까지 도달한다.
  1. 헤더에서 searchParams 추출: 서버 컴포넌트 내부에서 headers() 함수를 통해 x-search-params를 꺼내면 된다.

1) middleware.ts

// middleware.ts
import { NextResponse, NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const path = request.nextUrl.pathname;
  // ... 필요시 다른 로직 수행 ...

  // searchParams를 헤더에 추가
  const searchParams = request.nextUrl.searchParams.toString();
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-search-params', searchParams);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

2) searchParams 추출 유틸 함수 (utils/getSearchParams.ts)

// utils/getSearchParams.ts
import { headers } from 'next/headers';

export async function getSearchParams<T extends Record<string, string | string[]>>(): Promise<T> {
  const headersList = headers();
  const searchParamsString = headersList.get('x-search-params') || '';
  const searchParams = new URLSearchParams(searchParamsString);
  return Object.fromEntries(searchParams) as T;
}

3) 서버 컴포넌트에서 활용 (components/MyServerComponent.tsx)

// components/MyServerComponent.tsx
import { getSearchParams } from '@/utils/getSearchParams';

export default async function MyServerComponent() {
  const searchParams = await getSearchParams(); // 미들웨어에서 넣어준 값을 헤더에서 가져옴

  // 예: searchParams를 사용해 데이터 조회
  const data = await fetchDataBasedOnSearchParams(searchParams);

  return (
    <div>
      {/* ... 컴포넌트 내용 ... */}
    </div>
  );
}

async function fetchDataBasedOnSearchParams(searchParams: Record<string, string | string[]>) {
  // searchParams를 기반으로 데이터를 조회하는 로직
  // 예: const response = await fetch(`/api/data?${new URLSearchParams(searchParams)}`);
  // ...
  return {
    // ... 조회된 데이터 ...
  };
}

결론

이렇게 하면 서버 컴포넌트가 페이지 컴포넌트로부터 searchParams를 Prop으로 전달받을 필요 없이, 독립적으로 검색 파라미터에 접근할 수 있다. 덕분에

  • Prop Drilling 문제를 없앨 수 있고,
  • 코드 가독성이 좋아지며,
  • 여러 곳에서 재사용하기도 쉬워진다.

물론 이 방법을 쓰려면 미들웨어를 설정하고, 서버 컴포넌트 안에서 headers() 함수를 활용해야 한다. 그래도 한 번 해두면 깔끔하게 재사용할 수 있고, 유지 보수도 훨씬 편해진다.

정리하자면, 서버 컴포넌트에서 필요한 데이터는 스스로 가져올 수 있도록 만들어주는 게 좋다는 얘기다. 페이지 컴포넌트에 너무 의존하지 말고, 독립적인 역할을 할 수 있게 구조를 짜는 게 이상적인 형태라고 생각한다.