[#2] redux-toolkit-todo

redux-toolkit-todo

이번 포스팅에서는 TodoApp에서 사용되는 모든 action을 정의해보도록 하겠습니다. 이전 시리즈에서 TodoApp을 만들어보셨다면 어떤 기능들이 있는지 아실겁니다. 모르셔도 큰 상관은 없습니다.

TodoApp에서 사용되는 기능들은 다음과 같습니다.

  1. 새로운 Todo 추가하기
  2. Todo의 done 상태 변경하기
  3. Todo의 text 상태 변경하기
  4. Todo 삭제하기
  5. done 상태에 따라 목록 필터링하기
  6. done: true인 목록 제거하기
  7. Todo 목록의 done 상태 변경하기

현재 src/state/todos.js는 아래와 같습니다.

// src/state/todos.js

import { createSlice } from '@reduxjs/toolkit'

let uniqId = 0

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    filterType: 'all',
    items: [],
  },

  reducers: {
    add: {
      reducer: (state, action) => {
        state.items.push(action.payload)
      },

      prepare: text => {
        return {
          payload: {
            id: ++uniqId,
            done: false,
            text,
          },
        }
      },
    },
  },
})

export const { add } = todosSlice.actions

export default todosSlice.reducer

action들 구현하기

이제부터 하나씩 구현해보겠습니다.

1. 새로운 Todo 추가하기

이는 이미 구현되어 있는 기능입니다. add가 그 역할을 합니다.

2. Todo의 done 상태 변경하기

이 기능의 actioncheck라고 정의하겠습니다. add 밑에 다음과 같이 구현합니다.

check: (state, action) => {
  const { id, checked } = action.payload

  state.items = state.items.map(todo =>
    todo.id === id
      ? { ...todo, done: checked }
      : todo
  )
}

변경되는 아이템의 id와 체크 상태인 checked 값을 받아서 적용해줍니다. immer를 사용하고 있기 때문에, state.items = 와 같이 할당해줘야 변경됩니다. 참고로 reducer가 어떤 값을 리턴하게 되면, 이는 새로운 state setter로 동작하게 되므로 화살표 함수를 사용하실때 유의하셔야 합니다. 예를 들어

clearCompleted: (state, action) => state.items.filter(todo => !todo.done)

위와 같이 구현한다면, clearCompleted 액션의 reducer 결과는 필터링된 state.items가 됩니다. 원래의 initialState가 객체인데 배열로 바뀌어 버리는 것입니다. 이는 의도하지 않은 동작이기 때문에 주의해야 합니다.

3. Todo의 text 상태 변경하기

이는 done 상태를 변경하는 것과 유사합니다. check 밑에 edit으로 아래와 같이 추가합니다.

edit: (state, action) => {
  const { id, text } = action.payload

  state.items = state.items.map(todo =>
    todo.id === id
      ? { ...todo, text }
      : todo
  )
}

4. Todo 삭제하기

계속 비슷합니다. edit 밑에 remove을 아래와 같이 추가합니다.

remove: (state, action) => {
  const id = action.payload

  state.items = state.items.filter(todo => todo.id !== id)
}

5. done 상태에 따라 목록 필터링하기

이는 filterType의 상태를 변경하면 됩니다. filterremove 밑에 추가합니다.

filter: (state, action) => {
  state.filterType = action.payload
}

6. done: true인 목록 제거하기

filter 밑에 clearCompleted를 추가합니다.

clearCompleted: state => {
  state.items = state.items.filter(todo => !todo.item)
}

7. Todo 목록의 done 상태 변경하기

모두 done: true로 만들거나 done: false로 만드는 기능입니다. checkAll로 추가하겠습니다.

checkAll: state => {
  const done = action.payload

  state.items = state.items.map(todo => ({
    ...todo,
    done,
  }))
}

정리

TodoApp에서 사용하는 모든 기능들을 구현했습니다. 이제 이 action들을 export 해줍시다.

// 하단
export const {
  add,
  check,
  edit,
  remove,
  filter,
  clearCompleted,
  checkAll,
} = todosSlice.actions

액션들을 모두 export했습니다. 따로 action type, action creator, reducer들을 구현하지 않고 오직 reducer들만 구현하면 됩니다! 이제 리액트 컴포넌트는 이 액션들을 import하고 dispatch 해주면 reducer가 동작하게 될 것입니다.

지금까지 구현된 모습은 아래와 같습니다.

// src/state/todos.js

import { createSlice } from '@reduxjs/toolkit'

let uniqId = 0

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    filterType: 'all',
    items: [],
  },

  reducers: {
    add: {
      reducer: (state, action) => {
        state.items.push(action.payload)
      },

      prepare: text => {
        return {
          payload: {
            id: ++uniqId,
            done: false,
            text,
          },
        }
      },
    },

    check: (state, action) => {
      const { id, checked } = action.payload

      state.items = state.items.map(todo =>
        todo.id === id
          ? { ...todo, done: checked }
          : todo
      )
    },

    edit: (state, action) => {
      const { id, text } = action.payload

      state.items = state.items.map(todo =>
        todo.id === id
          ? { ...todo, text }
          : todo
      )
    },

    remove: (state, action) => {
      const id = action.payload

      state.items = state.items.filter(todo => todo.id !== id)
    },

    filter: (state, action) => {
      state.filterType = action.payload
    },

    clearCompleted: state => {
      state.items = state.items.filter(todo => !todo.done)
    },

    checkAll: (state, action) => {
      const done = action.payload

      state.items = state.items.map(todo => ({
        ...todo,
        done,
      }))
    }
  },
})

export const {
  add,
  check,
  edit,
  remove,
  filter,
  clearCompleted,
  checkAll,
} = todosSlice.actions

export default todosSlice.reducer

이제부터 컴포넌트들을 셋팅해보도록 하겠습니다.

컴포넌트 기본설정

현재 srcApp.jsindex.js가 있습니다. 우선 index.js에서, Provider를 이용해 store를 등록해줘야 합니다. 이는 기존 redux에서와 똑같습니다.

// src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './store'
import { Provider } from 'react-redux'
import 'todomvc-app-css/index.css'

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

그 다음, TodoApp의 기본 컴포넌트들을 만들어보겠습니다. src/components 디렉터리를 먼저 생성한 뒤, 이전 시리즈에서처럼 Header Main Footer Info 컴포넌트들을 구성해보겠습니다. 사실 Main 컴포넌트는 따로 자식컴포넌트인 Todo 컴포넌트를 가질 것을 생각해보면, 굳이 각 컴포넌트별로 디렉터리를 만들 필요는 없기 때문에 이번엔 Main을 제외하고 나머지는 .js 파일로 생성해주도록 하겠습니다.

// src/components/Header.js
function Header() {
  return (
    <header className="header">
      <h1>todos</h1>
      <input className="new-todo" placeholder="What needs to be done?" autoFocus />
    </header>
  )
}

export default Header
// src/components/Footer.js
function Footer() {
  return (
    <footer className="footer">
      <span className="todo-count"><strong>0</strong> item left</span>
      <ul className="filters">
        <li>
          <a className="selected" href="#/">All</a>
        </li>
    	<li>
    	  <a href="#/active">Active</a>
    	</li>
    	<li>
    	  <a href="#/completed">Completed</a>
    	</li>
      </ul>
      <button className="clear-completed">Clear completed</button>
    </footer>
  )
}

export default Footer
// src/components/Info.js
function Info() {
  return (
    <footer className="info">
      <p>Double-click to edit a todo</p>
      <p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
      <p>Created by <a href="http://todomvc.com">you</a></p>
      <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
    </footer>
  )
}

export default Info

이제 Main 컴포넌트는 디렉터리를 생성해주고 index.js로 만듭니다. 그 전에 Todo 컴포넌트를 만들겠습니다.

// src/components/Main/Todo.js
function Todo(props) {
  return (
    <li className="completed">
      <div className="view">
        <input className="toggle" type="checkbox" checked />
        <label>Taste JavaScript</label>
        <button className="destroy" />
      </div>
      <input className="edit" value="Create a TodoMVC template" />
    </li>
  )
}

export default Todo

이제 Main 컴포넌트를 구현합니다.

// src/components/Main/index.js
import Todo from './Todo'

function Main() {
  return (
    <section className="main">
      <input id="toggle-all" className="toggle-all" type="checkbox" />
      <label htmlFor="toggle-all">Mark all as complete</label>
      <ul className="todo-list" />
    </section>
  )
}

export default Main

아직 자식 컴포넌트가 없기 때문에 ul은 self-closing으로 구현합니다.

마지막으로, 이를 하나로 합친 App 컴포넌트를 아래와 같이 수정합니다.

// src/App.js

import Header from './components/Header'
import Main from './components/Main'
import Footer from './components/Footer'
import Info from './components/Info'

function App() {
  return <>
    <section className="todoapp">
      <Header />
      <Main />
      <Footer />
    </section>
    <Info />
  </>
}

export default App

이제 npm start를 하시면 아래와 같은 화면이 보입니다.

정리

지금까지 밑그림을 모두 그려봤습니다. 이제 색칠만 하면 됩니다. 리액트는 hook API가 나온 이후로 써드파티 생태계 역시 hook에 맞춰 진화하고 있는 것 같습니다. redux 역시 예외가 아닙니다.

redux는 redux-toolkit이 아닌 redux 자체에 이미 hook API가 구현되어있습니다. 그리고 redux에서는 앞으로 redux-toolkit을 사용하라고 강력히 권고하고 있습니다. 공식문서의 가장 첫번째 챕터인 introduction - Getting Started With Redux 페이지를 보면

위와 같이 Redux Toolkit에 대한 소개가 첫번째 페이지에 나오며, 공식적으로 권고하고 있습니다. redux-toolkit은 전통적인 방법으로도 redux를 사용할 수도 있지만, 일반적으로 useSelector 훅과 useDispatch 훅을 이용하여 쉽게 사용합니다.

다음 포스팅에서는 기능을 구현해보면서 useSelector, useDispatch 그리고 createSelector에 대해 알아보겠습니다.

좋은 웹페이지 즐겨찾기