중복 fetch 해결하기
📌 문제 정의
최근 Inertia.js + React + Laravel 조합으로 프로젝트를 진행하던 중, Context 내부에서 fetch
가 두 번 이상 발생하는 문제를 겪었습니다.
- Context 내부에서 초기
fetch
요청을 보내고 있었고, 이 요청은 특정state
에 의존하고 있었습니다. - 그런데 페이지 이동 시 Context가 재마운트되며
initialState
가 다시 적용되었고, 결과적으로fetch
가 두 번 이상 실행되었습니다. - 이로 인해 드물게 초기값이 UI에 반영되는 문제, 혹은 깜빡임(flickering)이 발생했습니다.
⚙️ 문제 원인 분석
Inertia 환경에선 다음과 같은 제약이 있었습니다:
-
Context Provider를
<App />
위에 둘 수 없는 구조
→ Laravel에서 렌더링되는 Blade 파일이 있고, 그 안에서 React가 마운트되기 때문에 Context를 루트 최상단에서 감싸는 게 사실상 불가능했습니다. -
Inertia 페이지 이동 시 전체 컴포넌트가 재마운트됨
→ 라우터보다 하위에서 Provider가 선언돼 있으므로 페이지 이동마다 Context가 새롭게 초기화됨. -
useReducer
의initialState
는 첫 마운트 시 한 번만 적용됨
→ 하지만 페이지 전환마다 Context 자체가 재마운트되므로, 결과적으로initialState
가 자주 재적용되고fetch
가 반복됨.
🛠️ 해결 과정
시도 1. initialState
에 pageProps
를 활용
다행히 state
가 url
에 의존하고 있었기 때문에, 아래처럼 initialState
를 동적으로 설정해 문제를 우회할 수 있었습니다:
const pageProps = usePage();
const [state, dispatch] = useReducer(
adminReducer,
getInitialAdminState(pageProps), // 초기값을 props로부터 설정
);
하지만 이런 방법은 한계가 있습니다.
- 초기화 기준이 URL이나
props
처럼 외부로부터 주어지는 경우에만 동작합니다. - 사용자의 클릭이나 입력처럼 내부 상태 변화에 의존하는 경우엔 쓸 수 없습니다.
시도 2. 초기값을 null
로 하고, useEffect
에서 조건부 fetch
보다 범용적인 해결책으로 아래 방식을 채택했습니다:
- 상태를 최소한으로 정의 (
targetId: null
) targetId
가 설정되면 그때fetch
를 트리거- 원하는 컴포넌트에서 명시적으로
dispatch
를 통해targetId
설정
const [state, dispatch] = useReducer(reducer, {
targetId: null,
data: null,
loading: false,
});
useEffect(() => {
if (!state.targetId) return;
dispatch({ type: 'FETCH_START' });
fetchData(state.targetId)
.then((data) => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
.catch(() => dispatch({ type: 'FETCH_FAIL' }));
}, [state.targetId]);
컴포넌트에서는 다음과 같이 트리거합니다:
onClick={() => dispatch({ type: 'SET_ID', payload: 123 })}
✨ 배운 점 & 인사이트
-
Context는 위치가 전부다.
최상단에서 감싸지 못하는 구조라면,initialState
의 의존성만으로 비즈니스 로직을 구성하긴 어렵다. -
초기값은 최소화하고, side-effect로 동작을 분리하자.
useEffect
는 의존성 기반으로 fetch 동작을 분리할 수 있어 훨씬 유연하고 안정적이다. -
상태 기반 조건부 fetch는 깜빡임을 줄이고 예측 가능성을 높여준다.
특히 초기값이null
인 상태에서 fetch를 막고, 필요한 시점에만 명확히 트리거하는 방식은 유지보수성과 안정성 측면에서 매우 강력했다. -
Context로 모든 걸 해결하려 하지 말자.
팀 상황상 전역 상태 라이브러리를 쓰지 않기로 했지만, 이런 문제가 반복된다면Zustand
를 쓰는게 편한다!