| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- CSS
- 리덕스
- 오픽표현
- Kotlin
- 공부
- 웹
- 영어표현
- react
- 리액트
- Next.js
- javascript
- merge
- 젠슨황
- Git
- async
- 오픽
- typescript
- 리사이클러뷰
- 알고리즘
- useState
- 리액트를 다루는 기술
- redux
- 프로그래머스
- pull
- 엔비디아
- 비동기처리
- list
- rebase
- useCallback
- await
- Today
- Total
공부블로그
Link로 이동했는데 이전 필터가 남아있다? - 리렌더와 리마운트의 차이, 그리고 React Query 캐시 전략 본문
Link로 이동했는데 이전 필터가 남아있다? - 리렌더와 리마운트의 차이, 그리고 React Query 캐시 전략
떠어영 2025. 8. 1. 13:57Next.js 기반 어드민 페이지를 개발하면서, 정산 실패한 경우 특정 리포트의 상세 페이지로 리다이렉트해야 하는 요구사항이 생겼다. 이건 searchParams로 파라미터 넘겨서 잘 처리했는데...
문제는 여기서부터였다.
❓문제 상황
- 정산 상세 페이지를 보고 있다가,
- 메뉴에서 동일한 상세 페이지 경로를 <Link>로 클릭해서 이동하면,
- 전체 상세가 아닌, 이전에 필터링된 결과가 그대로 남아 있는 것!
🧠 내가 처음에 했던 추측
“React Query가 쿼리 키를 업데이트하지 않아서 그런가?” → ❌ 쿼리 키는 잘 바뀌고 있음
“그럼 캐시가 남아있나?” → ❌ 서버 컴포넌트에서 prefetch도 잘 되고 있음
그런데도 여전히 이전 필터값으로 조회된 데이터가 유지되고 있었다.
🔍 디버깅 결과
- <Link> 클릭 → 서버 컴포넌트 재실행됨
- searchParams를 기반으로 prefetch 수행됨 (settleSummaryStatus 없음 = 필터 없음)
- 이 값들을 props로 클라이언트 컴포넌트(SettlementDetail)에 넘김
그런데도...
- SettlementDetail 내부 상태(queryParams)는 이전에 필터된 상태(settleSummaryStatus: FAILED) 유지
- 즉, React는 컴포넌트를 리렌더만 하고 리마운트하지 않음
- 그래서 useState()로 초기화한 queryParams는 한 번 세팅된 이후로 바뀌지 않음
✅ 문제 핵심 요약
- props는 바뀜 → 리렌더 발생
- 내부 useState는 그대로 → queryParams는 이전 필터 상태
- useGetSettlementDetail(queryParams) → 이전 필터 데이터 요청
🧩 이쯤에서 다시 보는: 리렌더 vs 리마운트
| 리렌더 (rerender) | 리마운트 (remount) | |
| 언제 발생? | props/state 변경 | key 변경되거나 컴포넌트가 트리에서 제거됐다가 다시 삽입될 때 |
| 내부 상태 (useState) | ✅ 유지됨 | ❌ 초기화됨 |
| useEffect(() => ..., []) | ❌ 실행 안됨 | ✅ 다시 실행됨 |
| React Query 훅 | 쿼리 키가 같으면 캐시 그대로 사용 | 훅은 재실행되지만, 쿼리 키가 같으면 캐시 사용됨 (옵션에 따라 서버 재요청 가능) |
🔍 진짜 원인은 이거였다
내가 실수한 건,
"props가 바뀌니까 리렌더 되면 초기화될 거라고 착각했다"는 점!
하지만 리렌더는 state를 유지하고, 리마운트만이 상태를 초기화한다.
그리고 React Query는 쿼리 키가 바뀌지 않으면 이전 캐시를 사용한다.
🛠 해결 방법
1️⃣ 컴포넌트에 key를 줘서 강제로 remount
<SettlementDetail key={query} filter={filter} ... />
이렇게 하면 컴포넌트가 완전히 새로 마운트되고, 내부 상태도 다시 초기화된다.
2️⃣ 상태를 props 기반으로 동기화 (useEffect)
useEffect(() => {
setQueryParams({
page: 0,
size: 20,
searchParams: {
fromSettleMonth: maxYM,
toSettleMonth: maxYM,
},
filters: filter ? { settleSummaryStatus: [filter] } : {},
});
}, [filter, maxYM]);
3️⃣ 상태 없이 useMemo로 항상 최신 값 계산
const queryParams = useMemo(() => ({
page: 0,
size: 20,
searchParams: {
fromSettleMonth: maxYM,
toSettleMonth: maxYM,
},
filters: filter ? { settleSummaryStatus: [filter] } : {},
}), [filter, maxYM]);
🚀 <Link>는 서버 컴포넌트를 거칠까?
이동 방식 서버 컴포넌트 실행 여부
| 브라우저 새로고침 / 직접 접근 | ✅ 항상 실행 |
| <Link href="..."> | ✅ 서버 컴포넌트 실행됨 (App Router 기준) |
| router.push() | ❌ 클라이언트 라우팅만 발생 (서버 안 거침) |
→ 나는 <Link>로 이동했기 때문에 서버 컴포넌트의 로그가 다시 찍혔고, 서버에서 props도 새로 내려왔던 것.
✅ 결론
- 리렌더와 리마운트의 차이,
- React Query의 캐시 전략,
- 초기 상태를 props에 의존할 때의 함정
이 세 가지가 모여서 아주 흔하지만 까다로운 버그를 만들었다.
초기 props 기반으로 useState() 세팅할 땐 반드시 리마운트 조건까지 고려하자.!!
🧑💻 나는 어떻게 해결했나?
나는 위에서 말한 1번 방식, key를 통해 컴포넌트를 강제로 remount하는 방법을 선택했다.
<SettlementDetail key={query} ... />
그럼에도 불구하고 React Query는 동일한 쿼리 키를 기준으로 캐시된 데이터를 그대로 사용할 수 있기 때문에, 성능상 손해도 거의 없고, UX도 자연스럽다.
즉,
“컴포넌트를 새로 마운트해서 상태 초기화는 하고, 쿼리 결과는 캐시된 걸 재사용”
이라는 구조를 만들 수 있었고, 내 케이스엔 이게 가장 깔끔하고 직관적인 해결책이었다.
'공부하기 > Trouble Shooting' 카테고리의 다른 글
| 프론트를 괴롭히는 CORS, 왜 일어나는걸까? (4) | 2025.08.04 |
|---|