Front/React

Rudux로 전역 상태 관리하기

oodada 2024. 5. 4. 19:16

Redux로 전역 상태 관리하기

Redux 소개

Redux는 상태 관리 라이브러리로, React와 함께 사용하기 좋습니다. Redux를 사용하면 컴포넌트 간에 상태를 쉽게 공유할 수 있습니다.

- Redux 특징

  • Single Source of Truth: 애플리케이션의 상태는 하나의 스토어에 저장됩니다.
  • State is Read-Only: 상태를 직접 변경할 수 없습니다. 상태를 변경하는 유일한 방법은 액션을 디스패치하는 것입니다.
  • Changes are Made with Pure Functions: 상태를 변경하는 함수인 리듀서는 순수 함수여야 합니다.

- Redux 구조

  • Actions: 상태 변경을 위한 객체
  • Reducers: 상태를 변경하는 함수
  • Store: 애플리케이션의 상태를 저장하는 객체
store/
|-- actions/
|   |-- todoAction.js
|   |-- cartAction.js
|
|-- reducers/
|   |-- todoReducer.js
|   |-- cartReducer.js
|
|-- index.js

Redux를 사용하면 상태를 컴포넌트 간에 쉽게 공유할 수 있습니다. 또한, 상태 변경을 추적하기 쉽고, 상태 변경에 따른 로직을 쉽게 구현할 수 있습니다.

Redux 사용하기

Redux를 사용하려면 다음과 같은 단계를 따르면 됩니다.

  1. Redux 설치: Redux를 설치합니다.
  2. Actions 정의: 상태 변경을 위한 액션을 정의합니다.
  3. Reducers 정의: 상태를 변경하는 리듀서를 정의합니다.
  4. Store 생성: 애플리케이션의 상태를 저장하는 스토어를 생성합니다.
  5. Store 연결: 스토어를 컴포넌트에 연결합니다.

1. Redux 설치

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

npm install @reduxjs/toolkit react-redux
yarn add @reduxjs/toolkit react-redux

2. Actions 정의

액션은 상태 변경을 위한 객체입니다. 액션은 다음과 같이 정의할 수 있습니다.

// store/action.js
const action = {
    type: 'ADD_TODO', // 액션 타입
    payload: 'Learn Redux', // 액션 데이터
}

3. Reducers 정의

리듀서는 상태를 변경하는 함수입니다. 리듀서는 이전 상태와 액션을 받아 새로운 상태를 반환합니다.

// store/reducer.js
const reducer = (state, action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return {
                ...state,
                todos: [...state.todos, action.payload],
            }
        default:
            return state
    }
}

4. Store 생성

스토어는 애플리케이션의 상태를 저장하는 객체입니다. 스토어를 생성하려면 다음과 같이 createStore 함수를 사용합니다.

// store/index.js
import { createStore } from 'redux'

const store = createStore(reducer)

여러 개의 리듀서를 사용할 때는 combineReducers 함수를 사용합니다.

// store/index.js
import { createStore, combineReducers } from 'redux'
import todoReducer from './reducers/todoReducer'
import cartReducer from './reducers/cartReducer'

const rootReducer = combineReducers({
    todo: todoReducer,
    cart: cartReducer,
})

const store = createStore(rootReducer)

5. Store 연결

스토어를 컴포넌트에 연결하려면 다음과 같이 Provider 컴포넌트를 사용합니다.

// App.js
import { Provider } from 'react-redux'

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root'),
)

Redux를 사용하면 상태를 컴포넌트 간에 쉽게 공유할 수 있습니다. 또한, 상태 변경을 추적하기 쉽고, 상태 변경에 따른 로직을 쉽게 구현할 수 있습니다.

[실습] 할 일 관리 앱에 Redux 적용하기

할 일 관리 앱에 Redux를 적용해보겠습니다.

// src/store/actions/todoAction.js
// 액션 타입 정의 (대문자로 작성)
export const ADD_TODO = 'ADD_TODO' // 할 일 추가
export const UPDATE_TODO = 'UPDATE_TODO'
export const DELETE_TODO = 'DELETE_TODO'

// 액션 생성자 함수
export const addTodo = (task) => ({
    type: ADD_TODO, // 액션 타입
    payload: { id: Date.now(), isDone: false, task, createdDate: new Date().getTime() }, // 액션 페이로드
})

export const updateTodo = (id) => ({
    type: UPDATE_TODO,
    payload: id,
})

export const deleteTodo = (id) => ({
    type: DELETE_TODO,
    payload: id,
})
// src/store/reducers/todoReducer.js
import { ADD_TODO, UPDATE_TODO, DELETE_TODO } from '../actions/todoAction'

// 초기 상태
const initialState = {
    todos: [
        {
            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(),
        },
    ],
}

// 리듀서 함수
const todoReducer = (state = initialState, action) => {
    switch (action.type) {
        case ADD_TODO:
            return {
                // 기존 상태를 변경하지 않고 새로운 상태를 반환합니다.
                // redux는 불변성을 유지하고 이전 상태를 변경하지 않아야 합니다.
                ...state, // 기존 상태를 복사합니다.
                todos: [action.payload, ...state.todos], // 새로운 할 일을 추가합니다.
            }
        case UPDATE_TODO:
            return {
                ...state,
                todos: state.todos.map((it) => (it.id === action.payload ? { ...it, isDone: !it.isDone } : it)),
            }
        case DELETE_TODO:
            return {
                ...state,
                todos: state.todos.filter((it) => it.id !== action.payload),
            }
        default:
            return state
    }
}

export default todoReducer
// src/store/index.js
import todoReducer from './reducers/todoReducer'
import { combineReducers, createStore } from 'redux'

const rootReducer = combineReducers({
    // todoReducer를 todo라는 이름으로 사용합니다.
    todo: todoReducer,
    // cart: cartReducer,
})

const store = createStore(rootReducer)

export default store
// src/App.js
// Redux를 사용하기 위해 불필요한 TodoProvider 컴포넌트 삭제
// Redux의 Provider 컴포넌트로 감싸줍니다.

import React from 'react'
import TodoHd from './TodoHd'
import TodoEditor from './TodoEditor'
import TodoList from './TodoList'
import { Provider } from 'react-redux' // Redux의 Provider 임포트
import store from './store' // Redux 스토어 임포트

function App() {
    return (
        <Provider store={store}>
            {' '}
            {/* Redux의 Provider로 감싸기 */}
            <div>
                <TodoHd />
                <TodoEditor />
                <TodoList />
            </div>
        </Provider>
    )
}

export default App
// src/components/TodoEditor.js
// Redux와의 연동을 위해 더 이상 useTodoContext 훅 사용하지 않음
// 액션 함수를 디스패치하기 위해 useDispatch 훅을 사용
// TodoEditor 컴포넌트에서는 더 이상 useState 훅을 사용하여 상태를 관리하지 않음
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { addTodo } from '../../store/actions/todoAction'

export default function TodoEditor() {
    const [task, setTask] = useState('')
    const dispatch = useDispatch() // useDispatch 훅으로 디스패치 함수 가져오기

    const onSubmit = () => {
        if (!task) return
        dispatch(addTodo(task)) // 액션 함수 디스패치
        setTask('')
    }

    const onKeyDown = (e) => {
        if (e.key === 'Enter') {
            onSubmit()
        }
    }

    return (
        <div>
            <h3>새로운 Todo 작성하기 ✏</h3>
            <div>
                <input
                    type="text"
                    ref={(inputRef) => inputRef && inputRef.focus()}
                    placeholder="할 일을 추가로 입력해주세요."
                    onChange={(e) => setTask(e.target.value)}
                    onKeyDown={onKeyDown}
                    value={task}
                />
                <button onClick={onSubmit}>할 일 추가</button>
            </div>
        </div>
    )
}
// src/components/TodoList.js
// Redux와의 연동을 위해 useSelector 훅을 사용하여 상태를 가져오기
// 액션 함수를 디스패치하기 위해 useDispatch 훅을 사용
import React, { useState } from 'react'
import TodoItem from './TodoItem'
import { useSelector } from 'react-redux' // useDispatch 대신 useSelector 사용

function TodoList() {
    const [search, setSearch] = useState('')
    // useSelector에서 state로 전달되는 객체는 rootReducer에서 정의한 객체입니다.
    // rootReducer에서 todoReducer를 todo라는 이름으로 사용했기 때문에 state.todo로 todo 상태를 가져옵니다.
    // 따라서 state.todo.todo로 todo 상태를 가져올 수 있습니다.
    const todo = useSelector((state) => state.todo.todos) // todo 상태를 가져옵니다.

    const onChangeSearch = (e) => {
        setSearch(e.target.value)
    }

    const filteredTodo = () => {
        // todo 상태가 배열인지 확인하고 배열 메서드를 사용하기
        if (Array.isArray(todo)) {
            return todo.filter((item) => item.task.toLowerCase().includes(search.toLowerCase()))
        } else {
            return []
        }
    }

    return (
        <div>
            <h3>할 일 목록 📃</h3>
            <input type="text" placeholder="검색어를 입력하세요" onChange={onChangeSearch} value={search} />

            <div>
                {filteredTodo().map((item) => (
                    <TodoItem key={item.id} {...item} />
                ))}
            </div>
        </div>
    )
}

export default TodoList
// src/components/TodoItem.js
// Redux와의 연동을 위해 useDispatch 훅을 사용하여 액션 함수 디스패치
import React from 'react'
import { format } from 'date-fns'
import { useDispatch } from 'react-redux'
import { deleteTodo, updateTodo } from '../../store/actions/todoAction'

function TodoItem({ id, isDone, task, createdDate }) {
    const dispatch = useDispatch()

    return (
        <li key={id}>
            <input type="checkbox" checked={isDone} onChange={() => dispatch(updateTodo(id))} />
            <strong style={{ textDecoration: isDone ? 'line-through' : 'none' }}>{task}</strong>
            <span>{format(new Date(createdDate), 'yyyy.MM.dd')}</span>
            <button onClick={() => dispatch(deleteTodo(id))}>삭제</button>
        </li>
    )
}

export default TodoItem

Redux Toolkit으로 상태 관리하기

- Redux Toolkit이란

  • Redux를 사용할 때 더 쉽게 사용할 수 있도록 도와주는 라이브러리입니다.
  • Redux Toolkit을 사용하면 Redux를 더 쉽게 사용할 수 있습니다.

- createSlice란

  • Redux Toolkit에서 제공하는 함수로, 액션 생성자 함수와 리듀서 함수를 한 번에 생성합니다.
  • 이를 통해 코드의 반복을 줄이고, 코드를 더 간결하게 작성할 수 있습니다.

- Redux Toolkit 사용하기

npm install @reduxjs/toolkit
yarn add @reduxjs/toolkit
// src/store/slices/todoSlice.js
import { createSlice } from '@reduxjs/toolkit'

// 초기 상태
const initialState = {
    todos: [
        {
            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(),
        },
    ],
}

// todoSlice 함수를 사용하여 `slice`를 생성합니다.
// slice는 액션 생성자 함수와 리듀서 함수를 한 번에 생성합니다.
export const todoSlice = createSlice({
    name: 'todo', // 슬라이스 이름
    initialState, // 초기 상태
    // 객체 형태로 리듀서 함수를 정의합니다.
    reducers: {
        addTodo: (state, action) => {
            // state.todos 배열에 새로운 할 일을 추가합니다.
            // action.payload에는 새로운 할 일이 들어있습니다.
            state.todos.push(action.payload) // 새로운 할 일을 추가합니다.
        },
        updateTodo: (state, action) => {
            // state.todos 배열에서 id가 일치하는 할 일을 찾습니다.
            // action.payload에는 할 일의 id가 들어있습니다.
            // 삼항 연산자를 사용하여 isDone 값을 반전시킵니다.
            state.todos = state.todos.map((todo) =>
                todo.id === action.payload ? { ...todo, isDone: !todo.isDone } : todo,
            )
        },
        deleteTodo: (state, action) => {
            state.todos = state.todos.filter((todo) => todo.id !== action.payload)
        },
    },
})

// 액션 생성자 함수 내보내기
export const { addTodo, updateTodo, deleteTodo } = todoSlice.actions

// todoSlice.reducer 내보내기
export default todoSlice.reducer
// src/App.js
// Redux를 사용하기 위해 불필요한 TodoProvider 컴포넌트 삭제
// Redux의 Provider 컴포넌트로 감싸줍니다.
import React, { useReducer, useState } from 'react'
import TodoHd from './components/TodoHd'
import TodoEditor from './components/TodoEditor'
import TodoList from './components/TodoList'
import { Provider } from 'react-redux'
import { configureStore } from '@reduxjs/toolkit'
import todoSlice from '../../store/slices/todoSlice'

// Redux store 생성
// configureStore 함수를 사용하여 Redux store를 생성합니다.
const store = configureStore({
    reducer: {
        todo: todoSlice,
    },
})

const Todo = () => {
    return (
        <Provider store={store}>
            <TodoHd />
            <TodoEditor />
            <TodoList />
        </Provider>
    )
}

export default Todo
// src/components/TodoEditor.js
// Redux와의 연동을 위해 더 이상 useTodoContext 훅 사용하지 않음
// 액션 함수를 디스패치하기 위해 useDispatch 훅을 사용
// TodoEditor 컴포넌트에서는 더 이상 useState 훅을 사용하여 상태를 관리하지 않음

import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { addTodo } from '../../../store/slices/todoSlice' // todoSlice에서 액션 함수 가져오기

export default function TodoEditor() {
    const [task, setTask] = useState('')
    const dispatch = useDispatch() // useDispatch 훅으로 디스패치 함수 가져오기

    const onSubmit = () => {
        if (!task) return
        dispatch(
            addTodo({
                id: Date.now(),
                isDone: false,
                task,
                createdDate: new Date().getTime(),
            }),
        ) // 액션 함수 디스패치
        setTask('')
    }

    const onKeyDown = (e) => {
        if (e.key === 'Enter') {
            onSubmit()
        }
    }

    return (
        <div>
            <h3>새로운 Todo 작성하기 ✏</h3>
            <div>
                <input
                    type="text"
                    ref={(inputRef) => inputRef && inputRef.focus()}
                    placeholder="할 일을 추가로 입력해주세요."
                    onChange={(e) => setTask(e.target.value)}
                    onKeyDown={onKeyDown}
                    value={task}
                />
                <button onClick={onSubmit}>할 일 추가</button>
            </div>
        </div>
    )
}
// src/components/TodoList.js
// Redux와의 연동을 위해 useSelector 훅을 사용하여 상태를 가져오기
// 액션 함수를 디스패치하기 위해 useDispatch 훅을 사용
import React, { useState } from 'react'
import TodoItem from './TodoItem'
import { useSelector } from 'react-redux' // useDispatch 대신 useSelector 사용

function TodoList() {
    const [search, setSearch] = useState('')
    // useSelector에서 state로 전달되는 객체는 rootReducer에서 정의한 객체입니다.
    // rootReducer에서 todoReducer를 todo라는 이름으로 사용했기 때문에 state.todo로 todo 상태를 가져옵니다.
    // 따라서 state.todo.todos로 todo 상태를 가져올 수 있습니다.
    const todo = useSelector((state) => state.todo.todos) // todo 상태를 가져옵니다.

    const onChangeSearch = (e) => {
        setSearch(e.target.value)
    }

    const filteredTodo = () => {
        // todo 상태가 배열인지 확인하고 배열 메서드를 사용하기
        if (Array.isArray(todo)) {
            return todo.filter((item) => item.task.toLowerCase().includes(search.toLowerCase()))
        } else {
            return []
        }
    }

    return (
        <div>
            <h3>할 일 목록 📃</h3>
            <input type="text" placeholder="검색어를 입력하세요" onChange={onChangeSearch} value={search} />

            <div>
                {filteredTodo().map((item) => (
                    <TodoItem key={item.id} {...item} />
                ))}
            </div>
        </div>
    )
}

export default TodoList
// src/components/TodoItem.js
// Redux와의 연동을 위해 useDispatch 훅을 사용하여 액션 함수 디스패치
import React from 'react'
import { format } from 'date-fns'
import { useDispatch } from 'react-redux'
import { updateTodo, deleteTodo } from '../../../store/slices/todoSlice' // todoSlice에서 액션 함수 가져오기

function TodoItem({ id, isDone, task, createdDate }) {
    const dispatch = useDispatch()

    return (
        <li key={id}>
            <input type="checkbox" checked={isDone} onChange={() => dispatch(updateTodo(id))} />
            <strong style={{ textDecoration: isDone ? 'line-through' : 'none' }}>{task}</strong>
            <span>{format(new Date(createdDate), 'yyyy.MM.dd')}</span>
            <button onClick={() => dispatch(deleteTodo(id))}>삭제</button>
        </li>
    )
}

export default TodoItem
티스토리 친구하기