Redux Toolkit은 Redux를 사용하여 상태 관리를 더 쉽게 구현할 수 있도록 도와주는 패키지입니다. Redux Toolkit을 사용하면 다음과 같은 장점이 있습니다:
createSlice
는 Redux 상태 관리의 핵심 함수입니다. 한 번에 다음을 모두 생성합니다:
기본 구조:
import { createSlice } from '@reduxjs/toolkit';
const mySlice = createSlice({
name: '슬라이스_이름', // 액션 타입의 접두사가 됨
initialState: { // 초기 상태
value: 0
},
reducers: { // 리듀서 함수들
increment: (state) => {
state.value += 1; // 직접 수정 가능!
},
decrement: (state) => {
state.value -= 1;
},
// 파라미터가 필요한 경우
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});
createSlice
함수로 생성한 객체에서 actions
와 reducer
를 추출하여 내보냅니다.
// 액션 생성자들 내보내기
export const { increment, decrement, incrementByAmount } = mySlice.actions;
// 리듀서 내보내기
export default mySlice.reducer;
configureStore
함수로 Redux 스토어를 생성합니다.
모든 리듀서를 합쳐서 스토어를 생성합니다.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './slices/counterSlice';
import userReducer from './slices/userSlice';
export const store = configureStore({
reducer: {
counter: counterReducer, // state.counter로 접근
user: userReducer // state.user로 접근
}
});
Provider
컴포넌트로 애플리케이션에 스토어를 연결합니다.
import { Provider } from 'react-redux';
import { store } from './store';
function App() {
return (
<Provider store={store}>
<div>
<Counter />
<UserProfile />
</div>
</Provider>
);
}
useSelector
훅으로 Redux 상태를 가져오고, useDispatch
훅으로 액션을 디스패치합니다.
상태 읽기 (useSelector):
import { useSelector } from 'react-redux';
function Counter() {
// state.counter.value 값을 가져옴
const count = useSelector((state) => state.counter.value);
return <div>Count: {count}</div>;
}
상태 변경하기 (useDispatch):
import { useDispatch } from 'react-redux';
import { increment, incrementByAmount } from './counterSlice';
function Counter() {
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(increment())}>
증가
</button>
<button onClick={() => dispatch(incrementByAmount(5))}>
5만큼 증가
</button>
</div>
);
}
Redux Toolkit을 사용하여 간단한 카운터를 구현해보겠습니다.
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
status: 'idle'
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
setStatus: (state, action) => {
state.status = action.payload;
}
}
});
export const { increment, decrement, setStatus } = counterSlice.actions;
export default counterSlice.reducer;
// Counter.js
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const status = useSelector((state) => state.counter.status);
const dispatch = useDispatch();
return (
<div>
<div>
현재 카운트: {count}
상태: {status}
</div>
<button onClick={() => dispatch(increment())}>증가</button>
<button onClick={() => dispatch(decrement())}>감소</button>
</div>
);
}
todolist 소스
Redux Toolkit을 사용하여 상태 관리를 구현하는 단계는 다음과 같습니다:
Redux를 설치하려면 다음 명령어를 실행합니다.
npm install @reduxjs/toolkit react-redux
Slice는 상태와 리듀서를 한 번에 정의하는 함수입니다. Slice를 생성하려면 createSlice
함수를 사용합니다.
// store/slices/todoSlice.js
import { createSlice } from '@reduxjs/toolkit';
const todoSlice = createSlice({
name: 'todo',
initialState: {
todoList: [] // 초기 상태
},
reducers: {
// 액션과 리듀서를 한번에 정의
addTodo: (state, action) => {
const newTodo = {
id: state.todoList.length + 1,
task: action.payload,
isDone: false,
createdDate: new Date().getTime(),
};
state.todoList.unshift(newTodo);
},
updateTodo: (state, action) => {
const todo = state.todoList.find(todo => todo.id === action.payload);
if (todo) {
todo.isDone = !todo.isDone;
}
},
deleteTodo: (state, action) => {
state.todoList = state.todoList.filter(todo => todo.id !== action.payload);
}
}
});
// 액션 생성자들을 내보냅니다
export const { addTodo, updateTodo, deleteTodo } = todoSlice.actions;
// 리듀서를 내보냅니다
export default todoSlice.reducer;
Redux 스토어를 생성하려면 configureStore
함수를 사용합니다.
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from './slices/todoSlice';
export const store = configureStore({
reducer: {
todo: todoReducer
},
// 추가 설정이 필요한 경우
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false, // Date 객체 사용을 위해 비활성화
})
});
Provider 컴포넌트를 사용하여 애플리케이션에 스토어를 연결합니다.
// app/layout.js 또는 pages/_app.js
import { Provider } from 'react-redux';
import { store } from '@/store';
export default function RootLayout({ children }) {
return (
<Provider store={store}>
{children}
</Provider>
);
}
Redux 상태를 사용하려면 useSelector
훅을 사용합니다. 상태를 변경하려면 useDispatch
훅을 사용합니다.
// components/TodoList.js
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, deleteTodo } from '@/store/slices/todoSlice';
function TodoList() {
// Redux 상태 가져오기
const todos = useSelector((state) => state.todo.todoList);
// 액션 디스패치를 위한 함수
const dispatch = useDispatch();
// 할일 추가
const handleAdd = (task) => {
dispatch(addTodo(task));
};
// 할일 삭제
const handleDelete = (id) => {
dispatch(deleteTodo(id));
};
return (
<div>
{todos.map(todo => (
<div key={todo.id}>
<span>{todo.task}</span>
<button onClick={() => handleDelete(todo.id)}>삭제</button>
</div>
))}
</div>
);
}
주요 변경사항:
createStore 대신 configureStore 사용
액션과 리듀서를 따로 만드는 대신 createSlice로 한번에 생성
Immer가 내장되어 있어 상태를 직접 수정하는 것처럼 작성 가능
보일러플레이트 코드가 크게 감소
TypeScript와의 더 나은 통합
이렇게 구성하면 Redux의 장점을 살리면서도 더 간단하고 현대적인 방식으로 상태 관리를 구현할 수 있습니다.
할 일 관리 앱에 Redux를 적용해보겠습니다.
// store/slices/todoSlice.js
import {createSlice} from "@reduxjs/toolkit";
const mockTodoData = [
{
id: 1,
isDone: false,
task: '고양이 밥주기',
createdDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
task: '감자 캐기',
createdDate: new Date().getTime(),
},
{
id: 3,
isDone: false,
task: '고양이 놀아주기',
createdDate: new Date().getTime(),
},
];
// createSlice 함수를 사용하여 todoSlice를 생성
const todoSlice = createSlice({
name: 'todo',
initialState: {
todoList: mockTodoData
},
reducers: {
addTodo: (state, action) => {
const newTodo = {
id: state.todoList.length + 1,
task: action.payload,
isDone: false,
createdDate: new Date().getTime(),
};
state.todoList.unshift(newTodo);
},
updateTodo: (state, action) => {
const {id, task} = action.payload;
const targetIndex = state.todoList.findIndex(todo => todo.id === id);
state.todoList[targetIndex].task = task;
},
deleteTodo: (state, action) => {
const id = action.payload;
state.todoList = state.todoList.filter(todo => todo.id !== id);
},
}
})
export const { addTodo, updateTodo, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
// store/index.js
const { configureStore } = require("@reduxjs/toolkit");
import todoReducer from './slices/todoSlice';
export const store = configureStore({
reducer: {
todo: todoReducer
}
})
// components/Todo.jsx
"use client"
import React, { useState } from 'react'
import TodoHd from './TodoHd'
import TodoEditor from './TodoEditor'
import TodoList from './TodoList'
import { mockTodoData } from '@/data/todoData'
import {Provider} from "react-redux";
import {store} from "@/store";
const Todo = () => {
return (
<Provider store={store}>
<div className='flex flex-col gap-4 p-8 pb-40'>
<TodoHd />
<TodoEditor />
<TodoList />
</div>
</Provider>
)
}
export default Todo
// components/TodoList.jsx
import React, { useState } from 'react'
import TodoItem from './TodoItem'
import { set } from 'date-fns'
import {useSelector} from "react-redux";
const TodoList = () => {
const [search, setSearch] = useState('')
const todos = useSelector(state => state.todo.todoList)
const filteredTodo = () => {
if (!todos) return [];
return todos.filter((item) =>
item.task.toLowerCase().includes(search.toLowerCase())
);
};
return (
<div>
<h2>할 일 목록</h2>
<input
type="search"
value={search}
onChange={(e) => {setSearch(e.target.value)}}
placeholder='검색어를 입력하세요.'
className='p-3 text-black w-full'
/>
<ul className='mt-5 flex flex-col gap-2 divide-y'>
{filteredTodo().map((item) => (
console.log(item),
<TodoItem key={item.id} {...item} />
)
)}
</ul>
</div>
)
}
export default TodoList
// components/TodoItem.jsx
import classNames from 'classnames'
import React from 'react'
import {useDispatch} from "react-redux";
import {deleteTodo, updateTodo} from "@/store/slices/todoSlice";
const TodoItem = ({id, task, isDone, createDate }) => {
const dispatch = useDispatch();
const onUpdate = (id) => {
dispatch(updateTodo(id))
}
const onDelete = (id) => {
dispatch(deleteTodo(id))
}
return (
<li key={id} className='pt-2 flex gap-2 items-center'>
<input
type="checkbox"
checked={isDone}
onChange={() => {onUpdate(id)}}
/>
<strong className={
classNames('py-2 text-lg', isDone ? 'line-through' : null)
}>{task}</strong>
<span className='ml-auto text-sm text-gray-400'>{createDate}</span>
<button onClick={() => {onDelete(id)}}>삭제</button>
</li>
)
}
export default TodoItem
// components/TodoEditor.jsx
"use client"
import classNames from 'classnames';
import React, { useRef, useState } from 'react';
import { IoCloseCircle } from "react-icons/io5";
import {useDispatch} from "react-redux";
import {addTodo} from "@/store/slices/todoSlice";
const TodoEditor = () => {
const [task, setTask] = useState("")
const dispatch = useDispatch();
// inputRef 변수가 useRef()를 통해 생성된 객체를 참조하도록 설정
const inputRef = useRef()
const onChangeTask = (e) => {setTask(e.target.value)}
const onSubmit = (e) => {
e.preventDefault()
// 빈 입력 방지
if (!task) return
// 할 일 추가
// addTodo(task);
dispatch((addTodo(task)))
// 입력창 초기화 및 포커스
setTask("");
inputRef.current.focus();
}
const onKeyDown = (e) => {
if (e.key === "Enter") onSubmit()
if (e.key === "Escape") {
setTask("");
inputRef.current.focus();
}
}
const onCloseKey = () => {
setTask("");
inputRef.current.focus();
}
return (
<div>
<h2>새로운 Todo 작성하기</h2>
<div>
<form className='flex' onSubmit={onSubmit}>
<div className='relative flex-1'>
<input
type="text"
value={task}
ref={inputRef}
onKeyDown={onKeyDown}
onChange={onChangeTask}
placeholder="할 일을 입력하세요."
className='p-3 text-black w-full'
/>
<button
disabled={!task}
onClick={onCloseKey}
className={
classNames('absolute top-1 right-1 w-10 h-10 flex justify-center items-center', task ? 'text-black' : 'text-gray')
}>
<IoCloseCircle />
</button>
</div>
<button
type='submit'
disabled={!task}
className={
classNames('p-3', task ? 'bg-blue-300' : 'bg-gray-300')
}>할 일 추가</button>
</form>
</div>
</div>
)
}
export default TodoEditor