이번 포스팅에서는 TodoApp에서 사용되는 모든 action
을 정의해보도록 하겠습니다. 이전 시리즈에서 TodoApp을 만들어보셨다면 어떤 기능들이 있는지 아실겁니다. 모르셔도 큰 상관은 없습니다.
TodoApp에서 사용되는 기능들은 다음과 같습니다.
done
상태 변경하기text
상태 변경하기done
상태에 따라 목록 필터링하기done: true
인 목록 제거하기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
들 구현하기이제부터 하나씩 구현해보겠습니다.
이는 이미 구현되어 있는 기능입니다. add
가 그 역할을 합니다.
done
상태 변경하기이 기능의 action
을 check
라고 정의하겠습니다. 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
가 객체인데 배열로 바뀌어 버리는 것입니다. 이는 의도하지 않은 동작이기 때문에 주의해야 합니다.
text
상태 변경하기이는 done
상태를 변경하는 것과 유사합니다. check
밑에 edit
으로 아래와 같이 추가합니다.
edit: (state, action) => {
const { id, text } = action.payload
state.items = state.items.map(todo =>
todo.id === id
? { ...todo, text }
: todo
)
}
계속 비슷합니다. edit
밑에 remove
을 아래와 같이 추가합니다.
remove: (state, action) => {
const id = action.payload
state.items = state.items.filter(todo => todo.id !== id)
}
done
상태에 따라 목록 필터링하기이는 filterType
의 상태를 변경하면 됩니다. filter
를 remove
밑에 추가합니다.
filter: (state, action) => {
state.filterType = action.payload
}
done: true
인 목록 제거하기filter
밑에 clearCompleted
를 추가합니다.
clearCompleted: state => {
state.items = state.items.filter(todo => !todo.item)
}
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
이제부터 컴포넌트들을 셋팅해보도록 하겠습니다.
현재 src
에 App.js
와 index.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
에 대해 알아보겠습니다.