📗

2.2 Redux 동작 원리

2.2.1 Redux 사용 환경 세팅하기

앞서 1장에서 React의 Context를 활용해 만든 계산기와 투두리스트 프로젝트를 Redux를 활용하여 구현해 보겠습니다. 먼저 새로운 리액트 프로젝트를 설정하고 준비해 보겠습니다.
 
임의의 이름으로 React 프로젝트 생성 및 의존성 설치를 진행합니다.
npx create-react-app calc-todo
생성한 프로젝트 디렉터리로 이동 후, Redux를 사용하기 위한 패키지를 설치합니다.
cd calc-todo npm install redux react-redux
 
Redux를 사용한 프로젝트 개발은 Action(액션)을 정의하여 Reducer(리듀서)를 작성하고, 리듀서를 통해 Store(스토어)를 만든 후 React 컴포넌트들에 연결하여 상태를 사용하고 업데이트하는 과정을 거치게 됩니다. 이러한 여러 요소를 작성하기 위해서는 먼저 파일 구조를 선택해야 합니다.
 
일반적인 파일 구조로는 액션, 리듀서, 스토어, 컴포넌트를 '역할'에 따라 분리하는 구조입니다.
notion imagenotion image
이 구조는 역할별로 모듈화가 되어 있어 분리는 편리하지만, 새로운 액션을 하나 만들 때마다 최소 세 개의 파일을 수정해야하고 프로젝트의 규모가 커진다면 한 기능을 수정할 때마다 각각의 경로에 있는 여러 개의 파일에 매번 접근해야 하는 것이 비효율적입니다.
 
위와 같은 방법 대신 '기능(도메인)'별로 분리는 하되, 한 파일에 액션 타입과 액션 생성 함수, 리듀서 함수를 몰아서 작성하는 방식이 있습니다. 이는 Ducks 패턴이라고 부르기도 합니다.
notion imagenotion image
이 구조는 첫 번째 방식에서의 비효율성 문제를 해결해 주어 현재 많은 개발자들이 선호하는 방식으로, 기능의 변경이나 추가가 한 파일에 대한 수정만으로 가능하므로 유지보수가 수월합니다.
 
위의 두 가지 패턴이 가장 많이 사용되는 방식이긴 하지만, 이 외에도 본인만의 방식으로 구조를 설정해도 무방합니다. 이번 장에서는 두 번째 Ducks 패턴을 사용해 구현해 보겠습니다.
 

2.2.2 Components 생성

기능 없이 계산기와 투두리스트의 틀만 잡아주는 컴포넌트를 생성하고 App.js에서 렌더링을 해줍니다.

1) Calculator 컴포넌트 만들기

// components/Calculator.jsx import { useState } from "react"; export default function Calculator() { const [inputValue, setInputValue] = useState(""); return ( <> <div>결과: </div> <input type="number" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <div> <button onClick={() => {}}>+</button> <button onClick={() => {}}>-</button> <button onClick={() => {}}>/</button> <button onClick={() => {}}>*</button> <button onClick={() => {}}>C</button> </div> <br /> </> ); }
 

2) TodoList 컴포넌트 만들기

// components/TodoList.jsx import { useState } from "react"; export default function TodoList() { const [inputValue, setInputValue] = useState(""); const todos = []; return ( <> <form onSubmit={(e) => e.preventDefault()}> <input type="text" value={inputValue} placeholder="할 일을 작성해 주세요" onChange={(e) => setInputValue(e.target.value)} /> <button type="submit" onClick={() => {}}> 추가 </button> </form> {todos.map((todo) => ( <section key={todo.id}> <input type="checkbox" onClick={() => {}} /> <div style={{ textDecoration: todo.isDone ? "line-through" : "none", }} > {todo.content} </div> <button onClick={() => {}}>❌</button> </section> ))} </> ); }
 

3) App에서 렌더링하기

// App.js import Calculator from "./components/Calculator"; import TodoList from "./components/TodoList"; function App() { return ( <div> <Calculator /> <TodoList /> </div> ); } export default App;
여기까지 진행하면 다음과 같은 화면이 렌더링 됩니다.
notion imagenotion image
 

2.2.3 Action

1) calculator 액션 정의하기

이제 애플리케이션에 어떤 일이 일어날지를 정의하는 액션을 만듭니다. 계산기 예제에서는 '덧셈, 뺄셈, 곱셈, 나눗셈, 초기화'와 같이 5개의 액션을 정의할 것이고, 투두리스트 예제에서는 '할일 추가, 할일 완료, 할 일 체크(토글)'와 같이 3개의 액션을 정의할 것입니다.
 
액션의 타입은 변화를 식별하기 위해 사용되는 문자열로 보통 대문자와 언더스코어(_)를 통해 정의합니다. 문자열상수 = '도메인/액션'의 형식으로 도메인은 액션이 포함된 범주의 모듈을 가리킵니다. 도메인을 명시해 주는 이유는 애플리케이션 내 수많은 액션을 명확히 구분 지어 충돌을 방지하기 위함이며, 이는 어떠한 기능을 위한 액션인지 직관적으로 파악이 가능합니다.
// modules/calculator.js // 1. Action type 정의 const ADD_NUMBER = "calculator/ADD_NUMBER"; const SUBTRACT_NUMBER = "calculator/SUBTRACT_NUMBER"; const MULTIPLY_NUMBER = "calculator/MULTIPLY_NUMBER"; const DIVIDE_NUMBER = "calculator/DIVIDE_NUMBER"; const RESET_NUMBER = "calculator/RESET_NUMBER";
 
액션 타입을 정의했다면, 액션에 필요한 값을 받아 이를 객체로 만들어 반환해 주는 액션 생성 함수를 정의해야 합니다. 우리는 일반적으로 액션 생성 함수를 사용해 매번 액션 객체를 직접 작성할 필요가 없습니다. 액션 객체는 일반적으로 { type: '액션_타입_상수', payload: '액션에 필요한 데이터' } 이러한 구조로 되어 있습니다. 또한 액션 생성 함수는 렌더링 되는 컴포넌트 내에서 사용되기 때문에 export를 해줘야 합니다.
// 2. Action 생성 함수 정의 export const onAddNumber = (number) => ({ type: ADD_NUMBER, payload: number }); export const onSubtractNumber = (number) => ({ type: SUBTRACT_NUMBER, payload: number, }); export const onMultiplyNumber = (number) => ({ type: MULTIPLY_NUMBER, payload: number, }); export const onDivideNumber = (number) => ({ type: DIVIDE_NUMBER, payload: number, }); export const onResetNumber = () => ({ type: RESET_NUMBER });
이처럼 액션 생성함수는 해당 함수가 필요한 컴포넌트에서 호출되어 액션을 생성하고, Redux 스토어에 디스패치(dispatch)하는 데 사용됩니다.
 

2) todoList 액션 정의하기

위와 같은 과정을 따라 todoList 모듈도 만들어 봅시다.
// modules/todoList.js // 1. Action type 정의 const ADD_TODO = "todoList/ADD_TODO"; const DELETE_TODO = "todoList/DELETE_TODO"; const TOGGLE_TODO = "todolist/TOGGLE_TODO"; // 2. Action 생성 함수 정의 export const onAddTodo = (inputValue) => ({ type: ADD_TODO, payload: inputValue, }); export const onDeleteTodo = (id) => ({ type: DELETE_TODO, payload: id }); export const onToggleTodo = (id) => ({ type: TOGGLE_TODO, payload: id });
이것으로 액션 및 액션 생성 함수를 정의하는 과정이 완료되었습니다. 이제 액션을 받아 상태를 변경해 주는 Reducer 함수를 만들어 보겠습니다.

2.2.4 Reducer

앞선 Context 챕터에서는 계산기의 기능을 추가하기 위해 createContext() 를 사용해서 초기값과 함수를 정의한 후, Consumer 또는 useContext() 를 통해 상태값을 사용했습니다.
 
Redux 라이브러리도 상태값을 저장하는 저장소를 만들고 필요한 컴포넌트로 값을 꺼내 사용하는 큰 흐름은 같습니다. 하지만 상태값을 저장하고 불러오는 함수들의 명칭이 달라집니다. Redux에서는 createContext() 대신 Reducer라는 함수를 사용합니다.
 
Reducer에는 위에서 정의해 준 action과 state라는 개념이 사용됩니다. state는 공통으로 사용할 상태 값을 나타내며, action은 state를 변경하기 위한 정보를 담은 객체입니다. Reducer 내에서 state는 초기값을 부여한 후 action type에 따라 업데이트됩니다. action내에는 type이라는 값이 필수 값으로 요구되며, 이 값에 따라 사용할 액션 생성 함수를 구분해 줍니다. 두 번째 파라미터인 payload에는 함수에 전달할 데이터가 들어갑니다.
 
Redux에서는 state의 불변성을 유지해야 합니다. 그 이유는 Redux가 상태관리를 할 때, 상태의 변경을 추적하고 예측 가능한 방식으로 관리하기 위해 이전 상태와 현재 상태의 차이를 비교하기 때문입니다. 이러한 이유로 Redux에서 새로운 상태를 반환할 때는 기존 상태를 변경하지 않고 복사하며, 변경이 필요한 부분만 업데이트하여 새 값을 반환하는 방법을 사용합니다. 객체나 배열 등의 값을 반환시에는 스프레드 연산자를 사용하여 기존 상태에 새로운 값을 덧붙여 새로운 객체나 배열로 이뤄진 상태를 반환합니다.
 

1) calculator의 Reducer 함수 만들기

state 값의 초기 값을 initialState 값으로 초기화 해주고, action 객체의 type에 따라 state와 payload 값을 이용해 계산해주는 Reducer 함수를 구현해보겠습니다.
// modules/calculator.js // 1. Action type 정의 const ADD_NUMBER = "calculator/ADD_NUMBER"; const SUBTRACT_NUMBER = "calculator/SUBTRACT_NUMBER"; const MULTIPLY_NUMBER = "calculator/MULTIPLY_NUMBER"; const DIVIDE_NUMBER = "calculator/DIVIDE_NUMBER"; const RESET_NUMBER = "calculator/RESET_NUMBER"; // 2. Action 생성 함수 정의 export const onAddNumber = (number) => ({ type: ADD_NUMBER, payload: number }); export const onSubtractNumber = (number) => ({ type: SUBTRACT_NUMBER, payload: number }); export const onMultiplyNumber = (number) => ({ type: MULTIPLY_NUMBER, payload: number }); export const onDivideNumber = (number) => ({ type: DIVIDE_NUMBER, payload: number }); export const onResetNumber = () => ({ type: RESET_NUMBER }); // 3. state의 초기값 설정 const initialState = 0; // 4. Reducer 함수 작성 // Action 객체와 현재 state를 받아서 다음 상태를 계산해줍니다. function calculatorReducer(state = initialState, action) { // 특정 액션 유형을 처리하고 해당 액션에 따라 state를 업데이트하고 반환합니다. switch (action.type) { case ADD_NUMBER: return (state = state + Number(action.payload)); case SUBTRACT_NUMBER: return (state = state - Number(action.payload)); case MULTIPLY_NUMBER: return (state = state * Number(action.payload)); case DIVIDE_NUMBER: return (state = state / Number(action.payload)); case RESET_NUMBER: return (state = 0); // 특정 액션 유형이 아닌 경우 기존 state 값을 반환합니다. default: return state; } } export default calculatorReducer;
 

2) todoList의 Reducer 함수 만들기

위와 동일한 작업을 todoList에 대해서도 진행하겠습니다.
// modules/todoList.js // 1. Action type 정의 const ADD_TODO = "todoList/ADD_TODO"; const DELETE_TODO = "todoList/DELETE_TODO"; const TOGGLE_TODO = "todolist/TOGGLE_TODO"; // 2. Action 생성 함수 정의 export const onAddTodo = (inputValue) => ({ type: ADD_TODO, payload: inputValue }); export const onDeleteTodo = (id) => ({ type: DELETE_TODO, payload: id }); export const onToggleTodo = (id) => ({ type: TOGGLE_TODO, payload: id }); // 3. state의 초기값 설정 const initialState = { todos: [] }; // 4. Reducer 함수 작성 // Action 객체와 현재 state를 받아서 다음 상태를 변경해줍니다. function todoListReducer(state = initialState, action) { // 특정 액션 유형을 처리하고 해당 액션에 따라 state를 업데이트하고 반환합니다. switch (action.type) { case ADD_TODO: const content = action.payload; const newTodo = { id: state.todos.length, content, isDone: false, }; /** state를 변경할 때는 그 자체를 수정하는 것이 아닌 스프레드 연산자(...)를 사용해 기존 state를 복사해서 내용을 추가한 뒤 새로운 배열로 반환합니다. **/ return { todos: [...state.todos, newTodo] }; case DELETE_TODO: return { todos: state.todos.filter((todo) => todo.id !== action.payload) }; case TOGGLE_TODO: return { todos: state.todos.map((todo) => todo.id === action.payload ? { ...todo, isDone: !todo.isDone } : todo ), }; // 특정 액션 유형이 아닌 경우 기존 state 값을 반환합니다. default: return state; } } export default todoListReducer;
위 코드에서 보이는 것과 같이 Reducer 함수에서 새로운 state 값을 반환할 때는 불변성을 유지해야 하기 때문에 기존의 state를 변경하는 것이 아닌 새로운 값 혹은 배열, 객체 등을 만들어 반환해야 합니다.
 

3) rootReducer에서 합치기

Redux의 createStore 함수를 사용하여 스토어를 생성할 때, 스토어는 기본적으로 하나의 루트 리듀서(Root Reducer)를 가져야 합니다. 하나의 루트 리듀서는 모든 상태 및 액션에 대한 처리를 담당하며, 이 루트 리듀서가 여러 개의 하위 리듀서들을 결합하고 관리합니다.
 
따라서 여러 개의 하위 리듀서를 루트 리듀서로 결합해줘야 하는데 combineReducers 함수를 사용하면 이 작업이 가능합니다. combineReducers를 사용하면 여러 리듀서를 하나의 루트 리듀서로 결합할 수 있으며, 각 하위 리듀서는 애플리케이션의 다른 부분에 대한 상태를 관리합니다. 이 방법을 사용하면 리듀서를 모듈화하고 관리하기 쉬우며, 상태의 일부를 개별적으로 처리할 수 있습니다.
 
위에서 작성해준 calculatorReducer와 todoListReducer를 하나의 rootReducer로 합쳐보겠습니다. rootReudcer는 위 두 개의 리듀서가 위치한 modules 폴더의 index.js라는 파일에 작성하겠습니다.
// modules/index.js import { combineReducers } from "redux"; // 만들어 놓은 Reducer 함수들을 import 해줍니다. import calculatorReducer from "./calculator"; import todoListReducer from "./todoList"; // combineReducers 함수는 여러 개의 리듀서들을 하나의 객체로 묶어 인자로 받습니다. const rootReducer = combineReducers({ calculatorReducer, todoListReducer, // 새로운 Reducer를 만든다면 아래에 추가해줍니다. }); export default rootReducer;
 

2.2.5 Store

Redux의 store는 앱 전체 상태를 저장하고 관리하는 객체입니다. Redux에서는 일반적으로 하나의 애플리케이션에 하나의 스토어를 사용하는 것이 권장됩니다. Redux의 아키텍처는 하나의 중앙 스토어를 사용하여 애플리케이션의 전역 상태를 관리하도록 설계되었기 때문입니다.
 
store 객체를 만들기 위해서는 먼저 redux의 createStore 함수를 import 해줘야 합니다.
import { createStore } from "redux";
 
state를 변경시키는 리듀서 함수를 모아놓은 rootReducer를 createStore의 매개변수로 넣어주어 스토어를 생성하고, 생성된 스토어 객체를 store 변수에 선언합니다.
const store = createStore(rootReducer);
 
여기서 콘솔에 store를 찍어보면 5개의 메서드들이 내장 되어있습니다.
notion imagenotion image
  • getState() : 현재 state값을 반환하는 함수입니다.
  • subscribe() : 매개변수에 구독자(listener) 함수가 들어가는데, 이 함수는 변화가 있을 때 어떤 것을 실행할지에 대한 함수입니다.
  • dispatch() : 액션을 디스패치하여 상태 변경을 트리거 하는 함수입니다.
💡
나머지 함수들은 생소한 함수들인 것 같은데 무슨 함수들인가요? 나머지 두 함수는 개발자들이 잘 사용하지 않는 함수들이기 때문에 그냥 "이런 함수구나~" 하고 넘어가도 좋습니다.
observable 함수는 RxJS 라이브러리 옵저버블 패턴을 구현한 것입니다. 이 함수는 Redux 스토어의 상태 변경에 대한 구독(subscribe) 및 리스닝(listening) 작업 등을 수행할 수 있습니다. 하지만 대부분은 React-Redux 라이브러리에서 제공하는 useSelector, useDispatch 기능을 활용하여 상태를 선택하고 액션 디스패치 같은 작업을 처리하므로 개발자들이 직접적으로 observable 함수를 사용할 필요는 없습니다.
replaceReducer 함수는 현재 시점에 스토어의 리듀서를 교체하는 데 사용합니다. 이 함수는 주로 분할 또는 런타임에 리듀서를 추가하거나 변경할 때 사용되는데 일반적으로 개발에서 이 메서드를 사용할 일이 거의 없습니다.

1) Provider로 store 설정하기

만들어 준 store에 자식 컴포넌트들이 접근하기 위해서는 Provider를 이용하여 앱 전체에 store를 주입하여 제공해 줍니다. store 안의 상태는 직접적으로 수정이 불가능하며, 상태를 변경하려면 반드시 Action을 디스패치하고, 그 액션은 Reducer 함수에서 변경하여 새로운 상태를 반환해야 합니다.
// index.js import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { createStore } from "redux"; import { Provider } from "react-redux"; import rootReducer from "./modules/index"; const store = createStore(rootReducer); const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <Provider store={store}> <App /> </Provider> );
💡
createStore에 취소선이 그이는 걸까?
notion imagenotion image
createStore에 마우스를 갖다 대면 @reduxjs/toolkit에서 제공하는 configureStore를 사용하라고 나오는데 Redux v4부터 createStore 함수가 공식적으로 더 이상 권장되지 않기 때문입니다. Redux v4부터는 더 많은 기능을 제공하기 위해 새로운 API인 configureStore가 도입되었습니다.
configureStore 함수는 createStore보다 간단한 스토어를 설정하고 여러 가지 기본 설정 및 middleware를 자동으로 처리해 주기 때문에 개발자가 별도로 구성할 필요가 없습니다. configureStore에 대해서는 다음 챕터인 redux-toolkit에서 다룰것이며 createStore에 대한 취소선을 무시하고 사용해도 문제가 없습니다.
 

2.2.6 useSelector

useSelector는 react-redux 라이브러리에서 제공하는 Hook입니다. 이 Hook을 사용하면 함수형 컴포넌트에서 Redux 스토어에 저장된 상태를 조회할 수 있습니다. 더불어 useSelector는 성능 최적화에 도움을 주는 중요한 역할을 하며, 내부적으로 구독과 해제를 자동으로 처리해 주므로 컴포넌트 내부에서 명시적으로 subscribe 메서드를 호출할 필요가 없습니다.
 
useSelector 훅의 인자로 전달되는 state 매개변수는 Redux 스토어의 현재 상태를 나타냅니다. 이 state 객체는 Redux 스토어에 저장된 전체 상태를 포함하고 있으며, 여러 리듀서(reducer) 함수에 의해 관리되는 여러 부분의 상태를 담고 있습니다.
const target = useSelector((state) => state.someReducer); const target = useSelector((state) => state.someReducer.someValue);
useSelector를 사용할 때 state를 인자로 받는 콜백 함수를 작성하고, 이 콜백 함수 내에서 state 객체를 통해 원하는 부분 상태를 선택합니다. 즉, state 객체를 분해하여 스토어에서 필요한 부분 상태를 추출하는 데 사용합니다. 필요한 부분 상태는 예를 들면, 리듀서 함수가 관리하는 특정 state가 될 수도 있습니다.

1) Calculator 컴포넌트에서 state 조회하기

위의 설명과 같이 Calculator 컴포넌트 내에서의 useSelector는 전역 상태의 저장소인 store의 현재 상태를 인자로 받아 화살표 함수를 통해 원하는 부분 상태인 calculatorReducer에 접근합니다.
// components/Calculator.jsx import React, { useState } from "react"; import { useSelector } from "react-redux"; import { onAddNumber, onDivideNumber, onMultiplyNumber, onResetNumber, onSubtractNumber, } from "../modules/calculator"; export default function Calculator() { const [inputValue, setInputValue] = useState(""); /* useSelector를 통해 store의 현재 state에 접근하고 그 값으로부터 파생된 요소(여기서는 calculatorReducer)에도 접근이 가능합니다. calculatorReducer에서 변화를 감지하면 Calculator 컴포넌트는 리렌더링 됩니다.*/ const result = useSelector((state) => state.calculatorReducer); return ( <> <div>결과: {result}</div> <input type="number" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <div> <button onClick={() => {}}>+</button> <button onClick={() => {}}>-</button> <button onClick={() => {}}>/</button> <button onClick={() => {}}>*</button> <button onClick={() => {}}>C</button> </div> <br /> </> ); }
따라서 Calculator 컴포넌트는 Redux 스토어의 calculatorReducer에 의존하게 되고 해당 상태가 변경되면 컴포넌트는 자동으로 리렌더링하게 됩니다.
 

2) TodoList 컴포넌트에서 state 조회하기

이어서 TodoList 컴포넌트에도 useSelector를 적용해 보겠습니다. TodoList 컴포넌트의 useSelector는 Calculator 컴포넌트와 다르게 선택자 함수에서 todoListReducer가 아닌 todoListReducer가 관리하는 특정 값인 todos를 추출하기 위해 state.todoListReducer.todos에 접근합니다.
// components/TodoList.jsx import React, { useState } from "react"; import { useSelector } from "react-redux"; import { onAddTodo, onDeleteTodo, onToggleTodo } from "../modules/todolist"; export default function TodoList() { const [inputValue, setInputValue] = useState(""); /* useSelector로 store의 현재상태를 조회해 그 중에서도 특정 Reducer(todoListReducer)에 의해 관리되는 특정 state(todos)에 접근합니다.*/ const todos = useSelector((state) => state.todoList.todos); return ( <> <form onSubmit={(e) => e.preventDefault()}> <input type="text" value={inputValue} placeholder="할 일을 작성해 주세요" onChange={(e) => setInputValue(e.target.value)} /> <button type="submit" onClick={() => {}}> 추가 </button> </form> {todos.map((todo) => ( <section key={todo.id}> <input type="checkbox" onClick={() => {}} /> <div style={{ textDecoration: todo.isDone ? "line-through" : "none" }} > {todo.content} </div> <button onClick={() => {}}>❌</button> </section> ))} </> ); }
추출한 todos를 map을 통해 순회하면서 각 할 일(todo)에 대한 새 배열을 생성하여 todos의 상태가 변경됨에 따라 리렌더링 됩니다.
 

2.2.7 Dispatch

dispatch는 Redux에서 액션을 보내는(또는 디스패치하는) 메서드입니다. 이것은 Redux 스토어를 통해 액션을 전달하고, 이로 인해 상태(state)의 변경이 발생합니다.
 
dispatch에 직접 액션 객체를 인자로 넣거나 액션 생성 함수를 보내면, 컴포넌트 내부에서 액션을 생성하여 Redux 스토어에 전달할 수 있습니다. 이렇게 하면 스토어는 리듀서 함수를 통해 현재 상태와 전달된 액션을 이용하여 새로운 상태를 계산하고 저장하게 됩니다. 따라서 dispatch를 통해 액션을 보내면 Redux 스토어에서 상태의 변경이 처리되고, 이로 인해 React 컴포넌트의 리렌더링이 트리거 됩니다.
 
컴포넌트 내부에서 dispatch를 사용하기 위해서는 먼저 react-redux 라이브러리 Hook 중 useDispatch를 import 해줘야 합니다.
import { useDispatch } from "react-redux";
 
useDispatch 훅을 사용하여 Redux 스토어의 dispatch 함수를 가져옵니다.
const dispatch = useDispatch();
 
이제 dispatch 함수를 사용하여 스토어에 액션을 보낼 수 있습니다. 액션은 일반적으로 액션 생성자 함수를 호출하여 생성한 후 dispatch 함수에 전달됩니다.
dispatch(someActionCreator(someData));

1) Calculator 컴포넌트에서 action 전달하기

button을 클릭 시, dispatch 함수에 액션 생성 함수를 전달합니다. 전달된 Action은 Reducer 함수를 통해 store의 state를 업데이트시킵니다.
// components/Calculator.jsx import React, { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { onAddNumber, onDivideNumber, onMultiplyNumber, onResetNumber, onSubtractNumber, } from "../modules/calculator"; export default function Calculator() { const [inputValue, setInputValue] = useState(""); const result = useSelector((state) => state.calculator); // useDispatch 훅을 사용하여 store의 dispatch 함수를 가져옵니다. const dispatch = useDispatch(); return ( <> <div>결과: {result}</div> <input type="number" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <div> {/* inputValue가 담긴 Action 생성 함수(혹은 액션 객체)들을 dispatch 함수에 전달합니다. */} {/* 전달된 값은 해당 리듀서를 통해 업데이트됩니다. */} <button onClick={() => dispatch(onAddNumber(inputValue))}>+</button> <button onClick={() => dispatch(onSubtractNumber(inputValue))}> - </button> <button onClick={() => dispatch(onDivideNumber(inputValue))}>/</button> <button onClick={() => dispatch(onMultiplyNumber(inputValue))}> * </button> <button onClick={() => dispatch(onResetNumber())}>C</button> </div> <br /> </> ); }
 

2) TodoList 컴포넌트에서 action 전달하기

TodoList 컴포넌트에서도 위와 동일한 작업을 진행합니다.
// components/TodoList.jsx import React, { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { onAddTodo, onDeleteTodo, onToggleTodo } from "../modules/todolist"; export default function TodoList() { const todos = useSelector((state) => state.todoList.todos); const dispatch = useDispatch(); const [inputValue, setInputValue] = useState(""); return ( <> <form onSubmit={(e) => e.preventDefault()}> <input type="text" value={inputValue} placeholder="할 일을 작성해 주세요" onChange={(e) => setInputValue(e.target.value)} /> {/*추가 버튼을 클릭 시, inputValue값이 담긴 onAddTodo 액션 함수를 dispatch를 통해 보내어 해당 리듀서에서 값을 처리합니다. */} <button type="submit" onClick={() => { dispatch(onAddTodo(inputValue)); }} > 추가 </button> </form> {todos.map((todo) => ( <section key={todo.id}> {/* onToggleTodo, onDeleteTodo 액션 함수는 todo.id를 인자로 받아 처리합니다. */} <input type="checkbox" onClick={() => { dispatch(onToggleTodo(todo.id)); }} /> <div style={{ textDecoration: todo.isDone ? "line-through" : "none" }} > {todo.content} </div> <button onClick={() => { dispatch(onDeleteTodo(todo.id)); }} > ❌ </button> </section> ))} </> ); }
이것을 끝으로 redux 라이브러리를 이용해 계산기와 투두리스트를 완성했습니다. 이러한 과정을 통해 Redux는 상태를 관리하고, 상태 변경을 예측 가능하고 추적 가능한 방식으로 처리합니다. 이로써 복잡한 상태 관리를 단순화하고 애플리케이션의 예측 가능성을 높이는 데 도움을 줍니다.