Ian's Archive 🏃🏻

thumbnail
Next.js App Router에서 SSR과 Client 상태 동기화
React
2025.07.21.

Next.js App Router는 서버 컴포넌트(RSC)를 기반으로 동작하여 서버 사이드 렌더링(SSR)의 이점을 누릴 수 있다.

하지만 검색, 필터링, 페이지네이션처럼 URL의 검색 파라미터(?page=2&keyword=test)와 상호작용해야 하는 기능을 구현할 때 두 가지를 모두 고려해야 해서 까다롭다.

서버 컴포넌트: 페이지에 처음 진입할 때 URL의 searchParams를 읽어서 초기 데이터를 가져와야 한다.

클라이언트 컴포넌트: 사용자의 입력(검색, 페이지 이동)에 따라 URL을 동적으로 바꾸고 UI를 업데이트해야 한다.

서버 컴포넌트에서 초기 데이터를 설정하고, 클라이언트 컴포넌트에서 이 값을 이어받아 동적인 상호작용을 처리하면서도 URL 파라미터를 일관되게 유지하는 방법을 코드를 통해 살펴본다.

(구현하느라 꽤 애먹어서 정리해 둔다.)

1. 페이지 컴포넌트 (서버) : 초기 데이터 준비

먼저 페이지의 진입점 역할을 하는 서버 컴포넌트다. 이 컴포넌트는 URL의 searchParams를 받아 서버에서 초기 데이터를 가져온 뒤, 클라이언트 컴포넌트에 넘겨주는 역할을 한다.

copyButtonText
interface PageProps {
  searchParams: {
    page?: string
    size?: string
    keyword?: string
    searchField?: string
  }
}

export default async function JobManagementPage({ searchParams }: PageProps) {
  const params = await Promise.resolve(searchParams)

  const page = Number(safe(params.page, '1'))
  const size = Number(safe(params.size, '10'))
  const keyword = safe(params.keyword, '')
  const searchField = safe(params.searchField, 'clct_data_nm')

  const result = await serverGet<ApiResponse<SchedulListResponse>>(
    `${process.env.NEXT_PUBLIC_CLCT_URL}/wrk/api/clct/scheduls`,
    {
      page,
      size,
      keyword,
      searchField,
    },
  )

  const data = result?.data?.data ?? {
    list: [],
    meta: {
      page,
      size,
      totalCount: 0,
      totalPages: 1,
      hasNext: false,
      hasPrevious: false,
    },
  }

  return (
    <div className={styles.mainContent}>
      <div className={styles.breadcrumbWrapper}>
        <ul className={styles.breadcrumb}>
          <li></li>
          <li>연계 작업 관리</li>
          <li>연계 작업 관리</li>
        </ul>
      </div>

      <h2 className={styles.pageTitle}>연계작업관리</h2>

      <DataTable
        initialSearchParams={{ page, size, keyword }}
        initialData={data}
      />
    </div>
  )
}
  • Next.js App Router는 페이지 컴포넌트에 URL의 쿼리 스트링을 파싱한 searchParams 객체를 자동으로 props로 전달해 준다.
  • 이렇게 받은 파라미터를 사용해 서버 환경에서 직접 API를 호출하여 데이터를 가져온다.
  • 마지막으로, 가져온 데이터(initialData)와 이때 사용된 파라미터(initialSearchParams)를 클라이언트 컴포넌트인 에 props로 넘겨준다.

2. UI 컴포넌트 (클라이언트): 상태 동기화 및 상호작용

copyButtonText
'use client'

import { useRouter, useSearchParams } from 'next/navigation'
import { useState, useEffect } from 'react'

export default function DataTable({
  initialSearchParams,
  initialData,
}: DataTableProps) {
  const router = useRouter()
  const searchParams = useSearchParams() // 클라이언트에서 현재 URL 파라미터 읽기

  // 1. 서버에서 받은 초기 파라미터를 fallback으로 사용
  const fallbackPage = initialSearchParams.page
  const fallbackSize = initialSearchParams.size
  const fallbackKeyword = initialSearchParams.keyword ?? ''

  // 2. 현재 URL 파라미터를 우선으로 사용, 없으면 fallback 값 사용
  const page = Number(searchParams.get('page') || fallbackPage)
  const size = Number(searchParams.get('size') || fallbackSize)
  const keyword = searchParams.get('keyword') ?? fallbackKeyword
  const initialSearchField = initialSearchParams.searchField || 'clct_data_nm'

  // 3. 사용자 입력을 위한 React State (UI 상태)
  const [searchField, setSearchField] = useState(initialSearchField)
  const [searchKeyword, setSearchKeyword] = useState(keyword)

  // ... (생략) ...

  const handleSearch = () => {
    // 4. URL을 업데이트하여 서버에 새로운 데이터 요청
    const query = new URLSearchParams({
      page: '1', // 검색 시에는 첫 페이지로
      size: size.toString(),
      keyword: searchKeyword,
      searchField: searchField,
    }).toString()
    router.push(`?${query}`)
  }

  return (
    <>
      <div className={styles.searchBox}>
        {/* ... */}
        <input
          type="text"
          value={searchKeyword} // React state와 연결
          onChange={e => setSearchKeyword(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && handleSearch()}
        />
        <button onClick={handleSearch}>검색</button>
        {/* ... */}
      </div>

      {/* ... 테이블 및 페이지네이션 렌더링 ... */}
      <Pagination currentPage={page} totalPages={totalPages} />
    </>
  )
}
  • searchParams.get(‘page’) || fallbackPage 이 부분이 핵심이다. 클라이언트에서 useSearchParams 훅을 통해 현재 URL의 파라미터를 우선적으로 가져온다. 만약 URL에 값이 없다면(예: 최초 페이지 로드), 서버에서 props로 내려준 초기값(fallbackPage)을 사용한다. 덕분에 SSR 결과물과 클라이언트의 첫 렌더링이 일치하여 화면 깜빡임이 없다.
  • handleSearch 함수는 router.push()를 호출하여 브라우저의 URL을 변경한다. URL이 바뀌면 Next.js는 이 변경을 감지하고, 새로운 searchParams와 함께 서버 컴포넌트(JobManagementPage)를 다시 실행한다. 이 과정이 반복되면서 데이터가 자연스럽게 갱신된다.
Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.