Redux Toolkit 기본 문법 가이드

odada·2025년 1월 7일
0

next.js

목록 보기
10/12

Redux Toolkit 기본 문법 가이드

1. Redux Toolkit의 핵심 개념

Redux Toolkit은 Redux를 사용하여 상태 관리를 더 쉽게 구현할 수 있도록 도와주는 패키지입니다. Redux Toolkit을 사용하면 다음과 같은 장점이 있습니다:

1.1. createSlice

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;
    }
  }
});

1.2. 액션과 리듀서 내보내기

createSlice 함수로 생성한 객체에서 actionsreducer를 추출하여 내보냅니다.

// 액션 생성자들 내보내기
export const { increment, decrement, incrementByAmount } = mySlice.actions;

// 리듀서 내보내기
export default mySlice.reducer;

1.3. 스토어 생성

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로 접근
  }
});

1.4. Provider 설정

Provider 컴포넌트로 애플리케이션에 스토어를 연결합니다.

import { Provider } from 'react-redux';
import { store } from './store';

function App() {
  return (
    <Provider store={store}>
      <div>
        <Counter />
        <UserProfile />
      </div>
    </Provider>
  );
}

1.5. 컴포넌트에서 사용

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>
  );
}

1.6. 실전 예제 - 간단한 카운터

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>
  );
}

2. TodoList에 Redux Toolkit 적용하기

todolist 소스
Redux Toolkit을 사용하여 상태 관리를 구현하는 단계는 다음과 같습니다:

  1. 패키지 설치: Redux Toolkit과 React-Redux를 설치합니다.
  2. Slice 생성: 상태와 리듀서를 한 번에 정의합니다.
  3. Store 생성: configureStore로 Redux 스토어를 생성합니다.
  4. Provider 설정: 애플리케이션에 스토어를 연결합니다.
  5. 컴포넌트에서 사용: Redux 상태를 사용하고 수정합니다.

2.1. Redux 설치

Redux를 설치하려면 다음 명령어를 실행합니다.

npm install @reduxjs/toolkit react-redux

2.2. Slice 생성자

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;

2.3. Store 생성

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 객체 사용을 위해 비활성화
    })
});

2.4. Provider 설정

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>
  );
}

2.5. 컴포넌트에서 사용

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 적용하기

할 일 관리 앱에 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

0개의 댓글