Skip to main content

React에서 Context API, Redux, Recoil 분해해보기

Context APIReduxRecoil
값을 전달하는 원리컴포넌트 트리를 따라서 데이터 전달
값을 관리하는 원리설명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;