Redux는 action이라는 이벤트를 통해 애플리케이션의 상태를 관리하고 업데이트하기 위한 패턴이자 라이브러리입니다.
Redux를 사용하면 애플리케이션의 여러 부분에 필요한 "전역적인 상태"를 관리할 수 있습니다.
Action은 단순히 type과 paload 프로퍼티가 존재하는자바스크립트 객체로 생각하시면 됩니다.
도메인/이벤트명
과 같은 유형의 문자열로 작성합니다.const addTodoAction = {
type: "todos/todoAdd",
payload: "Redux 공부하기",
};
리듀서는 현재 상태와 액션 객체를 구독하고, 필요한 경우에 따라 상태를 업데이트하는 로직을 작성 후 새로운 상태를 반환하는 하나의 함수입니다.
const initialState = {value: 0};
function counterReducer(state = initialState, action) {
switch (action.type) {
case "counter/increment":
return {
...state,
value: state.value + 1,
};
/* 리듀서에서 type이 존재하지 않을 경우 기존 상태를 변경하지 않고 반환합니다*/
default:
return state;
}
}
📑 Reducer 함수 작성 시 지켜야 할 규칙
rtk의 configureStore로 만든 store 내부에는 dispatch라는 함수가 존재합니다.
store.dispatch()
를 호출하고 액션객체를 전달하는 것입니다.getState()
를 통해 업데이트된 값을 참조할 수 있게됩니다.pnpm i -D @reduxjs/toolkit react-redux
Redux 폴더 생성(=개인취향) 폴더 내부에 store.ts 파일을 정의하고 configureStore함수를 통해 만든 스토어를 내보냅니다.
/* redux/store.ts */
import {configureStore} from "@reduxjs/toolkit";
const store = configureStore({
reducer: {},
});
export default store;
만든 store의 상태는 전역적으로 관리되어야 하기 때문에, main.tsx에서 Provider 컴포넌트로 App을 감쌉니다.
Provider
컴포넌트는 react-redux에서 제공하는 컴포넌트로 해당 Provider
를 통해 전역 상태를 공급할 수 있게됩니다./* main.tsx */
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import {Provider} from "react-redux";
import store from "./store/store.ts";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
마찬가지로 redux 폴더 내부에서 공통적으로 관리하기 위해 redux 폴더 내부에 todoSlice 파일을 생성합니다.
/* redux/todoSlice.ts */
import {createSlice, nanoid, PayloadAction} from "@reduxjs/toolkit";
interface Todo {
id: string;
text: string;
done: boolean;
}
const initialState: Todo[] = [
{id: nanoid(), text: "Redux 학습하기", done: false},
];
export const todoSlice = createSlice({
name: "redux/todos",
initialState,
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
const newTodo: Todo = {
id: nanoid(),
text: action.payload,
done: false,
};
state.push(newTodo);
},
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.find((todo) => todo.id === action.payload);
if (todo) {
todo.done = !todo.done;
}
},
removeTodo: (state, action: PayloadAction<string>) => {
const index = state.findIndex((todo) => todo.id === action.payload);
if (index !== -1) {
state.splice(index, 1);
}
},
},
});
export const {addTodo, toggleTodo, removeTodo} = todoSlice.actions;
export default todoSlice.reducer;
initalState
라는 배열을 설정했고, slice에 넘겨줄 기본값 변수의 역할을 하게 됩니다.
const initialState: Todo[] = [
{id: nanoid(), text: "Redux 학습하기", done: false},
];
createSlice
createSlice API는 기존 Redux 작성시, 리듀서,액션, 불변성을 지키는 업데이트를 작성하는데 작성해야하는 상용구들을 모두 제거하도록 설계되었습니다.
action.text
나 action.id
와 같이 고유한 이름의 모든 필드는 해당 필드들이 포함된 객체인 action.payload
로 대체할 수 있습니다.reducers 내부에서 직접적인 기능 설정하기
console.log(nanoid()); // 'dgPXxUz_6fWIQBD8XmiSy'
1. addTodo (todos 배열에 새로운 todoItem 추가하기)
text
는 action.payload
로 설정하게 되는데, 최종적으로 사용자의 input을 상태로 받고 해당 input이 text로 가게될 것입니다.push
를 통해 기존 state 배열에 새로운 TodoItem
을 추가합니다. addTodo: (state, action: PayloadAction<string>) => {
const newTodo: Todo = {
id: nanoid(),
text: action.payload,
done: false,
};
state.push(newTodo);
}
2. toggleTodo (todoItem의 done 프로퍼티 토글 하기)
toggleTodo
메서드는 Array.find
메서드를 통해 todo.id
가 action.payload
와 같으면, 즉, 사용자가 해당 todoItem을 눌렀을 때 발생하며, 최종적으로 사용자가 클릭한 todoItem의 done
상태가 토글링됩니다. toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.find((todo) => todo.id === action.payload);
if (todo) {
todo.done = !todo.done;
}
}
3. removeTodo (todoItem 삭제하기)
todoItem을 삭제하는 로직입니다.
findIndex
메서드를 통해 index
가 -1과 같지않으면 즉, 유효한 인덱스라면 todoItem을 삭제합니다.
removeTodo: (state, action: PayloadAction<string>) => {
const index = state.findIndex((todo) => todo.id === action.payload);
if (index !== -1) {
state.splice(index, 1);
}
}
4. 최종적으로 만든 액션들을 구조분해 할당(todoSlice.actions) 후 내보냅니다.
export const {addTodo, toggleTodo, removeTodo} = todoSlice.actions;
export default todoSlice.reducer;
기존 store의 reducer 내부에서 빈 객체로 남겨두었던 todos를 만든 slice로 재정의합니다.
import {configureStore} from "@reduxjs/toolkit";
import todosReducer from "./todoSlice";
const store = configureStore({
reducer: {
todos: todosReducer,
},
});
export default store;
이제 준비는 끝났습니다. 만든 전역상태를 실제 컴포넌트 단에서 사용할 수 있습니다!
useSelector
를 통해 정의한 상태를 참조할 수 있습니다.useDispacth
를 통해 액션을 디스페치 즉, 이벤트 핸들링을 할 수 있습니다.
이제 handleAddTodo와 같은 함수들을 추가해 실질적인 UI가 업데이트 될 수 있도록 로직을 작성합니다.
slice
파일 내부에서 기능들을 전부 정의해서 필요한건 find
나, findIndex
에서 필요한 action.payload
, 즉 id
값만 매개변수로 전달해주면 됩니다.
/* App/TodoList.tsx */
import {useState} from "react";
import {useDispatch, useSelector} from "react-redux";
import {addTodo, removeTodo, toggleTodo} from "./redux/todoSlice";
interface Todo {
id: string;
text: string;
done: boolean;
}
function App() {
const [inputTodo, setInputTodo] = useState("");
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
const handleAddTodo = () => {
dispatch(addTodo(inputTodo));
setInputTodo("");
};
const handleToggleTodo = (id: string) => () => {
dispatch(toggleTodo(id));
};
const handleRemoveTodo = (id: string) => () => {
dispatch(removeTodo(id));
};
return (
<>
<div>
<input
type="text"
value={inputTodo}
onChange={(e) => setInputTodo(e.target.value)}
/>
<button onClick={handleAddTodo}>할일 추가</button>
{todos.map((todo: Todo) => (
<li key={todo.id}>
<span
style={{textDecoration: todo.done ? "line-through" : "none"}}
onClick={handleToggleTodo(todo.id)}>
{todo.text}
</span>
<button onClick={handleRemoveTodo(todo.id)}>할일 삭제</button>
</li>
))}
</div>
</>
);
}
export default App;
우리 TodoList 귀엽죠?.. 😀