DEV Community

Gahyun
Gahyun

Posted on

Next.js: URL Query Parameter로 컴포넌트 간 상태 동기화하기

문제 상황

Next.js App Router 기반 프로젝트에서 다음과 같은 구조가 있다고 가정해보자.

Layout
├── SheetNavigation  (사이드바 - 시트 목록을 fetch하여 표시)
└── Page
    └── SheetDetail  (특정 시트 상세 - 시트 삭제 기능 포함)
Enter fullscreen mode Exit fullscreen mode

SheetDetail에서 시트를 삭제하면, SheetNavigation의 시트 목록도 새로고침되어야 한다.

두 컴포넌트는 부모-자식 관계가 아니라 형제 관계이고, 레이아웃 레벨에 각각 배치되어 있다. 가장 먼저 떠오르는 방법들:

방법 단점
Props 콜백 전달 두 컴포넌트의 공통 부모를 통해야 해서 구조 변경 필요
Context / Redux 작은 문제에 비해 과도한 설정
localStorage 이벤트 같은 탭에서는 storage 이벤트가 발생하지 않음
URL Query Parameter ✅ 간단하고 명시적

핵심 아이디어

페이지 이동 시 URL에 신호용 Query Parameter를 달아서 보내고,

목적지 컴포넌트가 해당 파라미터를 감지하면 원하는 동작을 실행한다.


구현

1단계 — 삭제 후 이동 시 파라미터 추가

// SheetDetail 컴포넌트 (삭제 실행 측)
const confirmDeleteSheet = () => {
  deleteSheetApi(sheet.id, {
    onSuccess: () => {
      // ❌ 기존: 그냥 이동
      // router.push(`/${locale}/dashboard`)

      // ✅ 변경: refresh 신호를 파라미터로 전달
      router.push(`/${locale}/dashboard?refresh=sheet-list`)
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

2단계 — 신호를 수신하여 목록 갱신

// SheetNavigation 컴포넌트 (신호 수신 측)
import { useSearchParams, useRouter } from 'next/navigation'

const SheetNavigation = () => {
  const router = useRouter()
  const searchParams = useSearchParams()

  // 기존: ready/session 변경 시에만 목록 fetch
  useEffect(() => {
    if (!ready) return
    getSheetsList()
  }, [ready, session?.user?.club_id])

  // 추가: refresh 파라미터 감지 시 목록 갱신
  useEffect(() => {
    if (searchParams?.get('refresh') === 'sheet-list') {
      getSheetsList()

      // 파라미터를 URL에서 제거 (뒤로가기 시 재실행 방지)
      router.replace(`/${locale}/dashboard`)
    }
  }, [searchParams])

  // ...
}
Enter fullscreen mode Exit fullscreen mode

동작 흐름

[SheetDetail] 삭제 확인
      │
      ▼
router.push('/dashboard?refresh=sheet-list')
      │
      ▼
[SheetNavigation] searchParams 변경 감지
      │
      ├─ refresh === 'sheet-list' ?
      │         │
      │         ▼
      │   getSheetsList() 호출  → 목록 UI 갱신
      │         │
      │         ▼
      │   router.replace('/dashboard')  → URL 정리
      │
      └─ 아니면 무시
Enter fullscreen mode Exit fullscreen mode

router.replace로 파라미터를 지워야 할까?

1. /dashboard?refresh=sheet-list  (push로 진입)
2. router.replace('/dashboard')   (파라미터 제거)
Enter fullscreen mode Exit fullscreen mode

replace를 쓰는 이유는 히스토리 스택을 오염시키지 않기 위해서다.

만약 push를 쓰면 사용자가 뒤로가기를 눌렀을 때 파라미터가 있는 URL로 돌아가고, searchParams 변경이 다시 트리거되어 getSheetsList가 중복 실행된다.


다른 방법과 비교

// ❌ localStorage - 같은 탭에서는 이벤트 미발생
localStorage.setItem('refresh', 'sheet-list')
window.addEventListener('storage', handler) // 다른 탭에서만 작동

// ❌ Custom Event - 페이지 이동 시 이벤트 리스너가 언마운트됨
window.dispatchEvent(new CustomEvent('sheet-deleted'))

// ✅ Query Parameter - 페이지 이동과 신호 전달이 동시에 처리됨
router.push('/dashboard?refresh=sheet-list')
Enter fullscreen mode Exit fullscreen mode

응용: 여러 신호를 하나의 파라미터로 관리

// 이동 측
router.push(`/dashboard?refresh=sheet-list`)
router.push(`/dashboard?refresh=member-list`)
router.push(`/dashboard?refresh=all`)

// 수신 측
useEffect(() => {
  const refresh = searchParams?.get('refresh')

  if (refresh === 'sheet-list' || refresh === 'all') getSheetsList()
  if (refresh === 'member-list' || refresh === 'all') getMemberList()

  if (refresh) router.replace('/dashboard')
}, [searchParams])
Enter fullscreen mode Exit fullscreen mode

정리

  • 구조 변경 없이 독립된 두 컴포넌트 간 상태 동기화 가능
  • useSearchParamsrouter.replace수신 후 URL을 깔끔하게 정리
  • 신호 이름(refresh=sheet-list)을 문자열로 명시하기 때문에 의도가 명확
  • React Context나 전역 상태 없이 Next.js 내장 API만으로 해결

간단하지만 꽤 실용적인 패턴이다.

Top comments (0)