문제 상황
Next.js App Router 기반 프로젝트에서 다음과 같은 구조가 있다고 가정해보자.
Layout
├── SheetNavigation (사이드바 - 시트 목록을 fetch하여 표시)
└── Page
└── SheetDetail (특정 시트 상세 - 시트 삭제 기능 포함)
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`)
}
})
}
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])
// ...
}
동작 흐름
[SheetDetail] 삭제 확인
│
▼
router.push('/dashboard?refresh=sheet-list')
│
▼
[SheetNavigation] searchParams 변경 감지
│
├─ refresh === 'sheet-list' ?
│ │
│ ▼
│ getSheetsList() 호출 → 목록 UI 갱신
│ │
│ ▼
│ router.replace('/dashboard') → URL 정리
│
└─ 아니면 무시
왜 router.replace로 파라미터를 지워야 할까?
1. /dashboard?refresh=sheet-list (push로 진입)
2. router.replace('/dashboard') (파라미터 제거)
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')
응용: 여러 신호를 하나의 파라미터로 관리
// 이동 측
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])
정리
- 구조 변경 없이 독립된 두 컴포넌트 간 상태 동기화 가능
-
useSearchParams와router.replace로 수신 후 URL을 깔끔하게 정리 - 신호 이름(
refresh=sheet-list)을 문자열로 명시하기 때문에 의도가 명확 - React Context나 전역 상태 없이 Next.js 내장 API만으로 해결
간단하지만 꽤 실용적인 패턴이다.
Top comments (0)