
Context API 하나에 다 때려 넣었다가 지옥을 봤습니다
전역 상태 관리를 위해 Redux 대신 Context API를 선택했습니다. 하지만 `UserContext`에 모든 정보를 담자마자 앱 전체가 리렌더링되기 시작했습니다. Context 분리(Splitting) 전략.

전역 상태 관리를 위해 Redux 대신 Context API를 선택했습니다. 하지만 `UserContext`에 모든 정보를 담자마자 앱 전체가 리렌더링되기 시작했습니다. Context 분리(Splitting) 전략.
매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

느리다고 느껴서 감으로 최적화했는데 오히려 더 느려졌다. 프로파일러로 병목을 정확히 찾는 법을 배운 이야기.

엄청난 데이터를 아주 적은 메모리로 검사하는 방법. 100% 정확도를 포기하고 99.9%의 효율을 얻는 확률적 자료구조의 세계. 비트코인 지갑과 스팸 필터는 왜 이것을 쓸까요?

텍스트에서 바이너리로(HTTP/2), TCP에서 UDP로(HTTP/3). 한 줄로서기 대신 병렬처리 가능해진 웹의 진화. 구글이 주도한 QUIC 프로토콜 이야기.

프로젝트 초기에 Redux의 보일러플레이트가 싫어서 React 내장 Context API를 쓰기로 했습니다.
AppStateContext 하나를 만들고, 거기에 유저 정보, 테마, 모달 상태, 알림 리스트까지 다 넣었습니다.
const AppStateContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [modal, setModal] = useState(false);
// 😱 최악의 코드: 모든 상태를 하나의 객체로 묶음
const value = { user, setUser, theme, setTheme, modal, setModal };
return <AppStateContext.Provider value={value}>{children}</AppStateContext.Provider>;
}
편했습니다. useContext(AppStateContext)만 하면 어디서든 데이터를 꺼낼 수 있었으니까요.
그런데 앱이 커지자 문제가 터졌습니다.
"다크 모드로 전환했는데(setTheme), 왜 회원가입 폼 입력한 게 날아가죠?"
"모달 하나 띄웠는데 왜 전체 페이지가 깜빡거리죠?"
저는 Context가 "필요한 데이터만 구독(Subscribe)한다"고 착각했습니다.
const { theme } = useContext(AppState)를 하면, theme이 바뀔 때만 리렌더링 될 줄 알았습니다.
하지만 React Context의 메커니즘은 단순 무식합니다. "Provider의 value가 바뀌면, 이걸 구독하는 모든 컴포넌트를 강제 리렌더링 한다."
제가 value 객체를 매번 새로 만들어서({...}) 넘겨줬기 때문에,
theme 하나만 바뀌어도 value 객체의 참조값(Reference)이 바뀝니다.
그러면 value를 구독하던 LoginForm, Header, Sidebar는 "어? 데이터가 바뀌었네?" 하고 전부 리렌더링 됩니다.
이걸 "마을 안내 방송"에 비유하니 이해가 됐습니다.
하나의 Context에 모든 걸 담는 건, "모든 잡다한 소식을 스피커 하나로 24시간 떠드는 것"과 같습니다.
Context API 최적화의 핵심은 "관심사의 분리"입니다. 서로 관련 없는 상태는 다른 Context에 담아야 합니다.
가장 흔한 패턴은 "값(Value)"과 "함수(Setter)"를 분리하는 겁니다.
값은 자주 바뀌지만, 함수(setUser)는 컴포넌트 생명주기 내내 안 바뀝니다.
export const UserStateContext = createContext(null);
export const UserDispatchContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
// 값이 바뀌면 여기만 리렌더
<UserStateContext.Provider value={user}>
{/* 함수는 안 바뀌므로 여기는 안전 */}
<UserDispatchContext.Provider value={setUser}>
{children}
</UserDispatchContext.Provider>
</UserStateContext.Provider>
);
}
이제 setUser만 필요한 하위 컴포넌트(로그인 버튼 등)는 UserDispatchContext만 구독합니다.
그러면 user 상태가 바뀌어도(로그인 성공), 버튼 컴포넌트는 리렌더링 되지 않습니다!
ThemeContext, ModalContext, UserContext...
귀찮더라도 파일을 나누세요.
ThemeContext: 라이트/다크 모드 (전체 영향)FormContext: 특정 페이지 안에서만 쓰이는 상태 (지역적)"Context가 너무 많아지면 Provider Hell(지옥)이 생기지 않나요?" 네, 보기 흉합니다. 하지만 성능을 위해서라면 이게 맞습니다.
Context를 직접 쓰는 것보다, Custom Hook을 만들어서 내부를 캡슐화하는 게 좋습니다.
// Hooks/useUser.ts
export function useUser() {
const state = useContext(UserStateContext);
if (!state) throw new Error('Cannot find UserProvider');
return state;
}
export function useUserDispatch() {
const dispatch = useContext(UserDispatchContext);
if (!dispatch) throw new Error('Cannot find UserProvider');
return dispatch;
}
이렇게 하면 컴포넌트에서는 useContext를 몰라도 됩니다.
또한, 나중에 Context 대신 Redux나 Recoil로 교체하더라도, 컴포넌트 코드는 수정할 필요가 없습니다. hook 내부만 바꾸면 되니까요.
Context가 분리되어 있어도, Provider의 부모가 리렌더링 되면 하위 컴포넌트들도 리렌더링 될 수 있습니다.
이땐 React.memo로 컴포넌트를 감싸야 합니다.
"Context 업데이트로 인한 리렌더링"은 React.memo를 뚫고 들어오지만,
"부모 컴포넌트의 리렌더링"은 React.memo가 막아줍니다.
Context API:
Zustand / Recoil / Redux:
useStore(state => state.bears)). 내가 원하는 데이터가 바뀔 때만 리렌더링 됨.입력 폼이나 실시간 차트 데이터를 Context에 넣는 건 자살행위입니다. 그땐 무조건 Zustand나 Recoil을 쓰세요.
이 문제는 제가 채팅 앱을 만들 때 극적으로 드러났습니다.
ChatContext에 messages(채팅 목록)와 typingUsers(입력 중인 사람)를 같이 넣었습니다.
누군가 타이핑을 칠 때마다 typingUsers가 바뀝니다. (1초에 5번)
그때마다 ChatContext를 구독하는 메시지 리스트 전체(수천 개)가 리렌더링 되었습니다.
결과적으로 타이핑을 할 때마다 화면이 버벅거리는 끔찍한 렉이 발생했습니다.
useChatStore(state => state.typingUsers)로 컴포넌트를 쪼갰습니다.TypingContext로 분리했습니다.이후 타이핑 렉이 완전히 사라졌습니다. 교훈: "자주 바뀌는 값"과 "안 바뀌는 값"을 절대 섞지 마세요.
"어디서 리렌더링이 일어나는지 눈으로 보고 싶어요." Chrome 확장 프로그램 React DevTools의 Profiler 탭을 켜세요.
그러면 어떤 컴포넌트가 렌더링 되었는지, "Why did this render?" (이유: Hook 1 changed)까지 다 알려줍니다. 범인을 잡는 최고의 도구입니다.
useMemo는 값을, useCallback은 함수를 저장.Q: 한 컴포넌트에서 여러 Context를 써도 되나요?
A: 네. UserContext와 ThemeContext를 동시에 구독해도 됩니다. 둘 중 하나라도 바뀌면 리렌더링 됩니다.
Q: 모든 걸 useMemo로 감싸야 하나요?
A: Provider 내부의 value는 무조건 감싸야 합니다. 하위 컴포넌트(Consumer)에서는 연산이 무거운 경우에만 감싸면 됩니다.
Q: Context는 느린가요?
A: Context 자체는 빠릅니다. 진짜 문제는 불필요한 리렌더링입니다. 최상위(App.js)에 Context를 두고 자주 업데이트하면, 앱 전체가 계속 다시 그려지므로 느려집니다.
Q: useReducer는요?
A: useReducer + Context는 Redux의 훌륭한 대체재입니다. 하지만 이것도 상태 분리를 안 하면 리렌더링 문제는 똑같이 발생합니다.
Context는 의존성 주입(DI)을 위한 도구지, 고성능 상태 관리 도구가 아닙니다.