React에서 Context API, Redux, Recoil 분해해보기
Context API | Redux | Recoil | ||
---|---|---|---|---|
값을 전달하는 원리 | 컴포넌트 트리를 따라서 데이터 전달 | |||
값을 관리하는 원리 | 설명 | Context 객체의 데이터가 변경되면, Context Provider 내부의 모든 컴포넌트들이 재렌더링(Context를 구독하지 않는 컴포넌트들도 재렌더링) | 중앙 데이터 저장소에서 값을 관리하다가 store의 상태가 변경되면, redux의 해당 데이터를 구독하고 있는 컴포넌트들만 다시 재렌더링 | atom 단위로 상태값 관리(의존성을 가질 수 있고, 이를 통해 상태값이 변경되면 의존하는 다른 atom이 자동으로 업데이트) |
atom은 RecoilRoot 컴포넌트 내에서 정의됨 | ||||
상태를 트리로 관리 | ||||
단일 상태 트리(Single State Tree)=> 컴포넌트 간 의존성 그래프=> 이 그래프를 통해 업데이트할 컴포넌트가 결정됨 | Directed Acyclic Graph(DAG)라는 의존성 그래프 사용=> atom은 서로 의존성 가질 수 있어서 이 관계를 DAG로 표현=> atom의 의존성관계가 변경되면 필요한 atom들만 다시 렌더링됨 | |||
장점 | 리덕스의 Provider는 React 컴포넌트에서 Redux store에 접근할 수 있게 해줄 뿐Redux는 상태 변경 시 해당하는 구독자(Subscriber)에게만 변경 사항을 전달해주기 때문에,구독하지 않은 컴포넌트는 변경 사항에 반응하지 않습니다.따라서 해당 코드에서도 Example2는 구독하지 않고 있으므로, count 값 변경 시 재렌더링 되지 않습니다. | atom과 selector 개념 사용=> atom은 상태(고유식별자 갖고있음)=> selector는 atom을 기반으로 값 처리를 담당상태 변경 시, recoil은 atom을 업데이트하고, 이를 의존하는 selector를 다시계산=> 이 때 atom, selector가 서로 연결돼있는 의존성 그래프를 형성해서, 이 그래프 기반으로 의존하는 컴포넌트들을 자동으로 업데이트 | ||
selector로 상태변화의 의존성을 추적해서 => 필요한 경우에만 재렌더링 | ||||
단점 | provider가 재공하는 context값이 변경될 때마다 전체 컴포넌트 트리가 재렌더링됨(React의 내부의 최적화방식) | 상태 트리가 크거나 복잡한 경우, 의존성 그래프가 복잡해질 수 있어서 디버깅 더려워짐 | ||
같은 Provider를 사용하는 경우리액트는 가상돔을 이용해서 변경사항을 비교하고, 필요할 때만 실제 돔에 업데이트를 적용=> 변경사항을 감지할 수 있는 방법이 필요한데, Context를 구족하지 않았는데, 해당 컴포넌트가 속한 컴포넌트 트리 중 구독하는 컴포넌트가 있다면, React Provider의 context 값이 변경될때마다 컴포넌트 트리가 변경됐는지 판단해야함=> 따라서 모든 컴포넌트 재렌더링(구독이란 useContext를 사용해서 값을 가져오는 것) | 꽤 최신의 라이브러리로, 생태계가 작음 | |||
다른 provider를 사용하거나 provider의 내부컴포넌트가 아닐 경우, 구독을 하지 않을 때에도 재렌더링이 일어나지 않음 | ||||
Context API
구독을 하고 있지 않지만 다른 컴포넌트의 값변경으로 인해 재렌더링이 일어나는 경우
import React, { useContext, useState } from "react";
// AppContext 생성const AppContext = React.createContext({} as any);
// Provider 컴포넌트 생성const AppProvider = ({ children }) => {
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
};
// Provider를 통해 값 전달return <AppContext.Provider value={{ count, incrementCount }}>{children}</AppContext.Provider>;
};
// 사용자 정의 컴포넌트 1
const Example1 = () => {
// useContext로 Provider로부터 값을 가져옴
const { count, incrementCount } = useContext(AppContext);
return (
<div style={{ padding: "8px", border: "1px solid red" }}>
<h2>Example 1</h2>
<p>Count: {count}</p>
<button onClick={incrementCount}>Increment Count</button>
</div>
);
};
// 사용자 정의 컴포넌트 2
const Example2 = () => {
// useContext로 Provider로부터 값을 가져옴
// const { count } = useContext(AppContext);
return (
<div style={{ padding: "8px", border: "1px solid blue", marginTop: "16px" }}>
<h2>Example 2</h2>
{/* <p>Count: {count}</p> */}
</div>
);
};
// App 컴포넌트
const App = () => {
// Provider를 이용해 Example1에게 값 전달
return (
<AppProvider>
<Example1 />
<Example2 />
</AppProvider>
);
};
export default App;
구독을 하고 있지 않지만 다른 컴포넌트의 값변경으로 인해 재렌더링이 일어나지 않는 경우
import React, { useContext, useState } from "react";
// AppContext 생성const AppContext = React.createContext({} as any);
// Provider 컴포넌트 생성const AppProvider = ({ children }) => {
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
};
// Provider를 통해 값 전달return <AppContext.Provider value={{ count, incrementCount }}>{children}</AppContext.Provider>;
};
// 사용자 정의 컴포넌트 1
const Example1 = () => {
// useContext로 Provider로부터 값을 가져옴
const { count, incrementCount } = useContext(AppContext);
return (
<div style={{ padding: "8px", border: "1px solid red" }}>
<h2>Example 1</h2>
<p>Count: {count}</p>
<button onClick={incrementCount}>Increment Count</button>
</div>
);
};
// 사용자 정의 컴포넌트 2
const Example2 = () => {
// useContext로 Provider로부터 값을 가져옴
// const { count } = useContext(AppContext);
return (
<div style={{ padding: "8px", border: "1px solid blue", marginTop: "16px" }}>
<h2>Example 2</h2>
{/* <p>Count: {count}</p> */}
</div>
);
};
// App 컴포넌트
const App = () => {
// Provider를 이용해 Example1에게 값 전달
return (
<>
<AppProvider>
<Example1 />
</AppProvider>
<Example2 />
);
};
export default App;
Redux
리덕스의 구독방식
import React from "react";
import { createStore } from "redux";
import { Provider, useDispatch, useSelector } from "react-redux";
// Redux Store 생성const initialState = { count: 0 };
const incrementCountAction = { type: "INCREMENT_COUNT" };
function reducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT_COUNT":
return { ...state, count: state.count + 1 };
default:
return state;
}
}
const store = createStore(reducer);
// 사용자 정의 컴포넌트 1const Example1 = () => {
const dispatch = useDispatch();
const count = useSelector((state) => state.count);
const incrementCount = () => {
dispatch(incrementCountAction);
};
return (
<div style={{ padding: "8px", border: "1px solid red" }}>
<h2>Example 1</h2>
<p>Count: {count}</p>
<button onClick={incrementCount}>Increment Count</button>
</div>
);
};
// 사용자 정의 컴포넌트 2const Example2 = () => {
const count = useSelector((state) => state.count);
return (
<div style={{ padding: "8px", border: "1px solid blue", marginTop: "16px" }}>
<h2>Example 2</h2>
<p>Count: {count}</p>
</div>
);
};
// App 컴포넌트const App = () => {
// Redux Store를 이용해 Example1과 Example2에게 값 전달return (
<Provider store={store}>
<Example1 />
<Example2 />
</Provider>
);
};
export default App;
Context를 컴포넌트 내에서 구독하는 방법
import React, { useContext } from "react";
import MyContext from "./MyContext";
function ChildComponent() {
const valueFromContext = useContext(MyContext);
return <div>{valueFromContext}</div>;
}
Recoil
countState의 값이 변경되면, selector인 countPlusOneState도 변경됨
하지만 이전 값을 읽어서 1을 더하는 연산을 수행하기 때문에, 값의 의존성 추적가능
따라서 countPlusOne 값이 필요한 경우에만 리렌더링
import React from "react";
import { atom, selector, useRecoilState, useRecoilValue } from "recoil";
// atom 생성const countState = atom({
key: "countState",
default: 0,
});
// selector 생성const countPlusOneState = selector({
key: "countPlusOneState",
get: ({ get }) => get(countState) + 1,
});
const Counter = () => {
const [count, setCount] = useRecoilState(countState);
const countPlusOne = useRecoilValue(countPlusOneState);
const handleClick = () => {
setCount((prevCount) => prevCount + 1);
};
return (
<div>
<h1>Count: {count}</h1>
<h2>Count Plus One: {countPlusOne}</h2>
<button onClick={handleClick}>Increment Count</button>
</div>
);
};
export default Counter;