이 문서는 React 애플리케이션의 상태를 전역으로 관리하기 위한 라이브러리인 Redux와, Redux를 더 쉽고 간편하게 사용하도록 돕는 Redux Toolkit에 대한 기본적인 개념과 차이점을 설명합니다.
createSlice와 configureStore를 사용한 간결한 상태 관리React의 기본 상태 관리는 단일 컴포넌트 또는 부모-자식 관계에 최적화되어 있습니다. 하지만 애플리케이션이 복잡해지면 여러 컴포넌트가 동일한 상태를 공유해야 하는 경우가 발생하며, 이때 지역 상태 관리만으로는 한계에 부딪힙니다.
최상위 컴포넌트 App의 user 상태를 가장 깊은 하위 컴포넌트 Greeting까지 전달하기 위해 HomePage, Profile 컴포넌트가 중간 다리 역할을 해야만 합니다.
// 최상위 컴포넌트(App)의 상태를 아주 깊은 하위 컴포넌트(Greeting)로 전달해야 하는 경우
function App() {
const [user, setUser] = useState({ name: "홍길동", isLoggedIn: true });
return <HomePage user={user} />;
}
function HomePage({ user }) {
// 이 컴포넌트는 user가 필요 없지만, 하위로 전달하기 위해 받아야 함
return <Profile user={user} />;
}
function Profile({ user }) {
// 이 컴포넌트도 user가 필요 없지만, 하위로 전달하기 위해 받아야 함
return <Greeting user={user} />;
}
function Greeting({ user }) {
return <div>{user.name}님, 환영합니다!</div>;
}
Redux와 같은 전역 상태 관리 라이브러리는 이러한 문제를 해결하기 위해 등장했습니다. 컴포넌트 계층 구조와 상관없이 중앙 집중화된 저장소(Store)를 통해 애플리케이션의 모든 상태를 한 곳에서 관리합니다.
Redux는 다음 세 가지 주요 구성 요소로 이루어져 있습니다.
상태에 어떤 변화가 필요한지 설명하는 정보 객체입니다. 액션은 반드시 어떤 종류의 액션인지를 나타내는 type 속성을 가져야 합니다.
type (필수): 액션의 종류를 나타내는 문자열 (예: "INCREMENT").payload (선택): 상태 변경에 필요한 추가 데이터를 담습니다.// 액션 생성자(Action Creator): 액션 객체를 만드는 함수
export const increment = () => ({
type: "INCREMENT",
});
export const incrementByAmount = (amount) => ({
type: "INCREMENT_BY_AMOUNT",
payload: amount,
});
현재 상태와 전달받은 액션을 사용하여 새로운 상태를 만드는 순수 함수입니다. 리듀서는 절대 기존 상태를 직접 수정해서는 안 되며, 반드시 새로운 상태 객체를 반환해야 합니다 (불변성 원칙).
// 리듀서 함수 정의
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "INCREMENT_BY_AMOUNT":
return { ...state, count: state.count + action.payload };
case "RESET":
return { ...state, count: 0 };
default:
return state;
}
};
export default counterReducer;
애플리케이션의 전역 상태를 담고 있는 단 하나의 저장소입니다. 스토어는 리듀서를 받아 생성되며, 상태를 읽고 변경하는 여러 내장 함수를 제공합니다.
import { createStore } from "redux";
import counterReducer from "./reducer";
// createStore 함수로 스토어 생성
const store = createStore(counterReducer);
export default store;
컴포넌트에서 스토어에 있는 상태를 변경하기 위해 액션을 발생시키는 함수입니다. dispatch(action) 형태로 호출하면 스토어는 리듀서를 실행하여 새로운 상태를 생성합니다.
import { useDispatch } from "react-redux";
function Counter() {
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button>
<button onClick={() => dispatch({ type: "INCREMENT_BY_AMOUNT", payload: 10 })}>+10</button>
</div>
);
}
Redux는 강력하지만, 액션 타입, 액션 생성자, 리듀서를 모두 별도로 작성해야 하는 등 보일러플레이트 코드가 많다는 단점이 있습니다. Redux Toolkit(RTK)은 이러한 단점을 개선하여 Redux를 더 쉽고 효율적으로 사용할 수 있도록 Redux 팀이 공식적으로 만든 라이브러리입니다.
createSlice리듀서와 액션을 한 번에 생성하는 강력한 함수입니다. name, initialState, reducers 객체를 받아 액션 생성자와 리듀서를 자동으로 만들어줍니다.
💡 Immer 라이브러리 내장
createSlice의 리듀서 내부에서는 Immer 라이브러리가 동작하여,state.value += 1처럼 상태를 직접 수정하는 것처럼 보이는 코드를 작성해도 자동으로 불변성을 유지해줍니다. 개발자는 더 이상...state와 같은 전개 연산자를 사용할 필요가 없습니다.
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Immer가 불변성을 유지하며 상태를 업데이트
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
},
},
});
// 액션 생성자 자동 생성 및 내보내기
export const { increment, incrementByAmount, reset } = counterSlice.actions;
// 리듀서 내보내기
export default counterSlice.reducer;
configureStorecreateStore를 대체하는 함수로, 여러 슬라이스의 리듀서를 쉽게 병합하고 유용한 미들웨어를 기본적으로 추가해줍니다.
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
// configureStore 함수로 스토어 생성
export const store = configureStore({
reducer: {
// 여기에 여러 리듀서를 추가하여 병합할 수 있음
counter: counterReducer,
},
});