-
[Redux] 공식문서 파먹기(2) - Todo List로 리덕스 이해하기JavaScript 2023. 5. 20. 22:41
해당 포스팅은 리덕스 공식문서를 번역하여 정리한 글입니다.
이전 포스팅에서는 리덕스의 기본과 컨셉을 정리했습니다.
이번 포스팅에서는 리덕스 공식문서의 예제를 기반으로 리덕스를 이해해 보겠습니다.
TodoList를 만드는 예제를 통해 리덕스를 이해해 보겠습니다
요구사항
요구사항을 구체화하여 상태와 리듀서를 작성해야 합니다.
먼저는 앱의 요구사항을 이해하고 어떤 상태와 리듀서를 작성할지 구상해 보겠습니다.
UI는 세 가지 주요 섹션으로 구성됩니다
- 사용자가 새 할 일 항목의 텍스트를 입력할 수 있는 input box
- 기존의 모든 할 일 목록
- 완료하지 않은 할 일의 수를 표기하고 필터링 옵션을 보여주는 바닥글 섹션
할 일 목록의 각 항목에 완료 상태를 전환하는 체크박스가 있어야 하며 미리 정의된 색상 목록에 대해 색상으로 구분된 카테고리 태그를 추가하고 할 일 항목을 삭제할 수 있어야 합니다.
카운터는 활성할 일의 수를 표기해야 합니다.
모든 할 일을 완료로 표기하고 완료된 모든 할 일을 제거하는 버튼이 있어야 합니다.
목록에 표기된 할 일을 필터링하는 방법은 두 가지입니다
- 'all', 'active', 'complete' 기준으로 필터링
- 하나 이상의 색상을 선택하여 해당 색상과 일치하는 태그가 있는 모든 할 일을 표기하는 방식으로 필터링
상태 구상하기
위의 요구사항을 기준으로 어떤 상태를 작성할지 구성해 보겠습니다.
이 앱을 크게 두 가지 주요 기능으로 구분해 보자면
- 현재 할 일 항목의 실제 목록
- 현재 필터링 옵션
먼저 각 할 일 항목에 대해 몇 가지 정보를 정의해야 합니다
- 사용자가 입력한 텍스트
- 완료 여부를 나타내는 boolean
- 고유한 ID
- 색상 카테고리
그리고 필터링 동작은 몇 가지 열거된 값으로 설명이 가능합니다
- 완료: "all", "active", "complete"
- 색상: "빨강", "노랑", "초록", "파랑", "주황", "보라"
위의 값들을 보면 할 일은 앱의 상태(앱이 동작하는 핵심 데이터)고 필터링의 값은 UI의 상태(앱이 무엇을 하고 있는지 설명하는 상태)로 정리할 수 있습니다.
이처럼 카테고리를 분류하면 앱의 다양한 상태들이 어떻게 사용돼야 하는지 구상하는데 도움이 됩니다.
상태 정의하기
상태 구상하기에서 정리한 내용을 기반으로 상태를 정의해 보겠습니다.
const initalState = { todos: [ {id: 0, content: 'userInputValue', complete: false }, {id: 1, content: 'userInputValue', complete: false, color: 'purple' }, {id: 2, content: 'userInputValue', complete: false, color: 'blue' }, ], filter: { status: 'Active', color: ['red', 'blue'] }, }별 다른 내용 없이 직관적으로 위에서 정의한 내용을 그대로 코드에 옮겼습니다.
액션 정의하기
요구사항을 기반으로 상태의 구조를 설계하고 정의했습니다.
액션도 위와 같이 동일하게 설계하고 정의해야 합니다.
액션은 사용자가 입력한 텍스트를 받아 새 할 일 항목을 추가해야 하고 사용자의 인터렉션에 따라 여러 가지 동작을 추가해야 합니다.
- 할 일의 완료 상태 전환
- 할 일의 색상 카테고리 선택
- 할 일 삭제
- 모든 할 일을 완료표기
- 완료된 모든 항목 지우기
- 다른 완료 필터 값 선택
- 새 색상 필터 추가
- 색상 필터 제거
액션은 'type' 프로퍼티와 상황에 따라 'payload'를 갖고 있는 일반 객체입니다.
{type: 'todos/todoAdded', payload: todoText} {type: 'todos/todoToggled', payload: todoId} {type: 'todos/colorSelected', payload: {todoId, color}} {type: 'todos/todoDeleted', payload: todoId} {type: 'todos/allCompleted'} {type: 'todos/completedCleared'} {type: 'filters/statusFilterChanged', payload: filterValue} {type: 'filters/colorFilterChanged', payload: {color, changeType}}위에서 구상한 액션을 코드로 옮기면 이러합니다.
상태 데이터와 마찬가지로 액션에도 어떤 일이 발생하는지 설명해 주는 최소한의 정보가 포함되야 합니다.
리듀서 작성하기
리듀서는 현재 상태와 액션을 인자로 받아 새로운 상태를 반환하는 함수입니다.
(state, action) => newState
루트 리듀서
리덕스의 리듀서는 루트리듀서 하나입니다. 추후에 스토어를 만들 때 루트 리듀서를 전달합니다.
루트 리듀서의 초기 상태가 필요하기 때문에 임의의 값을 정의해 전달할 겁니다.
const initialState = { todos: [ { id: 0, text: 'Learn React', completed: true }, { id: 1, text: 'Learn Redux', completed: false, color: 'purple' }, { id: 2, text: 'Build something fun!', completed: false, color: 'blue' } ], filters: { status: 'All', colors: [] } } // 초기값에 초기상태를 전달 export default function appReducer(state = initialState, action) { // 리듀서는 액션의 타입프로퍼티가 무엇을 하는지 알아야함 switch (action.type) { // 액션의 타입에따른 처리 default: // 액션의 타입에 해당하는것이 없다면 현재 상태를 반환 return state } }리듀서에 투두리스트 아이템을 추가하는 코드를 작성해 보겠습니다.
switch (action.type) { case 'todos/todoAdded': { // 새로운 객체를 반환합니다 return { // 상태를 복사합니다 ...state, // 복사한 상태의 todos 배열에 새로운 값이 있습니다 todos: [ // 상태의 이전 todos 배열을 복사합니다 ...state.todos, // 새로만들 todo 객체를 정의합니다 { id: nextTodoId(state.todos), text: action.payload, completed: false } ] } } default: return state }새로운 상태를 정의하는데 많은 코드가 필요합니다.
이렇게 작성해야 하는 이유는 바로 불변성을 지켜주기 위함입니다.
우리가 원하는 작업은 간단하게 투두리스트의 아이템을 하나 추가하는 것입니다만 굉장히 많은 보일러플레이트 코드를 작성해야 합니다.
Redux Toolkit을 이용하면 조금 더 간단하게 작성이 가능합니다.
툴킷은 추후에 리덕스의 API들과 비교하여 정리하겠습니다.
추가 액션 처리
case 'todos/todoToggled': { return { ...state, todos: state.todos.map(todo => { if (todo.id !== action.payload) { return todo } return { ...todo, completed: !todo.completed } }) } } case 'filters/statusFilterChanged': { return { // Copy the whole state ...state, // Overwrite the filters value filters: { // copy the other filter fields ...state.filters, // And replace the status field with the new value status: action.payload } } }ID를 기반으로 토글을 하는 액션과 필터 프로퍼티에 status 필드를 새로 정의하는 액션을 추가했습니다.
이제 겨우 세 개의 액션을 추가했지만 루트 리듀서의 코드가 길어지고 있습니다.
리듀서 분할하기
위에서 세 개의 액션 코드가 루트 리듀서의 몸집을 크게 만들었습니다.
우리가 정의한 상태는 크게 두 개로 나눌 수 있었습니다.
투두리스트 아이템 배열과 필터 객체입니다 이 두 개를 기준으로 리듀서를 나눠 작성하여 루트 리듀서의 전체 코드량을 줄여보겠습니다.
앱의 '기능'을 기준으로 리덕스 앱 폴더와 파일을 구성하는 것이 좋습니다.
특정 기능에 대한 리덕스 코드는 '슬라이스 파일'이라고 하는 단일 파일로 작성합니다.
해당 파일에는 앱 상태의 해당 부분에 대한 모든 리듀서와 액션을 정의합니다.
우리가 작성한 코드를 나눴을 때 todoSlice, filterSlice로 슬라이스 할 수 있습니다.
일반적으로 일부 액션 객체는 특정 슬라이스 리듀서와 연관성이 있습니다 그래서 가독성을 위해 액션의 타입 프로퍼티의 이름을 잘 정의해주어야 합니다.
예를 들면 기능의 이름이 todo, 이벤트가 todoAdded 라면 액션의 타입프로퍼티의 이름은 'todo/todoAdded'로 합쳐서 작성해 주는 게 좋습니다.
이제 분할하는 코드를 작성해 보겠습니다.
먼저 todoSlice.js 파일을 만들어줍니다.
// todoSlice.js const initialState = [ { id: 0, text: 'Learn React', completed: true }, { id: 1, text: 'Learn Redux', completed: false, color: 'purple' }, { id: 2, text: 'Build something fun!', completed: false, color: 'blue' } ] function nextTodoId(todos) { const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) return maxId + 1 } export default function todosReducer(state = initialState, action) { case 'todos/todoAdded': { // Can return just the new todos array - no extra object around it return [ ...state, { id: nextTodoId(state), text: action.payload, completed: false } ] } case 'todos/todoToggled': { return state.map(todo => { if (todo.id !== action.payload) { return todo } return { ...todo, completed: !todo.completed } }) } default: return state } }루트 리듀서에 정의했던 initalState의 todos 배열만 가져옵니다. todoSlice는 todos만 관심을 갖고 있기 때문에 filter 객체는 필요가 없습니다. 그리고 루트 리듀서에 작성했던 코드를 todoReducer 함수에 옮겨줍니다.
filterSlice.js 파일을 추가하고 루트 리듀서의 코드를 가져옵니다.
const initialState = { status: 'All', colors: [] } export default function filtersReducer(state = initialState, action) { switch (action.type) { case 'filters/statusFilterChanged': { return { ...state, status: action.payload } } default: return state } }이렇게 리듀서를 관심사(상태)를 기준으로 나누어 작성하기만 해도 읽기도 편하고 수정하기도 편해집니다.
그리고 초기 상태를 각 파일에서 관리하기 때문에 상태를 관리하기도 편합니다.
여러모로 슬라이스 리듀서로 작성을 하면 관리하기가 편합니다.
리듀서 결합하기
이제 나눈 리듀서들을 루트 리듀서에 합쳐보겠습니다.
combineReducers
리덕스 라이브러리에 있는 combineReducers를 이용하면 슬라이스 한 리듀서들을 간단하게 합칠 수 있습니다.
import { combineReducers } from 'redux' import todosReducer from './features/todos/todosSlice' import filtersReducer from './features/filters/filtersSlice' const rootReducer = combineReducers({ todos: todosReducer, filters: filtersReducer }) export default rootReducercombineReducers에 key: value 형태로 정의해 슬라이스한 리듀서들을 넣어 루트 리듀서에 리듀서들을 합칠 수 있습니다.
'JavaScript' 카테고리의 다른 글
[Redux] 공식문서 파먹기(4) - UI and React (0) 2023.06.02 [Redux] 공식문서 파먹기(3) - Store (0) 2023.05.24 [Redux] 공식문서 파먹기(1) - Essentials - Redux Overview and Concepts (0) 2023.05.18 [Javascript]변수와 메모리 (0) 2023.03.21 [Javascript]변수 (0) 2023.03.21