24
Posts
  • All (7)
  • React (5)
  • Nextjs (2)
  • thumbnail for Next.js URL 기반 테이블 상태 관리 구현하기

    Next.js URL 기반 테이블 상태 관리 구현하기

    페이지네이션, 필터링, 정렬처럼 사용자 인터랙션이 많은 테이블은 상태 관리가 쉽게 복잡해집니다. 특히 Next.js 앱 라우터의 서버 사이드 환경에서는 이러한 클라이언트 상태를 어떻게 다룰지가 고민거리입니다. 클라이언트 사이드에서 필터를 변경했을 때 서버에서 데이터를 어떻게 가져올지, URL이나 브라우저 뒤로가기 같은 웹의 기본 기능들을 어떻게 지원할지 등 고려해야 할 점이 많습니다. 이 글에서는 **@tanstack/react-table**과 **nuqs**를 조합하여 URL 기반의 테이블 상태 관리 시스템을 구축하는 방법을 다룹니다. 핵심 아이디어는 테이블의 모든 상태(페이지, 필터, 정렬 등)를 URL에 저장하여 상태를 공유 가능하게 만들고, 클라이언트의 상태 변경이 서버의 새로운 데이터 페칭으로 이어지는 구조입니다. --- ## 전체 아키텍처 개요 구현할 기능의 전체 동작 과정을 정리하면 ``` 사용자 인터랙션 (필터 변경, 페이지네이션 등) ↓ 클라이언트 사이드에서 URL 업데이트 (shallow: false) ↓ 서버사이드 렌더링 및 URL 파라미터 파싱 ↓ 새로운 파라미터로 API 호출 ↓ 클라이언트로 새 데이터 전달 및 UI 업데이트 ``` ## URL 파라미터 스키마 정의하기 테이블에서 어떤 정보들을 URL에 담을지 먼저 정해야 합니다. ```tsx //search-params.ts import { parseAsInteger, parseAsString, createSearchParamsCache } from 'nuqs/server'; export const searchParamsSchema = { page: parseAsInteger.withDefault(1), perPage: parseAsInteger.withDefault(10), email: parseAsString, role: parseAsString, createdAt: parseAsString, }; export const searchParamsCache = createSearchParamsCache(searchParamsSchema); ``` nuqs에서 제공하는 `searchParamsCache`는 URL 쿼리 파라미터를 파싱하고 타입 변환하여 메모리에 저장하는 캐시 객체입니다. ```tsx // URL: /users?page=2&email=john&perPage=20 // 이렇게 변환됩니다: { page: 2, perPage: 20, email: "john", role: undefined, createdAt: undefined } ``` `searchParamsCache`는 서버 사이드에서만 사용가능합니다. ## 서버에서 URL 기반 데이터 가져오기 사용자가 URL을 변경하면 Next.js는 이를 새로운 페이지 요청으로 인식하고, 가장 먼저 `page.tsx` 컴포넌트를 실행합니다. ```tsx // page.tsx export default async function Page(props: Props) { const searchParams = await props.searchParams; searchParamsCache.parse(searchParams); // 캐시에 URL 정보 저장 return ( <Suspense fallback={<DataTableSkeleton />}> <UserListingPage /> </Suspense> ); } ``` `/users?page=2&email=john` 같은 URL에서 쿼리 파라미터를 추출하고, 미리 정의된 스키마에 따라 `page`는 숫자로, `email`은 문자열로 타입을 변환한 뒤 메모리의 캐시에 저장합니다. 캐시에 저장된 파라미터들은 이후 하위 컴포넌트들에서 타입 안전하게 접근할 수 있게 됩니다. ### URL 파라미터를 API 요청으로 변환하는 컴포넌트 ```tsx // UserListingPage.tsx export default async function UserListingPage() { const page = searchParamsCache.get('page'); const email = searchParamsCache.get('email'); const limit = searchParamsCache.get('perPage'); const role = searchParamsCache.get('role'); const createdAt = searchParamsCache.get('createdAt'); const filters = { page, limit, ...(email && { email }), ...(role && { role }), ...(createdAt && { createdAt }), }; // 실제 API 호출 const response = await getUserList(filters); const users = response.data ?? []; const pageCount = response.totalPages ?? 1; // 데이터를 클라이언트 컴포넌트로 전달 return <UserTable data={users} pageCount={pageCount} columns={columns} />; } ``` ## 클라이언트에서 테이블 상태 관리 ```tsx // UserTable.tsx 'use client' export function UserTable({ data, pageCount, columns }) { const { table } = useDataTable({ data, columns, pageCount, shallow: false, // 서버 리페칭을 위해 false debounceMs: 500, }); return ( <DataTable table={table}> <DataTableToolbar table={table} /> </DataTable> ); } ``` shallow 옵션에 따라 URL 변경 시 동작이 결정됩니다. nuqs 내부 코드를 보면 shallow: false일 때만 Next.js 라우터를 호출합니다. ([소스 코드](https://github.com/47ng/nuqs/blob/next/packages/nuqs/src/adapters/next/impl.app.ts)) ```ts // 항상 실행 - 브라우저 URL 변경 history.pushState(null, "", url) // shallow: false일 때만 실행 - 서버 컴포넌트 재실행 트리거 if (!options.shallow) { router.replace(url, { scroll: false }) } ``` shallow: true면 history.pushState만 실행되어 URL만 바뀌고, shallow: false면 router.replace()가 추가로 호출되어 서버 컴포넌트가 재실행됩니다. 테이블 상태 변경이 서버의 데이터 페칭으로 이어져야 하므로 shallow: false를 사용합니다. ### useDataTable 훅 **nuqs 옵션 설정** ```tsx // useDataTable.ts export function useDataTable<TData>(props: UseDataTableProps<TData>) { const { debounceMs = 300, shallow = true, startTransition, // ... } = props; // nuqs에 전달할 옵션들 const queryStateOptions = React.useMemo(() => ({ history: 'replace', scroll: false, shallow, throttleMs: 50, debounceMs, clearOnDefault: false, startTransition, }), [shallow, debounceMs, startTransition]); } ``` nuqs는 URL 상태 관리를 위한 다양한 옵션들을 제공하는데, `queryStateOptions` 객체에 이런 설정들을 모아서 정의해둡니다. 이렇게 하면 이후에 사용할 모든 `useQueryState` 훅들이 동일한 동작 방식을 가지게 됩니다. 브라우저 히스토리 관리부터 성능 최적화, 스크롤 위치 유지까지 URL 상태 변경과 관련된 모든 세부 동작을 여기서 통합 관리하는 셈입니다. **URL과 React 상태 연결** ```tsx // 페이지네이션 상태를 URL과 연결 const [page, setPage] = useQueryState( 'page', parseAsInteger.withOptions(queryStateOptions).withDefault(1) ); const [perPage, setPerPage] = useQueryState( 'perPage', parseAsInteger.withOptions(queryStateOptions).withDefault(10) ); const pagination: PaginationState = React.useMemo(() => { return { pageIndex: page - 1, // URL은 1부터, React Table은 0부터 시작 pageSize: perPage, }; }, [page, perPage]); ``` `useQueryState`를 사용하면 테이블의 모든 상태가 URL에 저장되어서 브라우저의 기본 기능들(뒤로가기, 새로고침, URL 공유)이 자연스럽게 작동합니다. 무엇보다 사용자가 필터를 변경할 때마다 URL이 업데이트되고, 이것이 서버에서 새로운 데이터를 가져오는 트리거가 됩니다. --- ## 필터링 시스템 구현 **동적 파서 생성** 테이블에서 다양한 타입의 필터를 지원하려면 각 컬럼에 맞는 URL 처리 방식이 필요합니다. 이메일 검색은 단순 텍스트이지만, 역할 선택은 여러 값을 선택할 수 있고, 날짜는 범위로 처리해야합니다. ```tsx const columns = [ { id: 'email', }, { id: 'role', meta: { options: ['admin', 'user', 'guest'] // 다중 선택 } }, { id: 'createdAt', meta: { variant: 'dateRange' } } ]; const filterParsers = React.useMemo(() => { return filterableColumns.reduce((acc, column) => { const columnId = column.id ?? ''; if (!columnId) return acc; if (column.meta?.variant === 'dateRange') { // URL: ?createdAt=2023-01-01,2023-12-31 // 결과: ["2023-01-01", "2023-12-31"] acc[columnId] = parseAsArrayOf(parseAsString, ',').withOptions(queryStateOptions); } else if (column.meta?.options) { // URL: ?role=admin,user // 결과: ["admin", "user"] acc[columnId] = parseAsArrayOf(parseAsString, ',').withOptions(queryStateOptions); } else { acc[columnId] = parseAsString.withOptions(queryStateOptions); } return acc; }, {}); }, [filterableColumns, queryStateOptions]); const [filterValues, setFilterValues] = useQueryStates(filterParsers); ``` 결과적으로 URL이 `/users?email=john&role=admin,user&createdAt=2023-01-01,2023-12-31`이면: ```tsx filterValues = { email: "john", role: ["admin", "user"], createdAt: ["2023-01-01", "2023-12-31"] } ``` **디바운싱 적용** 성능을 위해 디바운싱을 적용합니다. 사용자가 500ms 동안 입력을 멈춘 후에야 URL이 업데이트되고 서버에서 새 데이터를 가져옵니다. ```tsx const debouncedSetFilterValues = useDebouncedCallback((values) => { void setPage(1); void setFilterValues(values); }, debounceMs); ``` ### 필터 삭제와 URL 정리 사용자가 검색 필터를 지웠을 때 URL에서도 해당 파라미터를 제거해야 합니다. ```tsx const onColumnFiltersChange = React.useCallback( (updaterOrValue: Updater<ColumnFiltersState>) => { setColumnFilters((prev) => { const next = typeof updaterOrValue === 'function' ? updaterOrValue(prev) : updaterOrValue; // 1. 현재 활성화된 필터들을 URL 업데이트 객체에 수집 const filterUpdates = next.reduce((acc, filter) => { if (filterableColumns.find(column => column.id === filter.id)) { acc[filter.id] = filter.value as string | string[]; } return acc; }, {}); // 2. 삭제된 필터들을 찾아서 null 값으로 설정 (URL에서 제거됨) for (const prevFilter of prev) { if (!next.some(filter => filter.id === prevFilter.id)) { filterUpdates[prevFilter.id] = null; } } debouncedSetFilterValues(filterUpdates); return next; }); }, [debouncedSetFilterValues, filterableColumns] ); ``` --- ## 마무리 이 글에서 다룬 내용은 Next.js 서버 컴포넌트 환경에서 클라이언트의 테이블 인터랙션을 URL을 통해 서버의 데이터 페칭과 연결하는 구조입니다. 사용자가 필터를 변경하거나 페이지를 넘기면, useDataTable 훅이 URL을 업데이트하고, shallow: false 설정으로 인해 서버 컴포넌트가 새로운 searchParams와 함께 재실행됩니다. 이때 searchParamsCache가 URL 파라미터를 파싱해서 API 호출에 사용하고, 최종적으로 새로운 데이터가 클라이언트로 전달되는 구조입니다. URL을 통한 상태 관리로 서버 렌더링의 성능 이점을 유지하면서도 클라이언트의 인터랙션을 자연스럽게 서버의 데이터 페칭과 연결할 수 있습니다. nuqs와 TanStack Table이 제공하는 타입 안전성과 함께 사용하면 복잡한 테이블 상태도 안정적으로 관리할 수 있습니다.

    2025년 09월 20일
  • thumbnail for Multi-Step Form

    Multi-Step Form

    웹 애플리케이션에서 복잡한 사용자 정보 수집이 필요할 때가 있습니다. 긴 회원가입 폼이나 설문조사를 한 페이지에 모두 담으면 사용자가 부담스러워하기 때문에, 여러 단계로 나누어 진행하는 것이 일반적입니다. 여러 단계의 회원가입이나 설문조사 폼을 구현할 때, 아래 코드처럼 useState와 switch문을 활용한 방식을 많이 사용합니다. ```tsx const [currentStep, setCurrentStep] = useState(0); const [formData, setFormData] = useState({}); const handleNext = (data) => { setFormData(prev => ({ ...prev, ...data })); setCurrentStep(prev => prev + 1); }; const renderStep = () => { switch(currentStep) { case 0: return <EmailStep onNext={handleNext} />; case 1: return <RoleStep onNext={handleNext} />; case 2: return <PasswordStep onNext={handleNext} />; default: return null; } }; ``` 코드자체는 간단하지만 몇가지 문제가 있습니다. **URL과 UI 상태 불일치** 이 코드에서는 URL이 폼의 진행 상태를 반영하지 못합니다. 사용자가 2단계까지 진행했어도 URL은 고정되어있습니다. - 브라우저 뒤로가기/앞으로가기 버튼 미작동: 사용자가 뒤로가기 버튼을 눌러도 폼의 단계는 변하지 않아 혼란을 줍니다. - 새로고침 시 초기화: 페이지를 새로고침하면 currentStep 상태가 초기화되어 사용자는 다시 처음부터 모든 정보를 입력해야 하는 불편함을 겪게 됩니다. - 링크 공유 불가: 특정 단계로 바로 이동할 수 있는 URL을 공유할 수 없습니다. **확장성, 유지보수 문제** 프로젝트가 복잡해질수록 점점 관리하기 어려워지는 문제가 있습니다. 처음에는 단순한 3단계의 Form이지만, 기획이 변경되면서 역할에 따라 다른 플로우를 보여줘야 하거나, 특정 조건에서만 나타나는 스텝이 추가되면 switch문이 복잡해집니다. 스텝이 늘어나고 조건부 로직이 추가될 때마다 복잡한 조건 분기가 들어가면서 코드의 가독성은 떨어집니다. ```tsx const renderStep = () => { switch(currentStep) { case 0: return <EmailStep onNext={handleNext} />; case 1: return <RoleStep onNext={handleNext} />; case 2: // 역할에 따른 분기 if (formData.role === 'enterprise') { return ( <CompanyInfoStep onNext={handleNext} onPrev={handlePrev} /> ); } else if (formData.role === 'individual') { return ( <PersonalInfoStep onNext={handleNext} onPrev={handlePrev} /> ); } else { return ( <BasicInfoStep onNext={handleNext} onPrev={handlePrev} /> ); } case 3: // 이전 스텝의 선택에 따른 분기 if (formData.role === 'enterprise' && formData.companySize > 100) { return ( <EnterpriseVerificationStep onNext={handleNext} onPrev={handlePrev} /> ); } else if (formData.needsApproval) { return ( <ApprovalRequestStep onNext={handleNext} onPrev={handlePrev} /> ); } else { // 일반 사용자는 바로 패스워드 스텝으로 setCurrentStep(4); return null; } case 4: return ( <PasswordStep onNext={handleNext} onPrev={handlePrev} /> ); case 5: return showTermsStep ? ( <TermsStep onNext={handleNext} onPrev={handlePrev} /> ) : ( <CompleteStep /> ); default: return <ErrorStep />; } }; // 뒤로 가기 로직도 마찬가지로 복잡해짐 const handlePrev = () => { if (currentStep === 3 && formData.role === 'individual') { setCurrentStep(2); } else if (currentStep === 4 && formData.needsApproval) { setCurrentStep(3); } else if (currentStep === 4 && formData.role === 'enterprise' && !formData.needsApproval) { setCurrentStep(2); } else { setCurrentStep(prev => prev - 1); } }; ``` --- ## Multi-Step Form 이런 문제들을 해결하고 더 나은 개발 경험을 제공하기 위해, URL과 상태가 동기화되고, 선언적이며, 타입 안전성까지 보장되는 새로운 Multi-Step Form 구현 방식을 소개합니다. ```tsx import { EmailStep, RoleStep, PasswordStep, type FunnelState, registerAction } from '@/features/auth'; import { useFunnel } from '@/shared/lib'; export default function RegisterPage() { const [Funnel, funnelState, updateFunnelState, clearFunnelState] = useFunnel( ['email', 'role', 'password'] as const, { initialStep: 'email', history: 'replace', } ).withState<Omit<FunnelState, 'currentStep'>>({}); return ( <Funnel> <Funnel.Step name="email"> <EmailStep defaultValue={funnelState.email} next={(email) => { updateFunnelState({ email, currentStep: 'role' }); }} /> </Funnel.Step> <Funnel.Step name="role"> <RoleStep defaultValue={funnelState.role} next={(role) => { updateFunnelState({ role, currentStep: 'password' }); }} /> </Funnel.Step> <Funnel.Step name="password"> <PasswordStep defaultValues={funnelState} next={async ({ password, confirmPassword }) => { await registerAction({ email: funnelState.email!, role: funnelState.role!, password, confirmPassword, }); clearFunnelState(); }} /> </Funnel.Step> </Funnel> ); } ``` --- ### 선언적인 스텝 정의 ```tsx <Funnel> <Funnel.Step name="email"><EmailStep /></Funnel.Step> <Funnel.Step name="role"><RoleStep /></Funnel.Step> <Funnel.Step name="password"><PasswordStep /></Funnel.Step> </Funnel> ``` 기존의 switch문과 숫자 인덱스를 사용한 명령형 방식과 달리, 폼의 전체 구조를 한눈에 파악할 수 있습니다. 새로운 스텝을 추가할 때도 단순히 `<Funnel.Step>` 태그 하나만 추가하면 되기 때문에 확장성도 뛰어납니다. --- ### type-safe 스텝 네비게이션 `['email', 'role', 'password'] as const`처럼 타입을 정확하게 지정함으로써 컴파일 타임에 오타나 잘못된 스텝명 사용을 방지할 수 있습니다. ```tsx const [Funnel, funnelState, updateFunnelState] = useFunnel( ['email', 'role', 'password'] as const // 리터럴 유니온으로 추론 ); // IDE에서 자동완성 지원 updateFunnelState({ email: 'test@test.com', currentStep: 'role' }); // 컴파일 에러 - 존재하지 않는 스텝명 updateFunnelState({ email: 'test@test.com', currentStep: 'invalid' }); ``` as const를 사용하면 타입이 단순한 string[]이 아닌 정확한 리터럴 유니온 타입으로 추론됩니다. 이로 인해 `updateFunnelState({ currentStep: 'invalid-step' })`처럼 존재하지 않는 스텝명을 사용하면 IDE에서 바로 에러가 표시됩니다. 스텝명 오타로 인한 런타임 오류를 완전히 방지할 수 있습니다. --- ### URL과 상태 동기화 nuqs 라이브러리를 활용해 URL 쿼리 파라미터와 React 상태를 동기화합니다. 사용자가 2단계에 있으면 URL도 `/register?funnel-step=role`처럼 실제 상태를 보여줍니다. 이로 인해 브라우저의 뒤로가기/앞으로가기 버튼이 자연스럽게 작동하고, 페이지를 새로고침해도 현재 스텝이 그대로 유지됩니다. 다음으로는 어떻게 구현하는지 살펴보겠습니다. --- ### URL 기반 스텝 관리- useRouteFunnel ```tsx export function useRouteFunnel<T extends FunnelSteps>( steps: T, options: RouteFunnelOptions<T> = {} ): [FunnelComponent, (step: T[number], navOptions?: NavigateOptions) => void] { const { initialStep, stepQueryKey = DEFAULT_STEP_QUERY_KEY, onStepChange, history: defaultHistory } = options; const defaultStep = initialStep ?? steps[0]; const [currentStep, setCurrentStep] = useQueryState( stepQueryKey, parseAsString.withDefault(defaultStep) ); const navigateToStep = (step: T[number], navOptions?: NavigateOptions) => { const { history = defaultHistory || DEFAULT_HISTORY } = navOptions || {}; setCurrentStep(step, { history }); onStepChange?.(step); }; const FunnelContainer = useMemo(() => { const Container = ({ children }: FunnelProps) => { return React.createElement(React.Fragment, null, children); }; const Step = ({ name, children }: FunnelStepProps) => { return currentStep === name ? React.createElement(React.Fragment, null, children) : null; }; (Container as FunnelComponent).Step = Step; return Container as FunnelComponent; }, [currentStep]); return [FunnelContainer, navigateToStep]; } ``` **URL과 상태 동기화** useQueryState를 사용해서 URL 쿼리 파라미터를 React 상태처럼 사용하는 것입니다. URL의 ?funnel-step=email 부분이 React 상태와 동기화됩니다. 사용자가 브라우저의 뒤로가기를 누르면 URL이 변하고, 그에 따라 currentStep 상태도 자동으로 업데이트됩니다. **History 방식 제어** `navigateToStep` 함수에서 history 옵션으로 브라우저 히스토리를 세밀하게 제어할 수 있습니다. 이는 nuqs 라이브러리가 내부적으로 제공하는 기능으로, 브라우저의 History API를 직접 조작합니다. push를 사용하면 `history.pushState()`가 호출되어 새로운 히스토리 엔트리가 브라우저 히스토리 스택에 추가됩니다. 이렇게 하면 사용자가 뒤로가기 버튼을 눌렀을 때 이전 스텝으로 돌아갈 수 있습니다 반면 replace 모드를 사용하면 `history.replaceState()`가 호출되어 현재 히스토리 엔트리를 덮어씁니다. 이 경우 사용자가 뒤로가기를 누르면 폼에 진입하기 전 페이지로 바로 이동합니다. **동적 컴포넌트 생성** ```tsx const Container = ({ children }) => { return React.createElement(React.Fragment, null, children); }; ``` 이 코드는 `<>{children}</>`과 동일합니다. 모든 자식 요소들을 그대로 렌더링하는 래퍼 역할입니다. ```tsx const Step = ({ name, children }) => { return currentStep === name ? React.createElement(React.Fragment, null, children) : null; }; ``` Step 컴포넌트는 자신의 name prop이 현재 활성화된 currentStep과 일치할 때만 `children`을 렌더링하고, 그렇지 않으면 null을 반환합니다. 즉, 현재 스텝이 아닌 모든 스텝들은 DOM에서 완전히 제거됩니다. ```tsx (Container as FunnelComponent).Step = Step; return Container as FunnelComponent; ``` 여기서 JavaScript의 함수 객체 특성을 활용합니다. 함수도 객체이므로 프로퍼티를 추가할 수 있습니다. Container 함수에 Step 프로퍼티를 붙여서 `Container.Step`으로 접근할 수 있게 만듭니다. 그래서 아래와 같이 사용가능합니다. ```tsx <Funnel> <Funnel.Step name="email">...</Funnel.Step> <Funnel.Step name="role">...</Funnel.Step> </Funnel> ``` --- ### 세션 스토리지 상태관리 - withState ```tsx export const withState = <TSteps extends FunnelSteps>( [Funnel, navigateToStep]: ReturnType<typeof useRouteFunnel<TSteps>> ) => <TState extends Record<string, unknown>>( defaultValue: Partial<TState> ) => { const [state, setState, clearStorage] = useSessionStorage( storageKey, defaultValue ); // 상태 업데이트와 네비게이션을 처리 const updateState = useCallback((action) => { setState((prevState) => { const newState = { ...prevState, ...action }; if (newState.currentStep) { navigateToStep(newState.currentStep); // URL도 함께 업데이트 } return newState; }); }, [setState, navigateToStep]); return [Funnel, state, updateState, clearState] as const; }; ``` withState는 기본적인 URL 스텝 관리에 세션 스토리지 기반 데이터 지속성 기능을 추가해주는함수입니다. URL만 관리하던 useRouteFunnel을 받아서 데이터 저장/복원 기능을 하게됩니다. 다음과 같은 3단계 과정을 통해 기능을 확장합니다: 1. useRouterFunnel의 반환값 `[Funnel, navigateToStep]`을 받아와서 2. 세션 스토리지 기반 상태 관리 기능을 더하고 3. `[Funnel, state, updateState, clearState]`로 반환 --- ### 통합 인터페이스 - useFunnel ```tsx export function useFunnel<T extends FunnelSteps>( steps: T, options: RouteFunnelOptions<T> = {} ) { const [Funnel, navigateToStep] = useRouteFunnel<T>(steps, options); const withState = _withState<T>([Funnel, navigateToStep]); return Object.assign([Funnel, navigateToStep], { withState }); } ``` useFunnel은 복잡한 내부 로직들을 감추고 간단한 인터페이스만 노출하는 추상화 레이어 역할을 합니다. 개발자는 URL 동기화나 세션 스토리지 처리 같은 세부 구현을 몰라도 되고, 필요한 기능만 체이닝으로 선택해서 사용할 수 있습니다. **기본 사용법 (URL 관리만)** ```tsx const [Funnel, navigateToStep] = useFunnel(['email', 'role', 'password']); ``` **확장 사용법 (세션 스토리지 사용)** ```tsx const [Funnel, state, updateState, clearState] = useFunnel( ['email', 'role', 'password'] ).withState({ email: '', role: '' }); ``` --- ### 마무리 지금까지 useState와 switch문으로 구현된 명령형 Multi-Step Form의 한계점을 짚어보고, 해결하기 위한 구현 방식을 살펴보았습니다. - useRouteFunnel: URL 쿼리 파라미터(nuqs 활용)를 통해 스텝 상태를 관리하여, 브라우저 히스토리와 완벽히 연동되고 새로고침에도 스텝을 유지할 수 있습니다. - withState: useRouteFunnel 위에 세션 스토리지를 얹어, 사용자가 입력한 데이터를 안전하게 저장하고 복원할 수 있는 지속성 계층을 제공합니다. - useFunnel: 이 모든 기능을 하나의 통합된 인터페이스로 묶어, 필요에 따라 유연하게 체이닝하며 확장할 수 있는 편리한 개발 경험을 제공합니다. 이 패턴을 사용하면 복잡한 조건 분기와 URL-UI 상태 불일치 문제를 해결할 수 있습니다. 선언적 구조와 TypeScript 타입 안전성 덕분에 코드 가독성이 높아지고, 새로운 기능 추가도 수월해집니다.

    2025년 03월 13일
© All rights reserved. Powered by dokimion24.