리액트의 Context API를 제대로 알고 사용했는지 돌아보기 위해 Why React Context is Not a "State Management" Tool 을 읽고 정리한 글
Context vs Redux는 현재의 Context API가 릴리즈된 이래로 리액트 커뮤니티 내에서 가장 널리 논의되어온 주제 중 하나이다. 안타깝게도 대부분의 논의는 둘의 목적과 쓰임에 대한 혼동에서 비롯한다.
이 글을 통해 Context와 Redux가 정확히 무엇인지, 어떤 식으로 쓰이는지, 어떻게 다른지, 언제 써야하는지를 명확히 정리하고자 한다.
어떤 도구이든 정확하게 사용하려면 반드시 이해해야 할 것:
또한 현재 해결하고자 하는 문제가 무엇인지 정확히 파악한 뒤에 직면한 문제를 가장 잘 풀어낼 도구를 선택하면 된다. 이 때 그 도구를 선택한 이유는 누군가 써보라고 해서, 유명해서가 아니라 현재 주어진 상황에서 최선으로 작동하기 때문이어야 한다.
Context vs. Redux를 둘러싼 대부분의 오해는 이 도구들의 기능과 목적에 대한 이해 부족에서 비롯한다. 그러므로 언제 사용할지 판단하기 위해 우선 이 둘이 무엇을 하고, 무엇을 해결하는지부터 명확히 정의해보자.
현재 리액트 문서에 있는 Context에 대한 설명부터 살펴보자.
context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.
일반적인 React 애플리케이션에서 데이터는 위에서 아래로 (즉, 부모로부터 자식에게) props를 통해 전달되지만, 애플리케이션 안의 여러 컴포넌트들에 전해줘야 하는 props의 경우 (예를 들면 선호 로케일, UI 테마) 이 과정이 번거로울 수 있습니다. context를 이용하면, 트리 단계마다 명시적으로 props를 넘겨주지 않아도 많은 컴포넌트가 이러한 값을 공유하도록 할 수 있습니다.
관리에 대한 내용이 전혀 없으며, 값의 '전달', '공유'만을 언급한다.
현재의 React Context API(React.createContext()
)는 React 16.3 버전에서 처음 릴리즈 되었다. 이는 이전 버전의 리액트에서도 작동하였으나 큰 결함이 있던 기존의 Context API를 대체하는 것이었다. 기존 Context API의 주 문제점은 shouhlComponentUpdate()
를 통해 해당 컴포넌트의 렌더링이 생략될 경우, Context로 전달받은 값의 업데이트를 보장할 수 없다는 것이었다. 다수의 컴포넌트가 성능 최적화를 shouldComponentUpdate
에 기대고 있었으므로 기존의 Context는 데이터 전달 기능을 제대로 하지 못했다. 이 문제를 해결하기 위해 createContext()
가 만들어졌고, 중간에서 렌더링이 생략되더라도 하위 컴포넌트에서 모든 업데이트가 가능해졌다.
React Context를 사용하려면 다음 단계를 거친다:
const MyContext = React.createContext()
로 컨텍스트 객체 생성<MyContext.Provider value={someValue}>
를 렌더링한다. 데이터 한 덩어리를 컨텍스트에 넣어주는 것으로, string, a number, an object, an array, a class instance, an event emitter 등 모든 형태가 될 수 있다. const theContextValue = useContext(MyContext)
같은 형태로 이 데이터를 호출할 수 있다.상위 컴포넌트가 리렌더링 되면 context provider에 새로운 레퍼런스가 value
로 전달되고, context가 읽을 수 있는 모든 컴포넌트가 강제로 리렌더 된다.
보통 context value는 컴포넌트의 State로부터 온다 :
function ParentComponent() {
const [counter, setCounter] = useState(0);
// Create an object containing both the value and the setter
const contextValue = {counter, setCounter};
return (
<MyContext.Provider value={contextValue}>
<SomeChildComponent />
</MyContext.Provider>
)
}
이제 하위 컴포넌트에서 useContext
를 호출하여 이 값을 읽을 수 있다 :
function NestedChildComponent() {
const { counter, setCounter } = useContext(MyContext);
// do something with the counter value and setter
}
코드를 보면 Context가 그 어떤 것도 '관리'하지는 않음을 볼 수 있다. 컨텍스트는 파이프나 웜홀에 가깝다. <MyContext.Provider>
로 파이프의 최상단에 무언가를 넣으면, 그것이 useContext(MyContext)
를 외친 쪽으로 튀어나오는 것이다.
이런 개념을 의존성 주입(Dependenty Injection)이라고 한다. 하위 컴포넌트에서 특정 값을 필요로 할 때, 직접 생성 또는 마련하지 않고 런타임 시에 상위 컴포넌트의 어딘가로부터 값을 내려받는 것이다.
비교를 위해 Redux 공식 문서의 정의를 살펴본다.
Redux is a pattern and library for managing and updating application state, using events called "actions". It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.
Redux helps you manage "global" state - state that is needed across many parts of your application.
The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.
위 설명은
리덕스는 처음에 Flux Architecture 적용의 일환으로 만들어졌다. 이 패턴은 페이스북이 2014년 첫 제안하였으며 리액트의 근간이 되기도 한 아키텍처로, 이후 리액트 커뮤니티는 십여 개의 서로 다른 접근 방식을 가진 Flux 패턴 기반 라이브러리를 만들어냈다. 그리고 2015년, 최선의 디자인으로 당시의 여러 이슈에 대한 해결책이 되었으며 리액트와의 궁합도 훌륭한 리덕스가 출시되자마자 리덕스는 Flux 전쟁의 승자가 된다.
리덕스는 함수형 프로그래밍 원리를 사용하여 최대한 예측 가능한 코드로 리듀서 함수를 작성하도록 하며, '이벤트 정의' 로직과 '이벤트가 일어났을 때의 상태 변화 로직'을 분리하도록 한다. 리덕스 스토어의 기능을 확장하는 미들웨어를 사용하여 사이드 이펙트를 처리하기도 한다.
또한, Redux Devtools을 제공하므로 이를 통해 액션 히스토리와 스테이트 변화 과정을 앱에서 볼 수도 있다.
리덕스는 모든 UI layer(React, Vue, Angular, vanilla JS 등)와 함께 사용할 수 있다.
그러나 대부분의 경우 리액트와 함께 쓰인다. React-Redux 라이브러리는 공식 UI 바인딩 레이어로써 리액트 컴포넌트가 리덕스의 상태 값을 읽고 액션을 디스패치하는 방식을 통해 리덕스 스토어와 상호작용 할 수 있게 해준다. 그러므로 보통 '리덕스'를 사용한다고 말하면, Redux store
+ React-Redux library
를 함께 쓰는 것'을 뜻할 때가 많다.
React-Redux 라이브러리 덕분에 모든 리액트 컴포넌트는 리덕스 스토어와 소통할 수 있다. 이는 React-Redux 가 내부적으로 Context를 사용하기 때문에 가능한 것이다. 그러나 중요한 것은, React-Redux가 컨텍스트를 통해 전달하는 것은 현재의 상태 값(state value)이 아니라 리덕스 스토어 인스턴스(Redux store instance)라는 것이다. 이것이 바로 컨텍스트를 의존성 주입으로 사용하는 것의 예시이다. 우리는 리덕스와 연결된 리액트 컴포넌트들이 스토어의 문을 두드릴 것을 알지만, 컴포넌트를 정의할 때는 어떤 리덕스 스토어와 연결할지 신경쓰지 않고 알지도 못한다. 리덕스 스토어는 React-Redux의 <Provider>
를 통해 런타임 시에 트리에 주입될 뿐이다.
React-Redux가 내부적으로 Context를 사용한다는 이유 때문에 prop-drilling을 피하기 위한 목적으로 React-Redux를 사용하기도 한다. <MyContext.Provider>
안에 공공연하게 새로운 값을 넣어주는 대신, 리덕스 스토어에 넣고 어디에서든 접근하여 쓰는 것이다.
리덕스를 사용하는 주된 이유 중 하나가 공식 문서에 설명되어 있다:
The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.
prop-drilling을 피하기 위함도 그 외 다른 이유들 중 하나이다. 많은 이들이 이 이유로 초기에 리덕스를 선택했다. 리액트의 기존 Context가 제대로 작동하지 않았었기 때문이다.
상태(State)란 어플리케이션의 작동에 관여하는 모든 데이터를 말한다. 상태는 서버 상태(server state), 커뮤니케이션 상태(communication state), 로케이션 상태(location state)로 분류할 수 있다. 중요한 것은 데이터가 필요 시 저장되고(stored), 읽히고(read), 업데이트되고(updated), 사용되어야(used) 한다는 점이다.
그런 의미에서 상태 관리란:
리액트의 useState
와 useReducer
훅은 상태 관리의 좋은 예시가 된다. 이 두가지 훅을 통해
setState
나 dispatch
함수를 호출하여 값 업데이트이 맥락에서 Redux는 완전한 상태 관리 툴이다:
store.getState()
로 현재 값을 읽고, store.dispatch(action)
으로 값을 업데이트 할수 있게 하며 store.subscribe(listener)
를 통해 스토어의 업데이트를 알린다. 리액트 Context는 이 조건에 부합하지 않으므로 상태 관리 툴이 아니다. Context는 그 자체로는 아무것도 '저장'하지 않는다. 상위 컴포넌트가 <MyContext.Provider>
를 렌더하는 상위 컴포넌트는 Context에 어떤 값을 넣어줄지 결정하는 것에만 관여한다. 그리고 이 값은 보통 리액트 컴포넌트의 상태에 기반한다. 실질적인 '상태 관리'는 useState/useReducer
훅으로 이루어지는 것이다.
컨텍스트는 이미 어딘가에 존재하는 상태를 다른 컴포넌트와 공유하는 방법일 뿐이다.
Context와 React-Redux의 기능을 정리해보자:
Context
React+Redux
보다시피 이 둘은 기능적으로 매우 다른 툴이다. 하나의 공통점이 있다면 prop-drilling을 피하게 해준다는 것 뿐이다.
useReducer
Context vs Redux 논쟁에서의 유일한 문제점은, 사람들이 "useReducer
로 상태 관리를 하고, Context로 값을 전달해준다."고 말하는 대신 "나 Context 사용했어."라고 말한다는 것에 있다. 이것이 이 혼란의 공통적인 원인이며, 이 때문에 Context로 상태를 관리한다는 잘못된 이야기가 지속된다고 본다.
그렇다면, 이제 Context + useReducer
의 조합을 살펴보자. 이 조합은 Redux + React-Redux와 끔직히도 닮아있다. 두 조합 모두에는 아래의 공통점이 있다.
반면 큰 차이점도 있는데,
useReducer
는 리액트의 기능이므로 리액트에서만 사용할 수 있다. Redux 스토어는 모든 UI에 독립적이므로 별개로 사용 가능하다.useReducer
에는 미들웨어가 없다. useReducer
와 useEffect
를 함께 사용하여 사이드 이펙트를 낼 수 있고, 미들웨어와 비슷한 대안을 사용하려는 시도도 있어왔으나 기능과 효과면에서 리덕스 미들웨어에 비해서는 한계를 지닌다.스코프 문제와 불필요한 리렌더를 막기 위해 여러 개의 컨텍스트를 두고 같은 맥락을 공유하는 스테이트끼리 묶어 분리하는 방식을 추천하는 글도 여럿 볼 수 있다. 가능한 방법이긴 하나, 이는 React-Redux를 더 수준 낮게 재구현하는 방식과 다름없다.
얼핏 보기에는 Context + useReducer가 Redux + React-Redux와 거의 같아보일지 몰라도, 절대 동일하지 않고 기능적으로 리덕스를 대체할 수는 없는 것이다.
각각의 사용 예시를 다시 정리하면 이와 같다:
useReducer
useReducer
그래서 Context, Contexr + useReducer, Redux + React-Redux 중에 무엇을 사용할 것인가?
지금 해결할 문제가 무엇인지에 달렸다.
useReducer
조합을 사용하라.