
리덕스는 전역 상태 관리 라이브러리다. 상태를 저장하는 중앙 저장소가 있고 모든 컴포넌트가 이 저장소에서 상태를 사용할 수 있다. 중요한 점은 부모-자식 관계가 아니어도 되고, 자식 컴포넌트에서 만든 상태도 부모 컴포넌트에서 사용할 수 있다는 것이다.
애플리케이션의 크기가 커지면서 상태가 언제 어디서 업데이트 되는지 파악하기가 힘들어졌다. 수많은 상태들을 효과적으로 관리하기 위해 상태 관리만을 위한 기술이 필요했고, 그렇게 등장한 것이 Redux 되시겠다.
Flux는 리액트를 위한 단방향 데이터 바인딩을 기반으로 설계된 아키텍쳐(추상적인 구조, 방법론)다. Redux는 이 Flux를 실제로 구현한 구현체다. 이름부터가 Reducer + Flux = Redux 다.


Single Source of Truth : SSOT. 모든 상태는 하나의 Store에 저장된다. 상태의 출처는 언제나 하나! 데이터의 정확성, 일관성, 신뢰성을 보장해준다.
Read-Only State : 상태는 직접 변경이 불가능하고 Action객체를 이용한 제한적인 방법으로만 변경할 수 있다.
Reducer should be Pure Functions : 상태 업데이트는 순수 함수인 Reducer를 통해 이루어져야 한다.
Context API vs. Redux비슷한 기능을 가진 비슷한 Hook을 이미 다룬 적이 있다. Context API로 어느정도 데이터를 유하게 다룰 수 있는데, 왜 Redux가 필요할까?
Context API만으로는 제한이 생기기 때문에 상태가 복잡해질수록 Redux를 사용하는 것이 유리한 면이 있다. 무엇보다, Context API는 단방향 데이터 흐름을 기본으로 하기 때문에 전역 상태를 다루지 않는다.
리렌더링 회피
Context API는 Provider에서 제공한 상태가 변할 경우 모든 하위 컴포넌트가 리렌더링되어 이를 최적화하기 위해서 많은 노력이 필요하지만, 리덕스는 상태 변경 시 관련된 컴포넌트만 선택적으로 업데이트가 가능해서 성능 최적화가 용이하다.
상태 로직의 중앙화
리덕스는 상태들을 하나의 저장소(store)에 저장한다. 상태 로직이 중앙에서 관리되어 일관성과 신뢰성을 보장한다. Context API는 저장소의 개념이 아니다. 특정 데이터를 Context를 통해 하위 컴포넌트로 전달하는 방식이다.
다양한 미들웨어
리덕스는 다양한 미들웨어를 지원하여 비동기 작업, 로깅, 상태 변경에 대한 추가 처리 등 복잡한 기능들을 구현할 수 있다.
Context API는 "부모 컴포넌트의 상태를 하위 컴포넌트에 쉽게 전달하는 방법"이고, Redux는 "앱 전체에서 상태를 관리하는 글로벌 저장소"다.
Q. 리렌더링 때문에라도 리덕스를 사용하는 것이 Context API를 사용하는 것 보다 이득 아닌가요? 기능도 훨씬 다양하잖아요!
A. 그렇진 않다. 단순한 애플리케이션에 리덕스를 사용하면 오히려 역효과가 날 수 있다. 리덕스는 기본적으로 꽤 복잡한 구조를 가지고 있다. 기본만으로 꽤 코드량이 많기 때문에 불필요한 오버헤드가 생길 수 있다. 다만 상태 관리는 성능의 관점보다는 어떤 방법을 사용하는 것이 애플리케이션에 어울릴까를 생각하며 선택하는 것이 좋다
RootReducer 정의// reducers/index.js
import { combineReducers } from "redux";
import todos from "./todos";
const rootReducer = combineReducers({ // reducer들을 하나의 객체로 묶는다.
todos,
});
export default rootReducer;
Reducer 정의Reducer는 상태를 변화시키는 함수로서, 순수 함수(항상 같은 결과를 가지고 외부 상태를 변화시키지 않는 함수)여야 한다.
휴먼 에러 방지를 위해 Action Creator를 사용한다.
Action Creator의 인자로 payload를 전달할 수 있다.
// reducers/todos.js
export const ADD_TODO = "TODO/ADD_TODO";
export const REMOVE_TODO = "TODO/REMOVE_TODO";
export const TOGGLE_TODO = "TODO/TOGGLE_TODO";
// Action Creator
// dispatch로 전달할 액션 객체를 반환하는 함수. payload가 들어갈 수 있다.
export const addTodo = (text) => ({ type: ADD_TODO, text });
export const removeTodo = (id) => ({ type: REMOVE_TODO, id });
export const toggleTodo = (id) => ({ type: TOGGLE_TODO, id });
// 초기 상태값 설정. 아래 Reducer를 이용해 만들 store에 들어갈 초기값이다.
// 꼭 객체일 필요는 없다.
const initialState = {
todos: [],
};
// Reducer(상태 변화를 일으키는 순수 함수)
const todos = (state = initialState, action) => {
switch (action.type) { // action.type은 어떤 기능을 사용해야 하는지 알려준다
case ADD_TODO: // 기능에 따라 상태가 어떻게 변할지 달라진다.
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.text, completed: false },
],
};
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
};
default:
return state;
}
};
export default todos;
Store 만들기// store/index.js
import { createStore } from "redux";
import rootReducer from "../reducers";
const store = createStore( // 인자로 rootReducer를 전달
rootReducer
);
export default store;
App.jsx에 Store 넣기// index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
ReactDOM.render(
<Provider store={store}> // 만들었던 store를 Provider 속성으로 넣으면
<App /> // 태그로 감싸진 컴포넌트는 store를 사용할 수 있다.
</Provider>,
document.getElementById("root")
);
Redux 사용하기useSelector를 통해 원하는 reducer를 선택할 수 있다. useDispatch를 통해 action 객체를 reducer로 보낼 수 있다.dispatch를 이용하여 reducer로 action 객체를 전달한다.// components/TodoList.js
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { addTodo, removeTodo, toggleTodo } from "../reducers/todos";
const TodoList = () => {
const [input, setInput] = useState("");
// state 인자는 combineReducers에 넣었던 reducers를 말한다.
// 정확히는 key가 reducers고 value가 해당 reducer가 관리하는 상태인 객체다.
// state.todos는 여러 reducer 중 todos reducer가 관리하는 상태를 말하고,
// state.todos.todos는 todos reducer가 관리하는 상태의 todos 속성을 말한다.
const todos = useSelector((state) => state.todos.todos);
// dispatch는 객체를 전달한다. 이 객체에는 type과 type의 이름이 들어가야 한다.
const dispatch = useDispatch();
const handleAddTodo = () => {
if (input.trim()) {
dispatch(addTodo(input));
setInput("");
}
};
const handleRemoveTodo = (id) => {
dispatch(removeTodo(id));
};
const handleToggleTodo = (id) => {
dispatch(toggleTodo(id));
};
return (
<div>
<h1>Todo List</h1>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a new todo"
/>
<button onClick={handleAddTodo}>Add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id} style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
<span onClick={() => handleToggleTodo(todo.id)}>{todo.text}</span>
<button onClick={() => handleRemoveTodo(todo.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
};
export default TodoList;
type이란 key를 가진 객체로, reducer로 보낼 명령같은 것. 상태를 이렇게 바꿔줘~Redux는 편리하지만 최대 단점이 있는데, 바로 사용하기 위해 작성해야하는 코드가 너무 많다는 것! 실제로 이 문제 때문에 점유율도 줄고 다른 상태 관리 도구들이 생겨났었다. 그러나 RTK가 나오면서 다시 Redux로 모여들게 되었다.
Redux Toolkit이란 현재 Redux 로직을 작성할 때 권장되는 방법으로, Redux의 복잡성을 줄이고 개발 효율성을 높이기 위해 만들어진 일종의 문법이다. 기존의 Redux를 개량했다고 생각하면 된다. RTK를 통해 개발자는 불필요한 보일러플레이트(비슷한 로직이지만 이름만 다른, 반복적으로 사용해야 하는 코드들) 코드를 줄이고 간결하게 작성할 수 있다.
configureStore
Redux Store를 간단하게 설정하는 함수로 여러 reducer를 결합하고, 미들웨어와 개발자도구를 합칠 수 있게 해준다. 기존의 createStore보다 간편하다.
createSlice
Redux에서 state 슬라이스를 관리하는 함수. 상태와 관련된 reducer, Action creator를 한 곳에서 정의할 수 있고, 불변성을 유지하며 상태를 업데이트할 수 있도록 Immer 라이브러리를 내부적으로 사용한다. Immer 라이브러리는 객체를 깊은 복사해주는 라이브러리로, 불변성을 지켜주는 라이브러리다.
그래서 RTK는 개발 시간 절약, 유지보수 용이, Redux의 장점 유지, 불변성 유지라는 장점을 지닌다.
// features/todos/todosSlice.js
import { createSlice } from "@reduxjs/toolkit";
// createSlice를 사용해 Slice 생성
// name, initialState, reducres 3가지 속성을 가진다.
const todosSlice = createSlice({
name: "todos",
initialState: {
todos: [],
},
reducers: {
addTodo: (state, action) => {
state.todos.push({ // state를 직접 바꾸듯이 코드 작성 가능
id: crypto.randomUUID(), // Immer 라이브러리가 내부적으로 불변성을 보장하기 때문
text: action.payload, // 이런 식으로 payload를 명시하면
completed: false, // 알아서 payload가 포함된 Action Creator가 생성됨
});
},
removeTodo: (state, action) => {
state.todos = state.todos.filter((todo) => todo.id !== action.payload);
},
toggleTodo: (state, action) => {
const todo = state.todos.find((todo) => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
},
});
// actions 속성을 통해 Action Type, Action Creator 자동 생성
// 개발자가 명시한 속성을 이용하여 payload도 알아서 추가한다.
export const { addTodo, removeTodo, toggleTodo } = todosSlice.actions;
// reducer 속성을 통해 reducer 자동 생성
export default todosSlice.reducer;
참고로 prepare 속성을 이용하면 payload를 직접 커스터마이징할 수도 있다.
다만 prepare 함수를 추가하면 기존의 리듀서 함수가 객체 형태로 변형된다.
prepare 함수가 먼저 실행되어 payload를 원하는 형태로 가공한 후, reducer에서 그 값을 받아 상태를 변경하게 된다.
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
incrementByAmount: {
reducer: (state, action) => {
state.value += action.payload.amount; // 변경된 payload 구조 사용
},
prepare: (amount) => {
return { payload: { amount, timestamp: Date.now() } }; // payload를 직접 변경
},
},
},
});
// 커스텀 payload가 적용된 액션 객체
// prepare(10)가 먼저 실행되어 amount 값으로 10을 넣어
// payload: { amount: 10, timestamp: 1700000000000 } 반환
console.log(counterSlice.actions.incrementByAmount(10));
// 출력 결과:
// { type: "counter/incrementByAmount", payload: { amount: 10, timestamp: 1700000000000 } }
// prepare가 없었다면 { type: "counter/incrementByAmount", payload: 10 }이 되었을 것이다.
store 설정하기// store/index.js
import { configureStore } from "@reduxjs/toolkit";
import todosReducer from "../features/todos/todosSlice";
// configureStore의 reducer 속성에 내보냈던 리듀서를 넣는다.
const store = configureStore({
reducer: {
todos: todosReducer,
},
});
export default store;
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { addTodo, removeTodo, toggleTodo } from "../features/todos/todosSlice";
// 똑같은데, 경로가 다르기 때문에 이것만 주의하면 된다.
...
Ducks 패턴은 Redux 앱을 구성할 때 사용하는 패러다임 중 하나로, 일반적으로 분산되어 있던 Action type, Action Creator, Reducer를 하나의 파일로 구성하는 방식이다. Redux 관련 코드의 관리를 보다 간결하고 모듈화하여 관리할 수 있도록 돕는다.
아래 내용을 모두 지켜 모듈을 작성하면 된다.
Reducer 함수를 export default 한다.
Action creator 함수들을 export 한다.
Action type은 app/reducer/ACTION_TYPE 형태로 작성한다.
(외부 라이브러리로서 사용될 경우 또는 외부 라이브러리가 필요로 할 경우에는 UPPER_SNAKE_CASE 로만 작성해도 괜찮다.)