일반적으로 전역 상태 관리를 위해서는 Redux와 같은 전역 상태 관리 라이브러리를 사용한다.
이때 Context API를 왜 사용하지 말라는 부분을 정리된 글을 많이 봤다.
하지만 이것을 직접 테스트하지 않았기 때문에 이번에 비교를 해보려고 한다.
( 머리로만 이해하는 것과 눈과 손을 함께 사용하는 것 중 후자가 더 좋을 테니 ㅎㅎ )
작업
Redux
사용하는 패키지는
react-redux, @reduxjs/toolkit
간단한 테스트를 하기 때문에 기본적인 패키지만 사용했다.
// /reducer/counter.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
value: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.value++;
},
decrement(state) {
state.value--;
},
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
간단한 테스트를 위해서 Counter Reducer를 만들었다.
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../reducer/counter";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
만들어둔 Reducer를 Store에 설정했다.
import React from "react";
import styled from "styled-components";
import Control from "./ChildComponentOne/Control";
import NotThing from "./ChildComponentOne/NotThing";
import View from "./ChildComponentOne/View";
const ChildComponentOne = () => {
return (
<ChildWrapper>
<Control />
<View />
<NotThing />
</ChildWrapper>
);
};
const ChildWrapper = styled.div`
width: 45%;
padding: 10px;
`;
export default ChildComponentOne;
ChildComponentOne 컴포넌트에서 Redux를 컨트롤하는 Control과 State값을 보이기 위한
View 그리고 리렌더링 테스트를 위한 NotThing 컴포넌트를 만들었다.
// Control.jsx
import { useDispatch } from "react-redux";
import { decrement, increment } from "../../../reducer/counter";
const Control = () => {
const dispatch = useDispatch();
const handleIncrement = () => {
dispatch(increment());
};
const handleDecrement = () => {
dispatch(decrement());
};
return (
<div>
<button onClick={handleIncrement}>+</button>
<button onClick={handleDecrement}>-</button>
</div>
);
};
export default Control;
Redux의 State 값을 Action을 통해서 변경해주는 작업을 한다.
// View.jsx
import { useSelector } from "react-redux";
const View = () => {
const { value } = useSelector((state) => state.counter);
return <div>ChildComponentOne Value : {value}</div>;
};
export default View;
Store에서 counter의 State 값을 가지고 와서 화면에 보여준다.
// NotThing.jsx
const NotThing = () => {
return <div>저는 아무것도 아닙니다. 단지 렌더링이 되는지 확인용일뿐...</div>;
};
export default NotThing;
리렌더링 테스트를 위한 의미없는 컴포넌트이다 ㅎㅎ..
Context API
import { useState } from "react";
import { createContext } from "react";
import styled from "styled-components";
import Control from "./ChildComponentTwo/Control";
import NotThing from "./ChildComponentTwo/NotThing";
import View from "./ChildComponentTwo/View";
export const CounterContext = createContext(null);
const CounterContextProvider = ({ children }) => {
const counterState = useState(0);
return (
<CounterContext.Provider value={counterState}>
{children}
</CounterContext.Provider>
);
};
const ChildComponentTwo = () => {
return (
<CounterContextProvider>
<ChildWrapper>
<Control />
<View />
<NotThing />
</ChildWrapper>
</CounterContextProvider>
);
};
const ChildWrapper = styled.div`
width: 45%;
padding: 10px;
`;
export default ChildComponentTwo;
createContext를 사용해서 저장소를 만들고 Provider를 따로 만들었다.
이때 value로 useState를 사용해서 만들어서 Context의 value에 넣어주었다.
Control, View, NotThing의 역할은 Redux와 동일하다.
// Control.jsx
import { useContext } from "react";
import { CounterContext } from "../ChildComponentTwo";
const Control = () => {
const [_, setValue] = useContext(CounterContext);
const handleIncrement = () => {
setValue((prev) => prev + 1);
};
const handleDecrement = () => {
setValue((prev) => prev - 1);
};
return (
<div>
<button onClick={handleIncrement}>+</button>
<button onClick={handleDecrement}>-</button>
</div>
);
};
export default Control;
useContext를 사용해서 setValue를 받아와서 함수를 만들었다.
// View.jsx
import { useContext } from "react";
import { CounterContext } from "../ChildComponentTwo";
const View = () => {
const [value, _] = useContext(CounterContext);
return <div>ChildComponentTwo Value : {value}</div>;
};
export default View;
마찬가지로 useContext에서 value를 가지고 와서 화면에 렌더링한다.
NotThing 컴포넌트는 동일하다.
비교
Redux를 사용한 ChildComponentOne의 경우 실제 State값에 의존성이 있는
View 컴포넌트만 리렌더링이 발생하고 있다.
Context API를 사용한 ChildComponentTwo의 경우 useContext를 사용한 모든 컴포넌트가
다시 리렌더링이 발생한다.
const CounterContextProvider = ({ children }) => {
const counterState = useState(0);
return (
<CounterContext.Provider value={counterState}>
{children}
</CounterContext.Provider>
);
};
이런 결과가 나오는 이유는 ContextAPI를 사용하는 컴포넌트인 ChildComponentTwo에서
ContextProvider에 setState를 사용했기 때문에 counterState 자체가 변화가 발생하면서
counterState를 value로 가지고 있는 CounterContext.Provider가 업데이트 된다.
결론
Redux를 사용할 경우 State의 변경이 일어나면 State를 사용하고 있는 컴포넌트에서만
리렌더링이 발생하지만
Context API를 사용할 경우 Provider를 선언한 부모 컴포넌트에서부터 useContext를 사용한
모든 자식 컴포넌트가 리렌더링이 발생한다.
이부분은 해결하는 방법은 추후에 다시 작성하겠다.
이런 전역적으로 리렌더링이 발생하는 부분 외에도 Redux가 보일러플레이트가 크거나
러닝 커브가 있더라도 다양한 미들웨어를 함께 사용하는 것으로 이점이 있다고 생각한다.
또 굳이 Redux가 아닌 Recoil, MobX와 같은 상태 관리 라이브러리가 있다.
그러므로 전역 상태 관리를 위해서는 Context API를 사용하기보단 Redux같은
전역 상태 관리 라이브러리를 사용하는 것이 좋다.
'React > 실험실' 카테고리의 다른 글
[React] Recoil - 시작하기 (0) | 2022.11.09 |
---|---|
[React] Context API 사용하기 (0) | 2022.11.05 |
[React] 제어 컴포넌트 vs 비제어 컴포넌트 (0) | 2022.11.03 |
[React] useMemo vs useRef (0) | 2022.11.02 |
[React] useCallback의 남용 (0) | 2022.10.31 |