이번 글은 TodoList 구현입니다.
큰 구성은 이전 로그인 구현 글과 같습니다.
Server API
먼저 TodoList의 CRUD api를 작성합니다.
/src/server/index.js
각 todo에는 id가 필요하기에 uuid를 설치하여 사용하였습니다. (https://www.npmjs.com/package/uuid)
로그인 때와 마찬가지로 DB는 localStorage로 대체하였으며 통신 시간은 1초로 설정하였습니다.
import { v4 as uuidv4 } from 'uuid';
// ...
// todos CRUD
// DB mocking with localStorage
function getTodos() {
return JSON.parse(localStorage.getItem('todos'));
}
function setTodos(todos) {
localStorage.setItem('todos', JSON.stringify(todos));
}
// create
export function addTodoRequest(todo) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
let todos = getTodos();
const id = uuidv4();
if (!todos) {
todos = { [id]: todo };
} else {
todos[id] = todo;
}
setTodos(todos);
resolve(todos);
} catch (error) {
reject(error);
}
}, 1000);
});
}
// read
export function getTodosRequest() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const todos = getTodos();
if (!todos) {
reject(`Read Error: No todo in DB.`);
}
resolve(todos);
}, 1000);
});
}
// update
export function updateTodoRequest(id, todo) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const todos = getTodos();
if (!todos[id]) {
reject(`Update Error: Item '${id}' doesn't exist.`);
}
todos[id] = todo;
setTodos(todos);
resolve(todos);
}, 1000);
});
}
// delete
export function deleteTodoRequest(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const todos = getTodos();
if (!todos[id]) {
reject(`Delete Error: Item '${id}' doesn't exist.`);
}
delete todos[id];
setTodos(todos);
resolve(todos);
}, 1000);
});
}
Redux
todoList에 대한 리덕스 모듈을 작성해줍니다.
/src/redux/module/todoList.js
todo 목록은 todos라는 이름의 object로 저장하도록 하였습니다. key는 uuid로 생성한 id, value는 todo에 해당하는 string입니다.
action은 pending, success, rejected 세 가지 종류로 작성하였으며 각 CRUD에 대한 action creator를 작성하였습니다.
import { AsyncState } from '../AsyncState';
import {
addTodoRequest,
updateTodoRequest,
deleteTodoRequest,
getTodosRequest,
} from '../../server';
// Actions
const TODOS_PENDING = 'my-todo/todos/TODOS_PENDING';
const TODOS_SUCCESS = 'my-todo/todos/TODOS_SUCCESS';
const TODOS_REJECTED = 'my-todo/todos/TODOS_REJECCTED';
// Reducer
const initialState = { todos: {}, todosState: AsyncState.IDLE, error: '' };
export default function reducer(state = initialState, action) {
switch (action.type) {
case TODOS_PENDING: {
return {
...state,
todosState: AsyncState.PENDING,
};
}
case TODOS_SUCCESS: {
return {
...state,
todosState: AsyncState.IDLE,
todos: action.payload,
};
}
case TODOS_REJECTED: {
return {
...state,
todosState: AsyncState.REJECTED,
error: String(action.payload),
};
}
default:
return state;
}
}
// Action Creators
// create
export function addTodo(todo) {
return (dispatch, getState) => {
dispatch({ type: TODOS_PENDING });
handleResponse(addTodoRequest(todo), dispatch);
};
}
// read
export function getTodos() {
return (dispatch, getState) => {
dispatch({ type: TODOS_PENDING });
handleResponse(getTodosRequest(), dispatch);
};
}
// update
export function updateTodo(id, todo) {
return (dispatch, getState) => {
dispatch({ type: TODOS_PENDING });
handleResponse(updateTodoRequest(id, todo), dispatch);
};
}
// delete
export function deleteTodo(id) {
return (dispatch, getState) => {
dispatch({ type: TODOS_PENDING });
handleResponse(deleteTodoRequest(id), dispatch);
};
}
function handleResponse(response, dispatch) {
response
.then((res) => {
dispatch({ type: TODOS_SUCCESS, payload: res });
})
.catch((error) => {
dispatch({ type: TODOS_REJECTED, payload: error });
});
}
이제 이것을 rootReducer에 추가해주면 모듈 추가 작업은 완료됩니다.
/src/redux/module/index.js
import { combineReducers } from 'redux';
import user from './user';
import todoList from './todoList';
const rootReducer = combineReducers({ user, todoList });
export default rootReducer;
Components
TodoList 전체 영역을 나타내는 TodoListComponent와 개별 리스트를 나타내는 TodoItemComponent를 작성하였습니다.
/src/App.jsx
App에 TodoListComponent를 추가하였습니다.
import './App.css';
import { Provider } from 'react-redux';
import store from './redux/index';
import LogInComponent from './components/LogInComponent';
import TodoListComponent from './components/TodoListComponent';
function App() {
return (
<div className='App'>
<Provider store={store}>
<h1>My TodoList</h1>
<LogInComponent />
<TodoListComponent /> // 추가!
</Provider>
</div>
);
/src/components/TodoListComponent.jsx
로그인 시 (userData가 변경될 시) TodoList 데이터를 가져오기 위해 useEffect를 사용해 처리하였습니다.
jsx는 로딩 중, 실패, 성공 시로 케이스를 나눠 작성하였습니다.
import { useDispatch, useSelector } from 'react-redux';
import { AsyncState } from '../redux/AsyncState';
import { useCallback, useRef, useEffect } from 'react';
import { addTodo, getTodos } from '../redux/module/todoList';
import TodoItemComponent from './TodoItemComponent';
export default function TodoListComponent() {
const newTodoRef = useRef(null);
// redux
const dispatch = useDispatch();
const userData = useSelector((state) => state.user.data);
const todos = useSelector((state) => state.todoList.todos);
const todosState = useSelector((state) => state.todoList.todosState);
const error = useSelector((state) => state.todoList.error);
useEffect(() => {
if (userData) {
dispatch(getTodos());
}
}, [userData]);
// event handlers
const onAddTodo = useCallback(() => {
dispatch(addTodo(newTodoRef.current.value));
newTodoRef.current.value = '';
}, []);
const todoList = Object.entries(todos);
return (
<>
<h2>Todo List</h2>
{userData ? (
// 로딩
todosState === AsyncState.PENDING ? (
<div>Loading...</div>
) : (
<>
// 새로운 todo 추가
<input type='text' ref={newTodoRef} />
<button onClick={onAddTodo}>Add Todo</button>
{todosState === AsyncState.REJECTED ? (
// 실패
<div style={{ color: 'red' }}>{error}</div>
) : (
// 성공
<>
{todoList.length ? (
<ul style={{ paddingLeft: '20px' }}>
{todoList.map(([id, todo]) => (
<TodoItemComponent id={id} todo={todo} />
))}
</ul>
) : (
<div>Empty</div>
)}
</>
)}
</>
)
) : (
<div>Please Log In.</div>
)}
</>
);
}
/src/components/TodoItemComponent.jsx
각 todo를 나타내는 컴포넌트입니다.
현재 존재하는 todo의 수정 여부에 따라 다른 다른 화면이 나타나도록 작성하였습니다.
import { useCallback, useRef, useState } from 'react';
import { deleteTodo, updateTodo } from '../redux/module/todoList';
import { useDispatch } from 'react-redux';
export default function TodoItemComponent({ id, todo }) {
const [isEdit, setEdit] = useState(false);
const todoRef = useRef(null);
const dispatch = useDispatch();
// event handlers
const startEdit = useCallback(() => {
setEdit(true);
}, []);
const confirmEdit = useCallback(() => {
setEdit(false);
dispatch(updateTodo(id, todoRef.current.value));
}, []);
const cancelEdit = useCallback(() => {
setEdit(false);
}, []);
const onDelete = useCallback(() => {
dispatch(deleteTodo(id));
}, []);
return (
<li key={id}>
{isEdit ? (
<>
<input defaultValue={todo} ref={todoRef}></input>{' '}
<button onClick={confirmEdit}>Confirm</button>{' '}
<button onClick={cancelEdit}>Cancel</button>
</>
) : (
<>
<span>{todo}</span> <button onClick={startEdit}>Edit</button>{' '}
<button onClick={onDelete}>Delete</button>
</>
)}
</li>
);
}
결과
최초 로그인 시 todoList 표시
새로운 todo 추가
todo 수정
todo 삭제
작성한 todo 다시 불러오기
추가 사항
localStorage에 저장된 내용은 크롬 개발자도구에서 확인 가능합니다.
개발자 도구 -> Application -> 사이드바에서 Local storage 하위 항목 url 선택
저장된 데이터를 삭제하고 싶을 때는 해당 데이터 위에서 우클릭 하여 delete 가능합니다.
소스 코드
GitHub repo: https://github.com/dev-wann/redux-practice, my-todo 폴더 내부 내용
이번 글에 해당하는 커밋: https://github.com/dev-wann/redux-practice/commit/80bf241bcfd7830cb6329139c4ad800e030b96be
'Web development > 상태관리' 카테고리의 다른 글
Redux - 로그인 + TodoList 예제 구현 (4) - RTK (0) | 2023.11.16 |
---|---|
Redux - 로그인 + TodoList 예제 구현 (2) redux-thunk (1) | 2023.11.14 |
Redux - 로그인 + TodoList 예제 구현 (1) redux (0) | 2023.11.12 |
Redux - 기본 개념 (0) | 2023.11.02 |