이전에 Recoil 관련 강의를 듣다가 Context API는 상태 관리 도구라고 볼 수 없다고 했다. 이에 궁금한 점이 생겨 정리해보고자 한다.
공식문서에서 React Context란?
- 모든 컴포넌트에서 수동으로 props를 전달할 필요 없이 구성 요소 트리를 이용해 데이터를 전달한다.
- 트리의 모든 수준을 통해 명시적으로 props를 전달하지 않고도 구성 요소 간에 이와 같은 값을 공유하는 방법을 제공한다.
- 공식문서에서는 값을 "관리"하는 것에 대해서는 아무것도 언급하지 않고 값을 "전달" 및 "공유" 하는 것만을 나타낸다.
Context 사용하기
- 먼저
createContext함수를 사용하여 **UserContext**라는 새로운 **Context**객체를 생성한다. UserStore컴포넌트는 **props.children**을 포함하는UserContext.Provider컴포넌트를 반환한다.- **
UserContext.Provider**는 UserContext 객체를 이용해valueprop을 전달한다.
import { createContext, useState } from "react";
export const UserContext = createContext(null);
const UserStore = (props) => {
const [userId, setUserId] = useState("");
const [password, setPassword] = useState("");
return(
<UserContext.Provider value={{
userId,
setUserId,
password,
setPassword
}}>
{props.children}
</UserContext.Provider>
);
};
export default UserStore;
- 전역으로 상태 값을 가져오기 위해 **
App.js**를 수정한다. - **
UserStore**를 먼저 렌더링하고, 그 안에 **Router**와 **Routes**를 중첩하여 페이지 컴포넌트를 렌더링하게 한다.
import './App.css';
import Login from './Login';
import Signup from './pages/Signup';
import Home from './pages/Home';
import { BrowserRouter as Router, Route, Routes} from "react-router-dom";
import UserStore from "./context/UserStore";
function App() {
return (
<UserStore>
<Router>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/Signup" element={<Signup />} />
<Route path="/Home" element={<Home />} />
</Routes>
</Router>
</UserStore>
);
}
export default App;
- 이제 컴포넌트에서 **
UserContext**를 사용해 전역 상태 변수인 **username**과 **email**이 업데이트가 가능하다.
import {UserContext} from "../context/UserStore";
import React, {useState, useEffect, useContext} from 'react';
const context = useContext(UserContext);
const {setUserId, setPassword} = context;
setUserId(inputId);
setPassword(inputPw);
- 코드를 보면 Context가 어떤 것도 '관리' 하고 있지 않다.
- **
<UserContext.Provider>**로 파이프의 최상단에 무언가를 넣는다면 **userContext**를 외친 쪽으로 튀어나온다. - 이것은 상태 관리가 아닌 의존성 주입(Dependenty Injection)이라고 하며 하위 컴포넌트에서 값을 필요로 할 때, 런타임 시에 상위 컴포넌트의 어딘가로부터 값을 내려받는 것이다.
- 따라서 Context를 사용하는 주요 목적은 "prop-drilling"을 피하는 것이다.
Redux란?
- "action"이란 이벤트를 사용해 애플리케이션 상태를 관리하고 업데이트하기 위한 패턴 및 라이브러리
- "전역"상태를 관리하는데 도움이 되며 전체 애플리케이션에서 사용해야 하는 상태에 대한 중앙 집중 저장소 역할을 한다.
- 제공하는 패턴과 도구를 사용하면 상태가 언제, 어디서, 왜, 어떻게 업데이트 되고 변경이 발생할 때 로직이 어떻게 작동하는지 더 쉽게 이해할수 있다.
- 공식문서에서는 Redux가 구체적으로 "상태 관리"를 나타낸다
- Redux의 목적은 시간이 지남에 따라 상태가 어떻게 변하는지 이해하도록 돕는 것이라고 한다.
Redux와 React
- 리덕스는 모든 UI layer(React, Vue, Angular, vaninlla JS)와 함께 사용 가능
- 하지만 대부분 React와 함께 쓰기 때문에 '리덕스'를 사용한다고 하면 Redux store + React-Redux library를 함께 쓰는 것을 말할 때가 많다.
- React-Redux는 애플리 케이션의 모든 React 요소들이 Redux 저장소에 접근이 가능한데 이는 React-Redux 내부에서 Context를 사용하기 때문이다.
- 여기서 주의할 점은 React-Redux는 현재의 상태값을 Context를 통해 전달하는게 아니라 Redux 저장소 인스턴스만 전달한다는 점이다.

- 이것은 Context를 위에서 설명한 종속성 주입을 위해 사용하는 좋은 예이다.
- 이 덕분에 React-Redux는 내부적으로 Context를 사용하기 때문에 prop-drilling을 피하기 위해 사용될 수도 있다.
Redux를 사용하는 이유
- UI레이어와 완전히 별개로 상태 관리 로직을 작성하고 싶은 경우
- 다양한 UI레이어(ex. AngularJS에서 React로 마이그레이션되는 애플리케이션)간에 상태 관리 로직 공유
- 작업이 전달될 때 Redux 미들웨어의 기능을 사용하여 추가 논리 추가
- Redux 상태의 일부를 유지할 수 있음
- 개발자가 재생할 수 있는 버그 보고서 활성화
- 개발 중에 로직과 UI를 더 빠르게 디버깅
Context가 "상태 관리"가 아닌 이유
- 상태 관리의 정의는 "시간이 지남에 따라 상태가 어떻게 변하는 가"이다.
- "상태 관리"는 다음과 같은 방법을 갖는 것을 의미한다.
- 초기값을 저장한다.
- 현재 값을 읽어온다.
- 값을 업데이트 한다.
- useState와 useRedux Hooks은 상태관리의 좋은 예이다.
- Hook을 호출해 초기값을 저장한다.
- Hook을 호출해 현재 값을 읽는다.
- 제공된 setState 또는 dispatch 함수를 호출해 값을 업데이트 한다.
- 구성요소가 다시 렌더링되었기 때문에 값이 업데이트 된 것을 알 수 있다.
- Redux와 MobX 또한 분명한 상태 관리이다.
- Redux는 root Reducer를 호출하여 초기 값을 저장하고 store.getState()을 사용하여 현재 값을 읽을 수 있다.
- 또한 store.dispatch(action)를 사용하여 값을 업데이트하고 store.subscribe(listener)을 사용하여 저장소가 업데이트 되었음을 리스너에게 알린다.
- MobX는 스토어 클래스에 필드 값을 할당하여 초기 값을 저장하고, 스토어의 필드에 액세스하여 현재 값을 읽을 수 있게하며 해당 필드에 할당하여 값을 업데이트하고 변경사항이 발생했음을 알린다.
- 하지만 Context는 그 자체로는 아무것도 "저장"하지 않는다! 렌더링하는 상위 구성 요소는
<MyContext.Provider>Context에 전달되는 값을 결정하는 역할을 하며 해당 값은 일반적으로 React 구성 요소 상태를 기반으로 한다. - 실제 "상태 관리"는 useState/useReducer Hook을 통해 이루어진다.
Context와 Redux 비교
- Context
- 아무것도 저장하거나 "관리"하지 않는다.
- React 컴포넌트에서만 작동 한다.
- 타입에 관계없는 단일 값을 전달한다.(원시, 객체, 클래스 등)
- 단일 값을 읽을 수 있도록 한다.
- prop-drilling을 피하기 위해 사용할 수 있다.
- React DevTools에서 Provider, Consumer가 있는 컴포넌트의 현재 값을 볼 수 있으나 상태 업데이트 히스토리는 보지 못한다.
- Context가 전달하는 값이 업데이트 되면 Consumer 컴포넌트도 자동으로 업데이트 되며 이를 스킵할 방법은 없다.
- 사이드 이펙트를 처리하는 매커니즘이 전무하며, 오로지 컴포넌트 렌더링에만 관여한다.
- React-Redux
- 단일 값을 저장하고 관리한다.(원칙적으로 객체이다.)
- 리액트 컴포넌트가 아닌 다른 UI와 함께 사용할 수 있다.
- 단일 값을 읽을 수 있도록 한다.
- prop-drilling을 피하기 위해 사용할 수 잇다.
- action을 dispatching하고 reducer를 작동시킴으로써 값을 업데이트 한다.
- DevTool에서 dispatch된 모든 action과 state 변화를 시간 순으로 볼 수 있다.
- 사이트 이펙트를 발생시키기 위한 미들웨어를 사용할 수 있다.
- 컴포넌트가 store 업데이트를 구독하고, store 상태의 특정한 일부를 추출하고 해당 값이 바뀌었을 때만 컴포넌트가 리렌더되도록 할 수 있다.
Context와 useReducer
- Context vs Redux 논쟁에서의 문제점은 사람들이 "useReducer로 상태관리를 하고 Context로 값을 전달해준다"라고 말하는 것과 달리 그저 "나 Context 사용했어"라고만 말하기 때문에 Context로 상태를 관리한다는 잘못된 이야기가 지속된다고 본다.
- Context + useReducer의 조합은 Redux + React-Redux와 닮아있다.
- 두 조합은 아래의 공통점이 있다.
- 저장되는 값
- Reducer 함수
- action, dispatch
- 중첩 컴포넌트 내에서 값을 전달하고 읽는 기능
- 반면 큰 차이점도 있다.
- Context + useReducer는 리액트의 기능이므로 리액트에서만 사용 가능하지만 Redux 스토어는 모든 UI에 독립적으로 사용 가능하다.
- React DevTools을 사용하면 현재 Context 값을 볼 순 있지만 시간 순으로 지난 기록을 볼 순 없다.
- Redux DevTools로는 디스패치된 모든 액션, 모든 액션의 컨텐츠, 액션 실행 후의 상태, 시간에 따른 상태의 차이를 볼 수 있다.
- useReducer에는 미들웨어가 없다. useReducer와 useEffect를 함께 사용해 사이드 이펙트를 낼 수 있고 미들웨어와 비슷한 대안을 사용하려는 시도도 있었으나 기능과 효과면에서 리덕스 미들웨어에 비해서는 한계가 있다.
- 얼핏 보기에는 비슷해보이나 절대 동일하지 않고 기능적으로 리덕스를 대체할 수는 없을 것이다.
올바른 도구 선택
- Context
- prop-drilling 없이 중첩 컴포넌트에 값을 전해준다.
- useReducer
- reducer 함수를 사용하여 보통 수준의 복잡도를 지닌 리액트 컴포넌트의 상태를 관리한다.
- Context + useReducer
- reducer 함수를 사용하여 보통 수준의 복잡도를 지닌 리액트 컴포넌트의 상태를 관리하고 prop-drilling 없이 중첩 컴포넌트에 값을 전달한다.
- Redux
- reducer 함수를 사용하여 중간 또는 높은 수준의 복잡도를 지닌 리액트 컴포넌트 상태를 관리한다.
- 언제, 왜, 어떻게 상태가 변해왔는지 시간 순으로 추적한다.
- UI 레이어와 상태 관리 로직을 완벽히 분리한다.
- 서로 다른 UI 레이어 간 상태 관리 로직을 공유한다.
- 강력한 Redux 미들웨어 기능으로 액션 디스패치 시 로직을 추가한다.
- Redux 상태에 항시적으로 접근된 상태이다.
- 개발자가 버그 리포트를 재현할 수 있다.
- 개발 시 로직과 UI를 빠르게 디버깅 할 수 있다.
- Redux + React-Redux
- 리액트 컴포넌트와 리덕스 스토어를 연결하여 모든 리덕스의 유즈 케이스를 구현할 수 있다.
결론
- 단순히 prop-drilling을 피하고 싶다면 Context를 사용하자
- 컴포넌트 복잡도가 보통이거나 외부 라이브러리를 사용하고 싶지 않다면 Context + useReducer 조합을 사용하자
- 상태 변화 추적이 필요할 때, 상태 변화 시 특정 컴포넌트만 리렌더 하고 싶을 때 사이드 이펙트를 강력하게 컨트롤하고 싶을 때는 Redux + React-Redux를 사용하자
마치며
Context가 왜 상태관리가 될 수 없었는지 궁금했는데 지금 보니 Context가 값을 저장하지 못한다는 것을 깨닫게 되었다. 또 이에 대해 공부하면서 어떤 애플리케이션에 상태 관리 툴을 적절하게 사용해야 할지 추가적으로 공부하게 되었다. 앞으로 애플리케이션을 개발하면서 상태 관리를 정하는 데에 있어서 큰 도움이 될 것 같다.
Reference
Context API vs Redux - Medium Blogged Answers: Why React Context is Not a "State Management" Tool (and Why It Doesn't Replace Redux)