일단 여러 에러들을 확인했고, toggle에 대한 saga와 reducer를 작성하지 않았기 때문에 발생했던 것들이 대부분이었다.
- 체크박스를 클릭해도, db에서는 바뀌지 않는다.
-> (toggle reducer, toggle saga 가 없었음)- 수정할 때, completed 가 없어짐(db)
-> (edit saga에서 api를 요청할 때, <id, title, completed> 인자 모두 넘겨줘야 했었음- 삭제할 때, db는 없어지는데 화면에는 남아있음
-> action.payload 자체가 id인데, action.payload.id 라고 payload에 남겨놔서 에러가 발생했던 거였다. action.payload 라고만 고치면 된다.
일단 saga를 사용하면서 생각했던 부분은 다음과 같다.
- 이벤트를 실행할때, 어떤 순서로 진행되고 어디가 끝인지 알아야 한다.
- 클릭이벤트 -> handle 함수 -> 특정 액션 생성함수 -> 해당 타입을 가지고 있는 saga 함수 실행 -> saga 실행 중간에 api -> put으로 액션 실행 -> 성공 시 해당 리듀서로 ( 보통 action.payload는 해당 전체 데이터이다. ) -> 화면 및 db 확인
이러한 부분을 고려하면서 코드를 작성한다면, 괜찮지 않을까 생각한다.
(사실 확답을 할수는 없다 ㅎ)
redux-saga 작성 순서는 action -> saga -> reducer 느낌으로 작성 해보려고 한다.
css 는 항----상 마지막에!
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { composeWithDevTools } from 'redux-devtools-extension';
import App from './App';
import rootReducer from './reducers/rootReducer';
import rootSaga from './sagas/todoSaga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware))
)
sagaMiddleware.run(rootSaga);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
index.js는 항상 나의 세팅이라고 생각한다.
핵심 키워드 :
<Provider store={store}>
,store = createStore
,rootReducer
,applyMiddleware
,createSagaMiddleware
,sagaMiddleware.run(rootSaga)
import React from "react";
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import TodoList from "./components/TodoList";
import Home from "./Home";
function App() {
return (
<Router>
<nav>
<ul>
<li>
<Link to='/'>Home</Link>
</li>
<li>
<Link to='/todos'>Todos</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/todos" element={<TodoList/>}/>
</Routes>
</Router>
);
}
export default App;
라우터 설정을 위해서 없는 컴포넌트를 미리 걸어 놓는 등의 작업을 해놓는것도 나쁘지 않다고 생각한다.
import { combineReducers } from 'redux';
import todoReducer from '../actions/todoAction';
const rootReducer = combineReducers({
todos: todoReducer
});
export default rootReducer;
본인은 rootReducer 파일 역시 세팅의 일부라고 생각하는 편이다.
그래서 어떤 리듀서를 사용할것인지 미리 적어두고 주석을 해제하는 식으로 하는것도 좋다고 생각한다.
import axios from 'axios';
const API_URL = 'http://localhost:3001';
export const getTodosApi = async () => {
const response = await axios({
method: 'GET',
url: `${API_URL}/todos`
})
return response.data;
}
export const addTodoApi = async (title) => {
const response = await axios({
method: 'POST',
url: `${API_URL}/todos`,
data: { title, completed: false }
})
return response.data;
};
export const editTodoApi = async (id, title, completed) => {
const response = await axios({
method: 'PUT',
url: `${API_URL}/todos/${id}`,
data: { title, completed }
})
return response.data;
};
export const deleteTodoApi = async (id) => {
try {
const response = await axios({
method: 'DELETE',
url: `${API_URL}/todos/${id}`
})
return response.data;
} catch (error) {
console.log(error)
}
};
export const toggleTodoApi = async (todo) => {
const { id, completed, title } = todo;
try {
const response = await axios({
method: 'PUT',
url: `${API_URL}/todos/${id}`,
data: { title, completed }
})
return response.data
} catch (error) {
console.log(error)
}
}
API 요청을 하는 함수를 저런식으로 미리 만들어 놓으면 나중에 API 요청할때 편할 것이라고 생각한다. 그래서 API부터 먼저 최대한 만들어 놓을 수 있으면 그렇게 하는게 좋다. ( 물론 json-server 설치하는게 최고 )
import React from "react";
const TodoList = () => {
return (
<div>
<h1>ToDo List</h1>
<input type="text"/>
<button></button>
<ul>
/* 여기에 map을 통해서 리스트를 자동으로 가져오게 함 */
<li>
<input type="checkbox"/>
<span>제목</span>
<div>
<span>미완료</span>
<button>수정</button>
<button>삭제</button>
</div>
</li>
</ul>
</div>
);
};
export default TodoList;
일단 기본적인 컴포넌트 구조만 짜둔다. map을 사용해서 컴포넌트를 재사용하여 나타내는 부분은 간단하게 주석으로 써둔다.
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { addTodo } from "../actions/todoAction";
const TodoList = () => {
const dispatch = useDispatch();
const todos = useSelector((state) => state.todos.todos);
// Effect Hook을 이용하여 컴포넌트가 마운트될 때에만 실행되는
// 비동기 함수 getTodos()를 호출하도록 한다.
useEffect(() => {
dispatch(getTodos());
}, [dispatch])
return (
<div>
<h1>ToDo List</h1>
<input type="text"/>
<button></button>
<ul>
{/* todos 배열의 각 요소에 대해 map() 메소드를 호출하여
<li> 요소를 생성하고, key prop을 지정한다. */}
{todos.map((todo) => (
<li
key={todo.id}
>
<span>{todo.title}</span>
<div>
{/* completed 속성이 true이면 'Completed',
false이면 'InComplete'를 표시한다. */}
<span completed={todo.completed}>
{todo.completed ? 'Completed' : 'InComplete'}
</span>
</div>
</li>
))}
</ul>
</div>
);
};
export default TodoList;
// 액션 타입 상수 선언
export const GET_TODOS = 'GET_TODOS';
export const GET_TODOS_SUCCESS = 'GET_TODOS_SUCCESS';
// getTodos 액션 생성 함수, type 필드만 가진 액션 객체 반환
export const getTodos = () => ({
type: GET_TODOS
});
// getTodosSuccess 액션 생성 함수, payload에 todos 배열을 포함한 액션 객체 반환
export const getTodosSuccess = (todos) => ({
type: GET_TODOS_SUCCESS,
payload: todos,
})
// 초기 상태값 선언
const initialState = {
todos: [],
}
// todoReducer 함수 정의, state와 action 객체를 받아 새로운 state 반환
export default function todoReducer(state=initialState, action) {
switch (action.type) {
// GET_TODOS_SUCCESS 액션이 들어오면 state.todos 필드를 action.payload.todos로 변경하고,
// 나머지 필드는 그대로 유지한 새로운 객체를 반환한다.
case GET_TODOS_SUCCESS:
return { ...state, todos: action.payload.todos }
// 다른 액션 타입일 경우 이전 상태를 그대로 반환한다.
default:
return state;
}
}
// Import 필요한 모듈
import { takeLatest, call, put } from 'redux-saga/effects';
import {
GET_TODOS, // GET_TODOS action 타입
GET_TODOS_SUCCESS, // GET_TODOS_SUCCESS action 타입
} from '../actions/todoAction'; // todoAction에서 export한 액션 타입들 import
import {
getTodosApi, // getTodosApi() 함수 import
} from '../api/todoApi'; // todoApi에서 export한 getTodosApi() 함수 import
// getTodos() 함수 정의
function* getTodos() {
try {
const todos = yield call(getTodosApi); // 비동기 API 호출
yield put({ type: GET_TODOS_SUCCESS, payload: { todos }}); // 성공적으로 데이터를 받아온 경우 GET_TODOS_SUCCESS action을 dispatch 함
} catch (error) {
console.log(error); // API 호출이 실패한 경우 error를 console에 출력
}
}
// rootSaga() 함수 정의
export default function* rootSaga() {
yield takeLatest(GET_TODOS, getTodos); // GET_TODOS action이 발생할 때마다 getTodos() 함수를 호출
}
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { addTodo } from "../actions/todoAction"; // todoAction에서 export한 addTodo() 액션 생성 함수 import
const TodoList = () => {
const dispatch = useDispatch(); // useDispatch hook을 이용하여 Redux store에 dispatch할 수 있는 함수를 받아옴
const [inputValue, setInputValue] = useState(""); // useState hook을 이용하여 input 값 상태 관리
const todos = useSelector((state) => state.todos.todos); // useSelector hook을 이용하여 Redux store의 todos state 값을 가져옴
const handleAddTodo = () => {
if (!inputValue.trim()) { // inputValue가 공백 문자열인 경우 추가하지 않음
return;
}
if (editId) { // editId가 존재하는 경우(edit 모드일 경우)
dispatch(editTodo(editId, inputValue, completed)); // editTodo action을 dispatch하여 수정된 todo를 업데이트
setEditId(null); // editId 값을 초기화
} else { // edit 모드가 아닌 경우(add 모드일 경우)
dispatch(addTodo(inputValue, completed)) // addTodo action을 dispatch하여 새로운 todo를 추가
}
setInputValue(''); // input 값을 초기화
}
useEffect(() => {
dispatch(getTodos()); // 페이지 로드 시 useEffect hook이 실행되면서 getTodos action을 dispatch하여 초기 데이터를 가져옴
}, [dispatch])
return (
<div>
<h1>ToDo List</h1>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> {/* input 값이 변경되면 inputValue 상태를 업데이트 */}
<button onClick={handleAddTodo}></button> {/* 추가 버튼을 누르면 handleAddTodo 함수를 호출하여 새로운 todo를 추가 */}
<ul>
{/* todos state 배열을 map 함수를 이용하여 리스트 형태로 렌더링 */}
{todos.map((todo) => (
<li
key={todo.id} // key 값으로 각 todo의 id를 사용하여 React가 컴포넌트를 효율적으로 렌더링할 수 있도록 함
>
<span>{todo.title}</span> {/* todo의 title 값을 렌더링 */}
<div>
<span completed={todo.completed}>
{todo.completed ? 'Completed' : 'InComplete'} {/* todo의 completed 상태에 따라 Completed 또는 InComplete를 렌더링 */}
</span>
</div>
</li>
))}
</ul>
</div>
);
};
export default TodoList;
export const ADD_TODO = 'ADD_TODO'; // ADD_TODO action 타입 상수
export const ADD_TODO_SUCCESS = 'ADD_TODO_SUCCESS'; // ADD_TODO_SUCCESS action 타입 상수
export const addTodo = (title, completed) => ({ // addTodo action 생성 함수 정의
type: ADD_TODO, // ADD_TODO action 타입
payload: { title, completed } // 전달할 데이터(payload)
});
export const addTodoSuccess = (todo) => ({ // addTodoSuccess action 생성 함수 정의
type: ADD_TODO_SUCCESS, // ADD_TODO_SUCCESS action 타입
payload: { todo } // 전달할 데이터(payload)
});
const initialState = { // 초기 상태
todos: [], // todos state는 빈 배열로 초기화
}
export default function todoReducer(state=initialState, action) { // todoReducer 함수 정의
switch (action.type) {
case ADD_TODO_SUCCESS: // ADD_TODO_SUCCESS action 발생 시
return { ...state, todos: [ ...state.todos, action.payload.todo ]}; // 기존의 todos 배열에 새로운 todo를 추가하여 state를 업데이트하고 반환
default:
return state; // 기존의 state를 반환
}
}
import { takeLatest, call, put } from 'redux-saga/effects';
import {
ADD_TODO, // ADD_TODO action 타입
ADD_TODO_SUCCESS, // ADD_TODO_SUCCESS action 타입
} from '../actions/todoAction'; // todoAction에서 export한 액션 타입들 import
import {
addTodoApi, // addTodoApi() 함수 import
} from '../api/todoApi'; // todoApi에서 export한 addTodoApi() 함수 import
// addTodo() 함수 정의
function* addTodo(action) {
try {
const todo = yield call(addTodoApi, action.payload.title); // 비동기 API 호출
yield put({ type: ADD_TODO_SUCCESS, payload: { todo }}); // 성공적으로 데이터를 받아온 경우 ADD_TODO_SUCCESS action을 dispatch 함
} catch (error) {
console.log(error); // API 호출이 실패한 경우 error를 console에 출력
}
}
// rootSaga() 함수 정의
export default function* rootSaga() {
yield takeLatest(ADD_TODO, addTodo); // ADD_TODO action이 발생할 때마다 addTodo() 함수를 호출
}
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { editTodo } from "../actions/todoAction"; // editTodo() 액션 생성 함수 import
const TodoList = () => {
const dispatch = useDispatch(); // useDispatch hook을 이용하여 Redux store에 dispatch할 수 있는 함수를 받아옴
const [editId, setEditId] = useState(null); // 현재 edit 모드인 todo의 id를 저장하는 state
const [completed, setCompleted] = useState(false); // 수정할 todo의 completed 상태를 저장하는 state
const [inputValue, setInputValue] = useState(""); // input 값 상태 관리
const todos = useSelector((state) => state.todos.todos); // useSelector hook을 이용하여 Redux store의 todos state 값을 가져옴
const handleAddTodo = () => {
if (!inputValue.trim()) { // inputValue가 공백 문자열인 경우 추가하지 않음
return;
}
if (editId) { // editId가 존재하는 경우(edit 모드일 경우)
dispatch(editTodo(editId, inputValue, completed)); // editTodo action을 dispatch하여 수정된 todo를 업데이트
setEditId(null); // editId 값을 초기화
} else { // edit 모드가 아닌 경우(add 모드일 경우)
dispatch(addTodo(inputValue, completed)) // addTodo action을 dispatch하여 새로운 todo를 추가
}
setInputValue(''); // input 값을 초기화
}
const handleEditTodo = (id, title, completed) => { // todo 수정 모드로 변경하는 함수
setEditId(id); // editId state를 변경하여 현재 edit 모드인 todo의 id를 저장
setInputValue(title); // inputValue state를 변경하여 수정할 todo의 title 값을 저장
setCompleted(completed || false); // completed state를 변경하여 수정할 todo의 completed 값을 저장
}
useEffect(() => {
dispatch(getTodos()); // 페이지 로드 시 useEffect hook이 실행되면서 getTodos action을 dispatch하여 초기 데이터를 가져옴
}, [dispatch])
return (
<div>
<h1>ToDo List</h1>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> {/* input 값이 변경되면 inputValue 상태를 업데이트 */}
<button onClick={handleAddTodo}></button> {/* 추가 버튼을 누르면 handleAddTodo 함수를 호출하여 새로운 todo를 추가 */}
<ul>
{/* todos state 배열을 map 함수를 이용하여 리스트 형태로 렌더링 */}
{todos.map((todo) => (
<li
key={todo.id} // key 값으로 각 todo의 id를 사용하여 React가 컴포넌트를 효율적으로 렌더링할 수 있도록 함
>
<span>{todo.title}</span> {/* todo의 title 값을 렌더링 */}
<div>
<span completed={todo.completed}>
{todo.completed ? 'Completed' : 'InComplete'} {/* todo의 completed 상태에 따라 Completed 또는 InComplete를 렌더링 */}
</span>
<button onClick={() => handleEditTodo(todo.id, todo.title, todo.completed)}>수정</button> {/* 수정 버튼을 누르면 handleEditTodo 함수를 호출하여 현재 todo를 수정 모드로 변경 */}
</div>
</li>
))}
</ul>
</div>
);
};
export default TodoList;
export const EDIT_TODO = 'EDIT_TODO';
export const EDIT_TODO_SUCCESS = 'EDIT_TODO_SUCCESS';
export const editTodo = (id, title, completed) => ({ // editTodo 액션 생성 함수
type: EDIT_TODO,
payload: { id, title, completed }
});
export const editTodoSuccess = (todo) => ({ // editTodoSuccess 액션 생성 함수
type: EDIT_TODO_SUCCESS,
payload: { todo },
});
const initialState = { // 초기 상태 정의
todos: [],
}
export default function todoReducer(state=initialState, action) {
switch (action.type) {
case EDIT_TODO_SUCCESS:
return { // todo 배열을 업데이트하기 위해 map 함수를 이용하여 기존 todo 배열을 순회하면서 해당 todo를 찾아 업데이트
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload.todo.id ? action.payload.todo : todo,
),
};
default:
return state;
}
}
import { takeLatest, call, put } from 'redux-saga/effects';
import {
EDIT_TODO,
EDIT_TODO_SUCCESS,
} from '../actions/todoAction';
import {
editTodoApi,
} from '../api/todoApi';
function* editTodo(action) { // editTodo saga 함수 정의
try {
const todo = yield call(editTodoApi, action.payload.id, action.payload.title, action.payload.completed); // editTodoApi 함수를 호출하여 수정된 todo 객체를 가져옴
yield put({ type: EDIT_TODO_SUCCESS, payload: { todo }}); // EDIT_TODO_SUCCESS 액션을 dispatch하여 수정된 todo 객체를 store에 저장
} catch (error) {
console.log(error);
}
}
export default function* rootSaga() { // rootSaga 함수 정의
yield takeLatest(EDIT_TODO, editTodo); // EDIT_TODO 액션이 발생할 때마다 editTodo saga 함수가 실행되도록 설정
}
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { deleteTodo } from "../actions/todoAction";
const TodoList = () => {
const dispatch = useDispatch(); // useDispatch hook을 이용하여 store의 dispatch 함수를 가져옴
const todos = useSelector((state) => state.todos.todos); // useSelector hook을 이용하여 store의 todos 상태 값을 가져옴
const handleDeleteTodo = (id) => { // todo 삭제 함수
dispatch(deleteTodo(id)); // deleteTodo action을 dispatch하여 해당 todo 객체를 삭제
}
useEffect(() => { // todos 상태가 변경될 때마다 getTodos action을 dispatch하여 최신 todos 상태를 가져옴
dispatch(getTodos());
}, [dispatch])
return (
<div>
<h1>ToDo List</h1>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> {/* input 값이 변경되면 inputValue 상태를 업데이트 */}
<button onClick={handleAddTodo}></button> {/* 추가 버튼을 누르면 handleAddTodo 함수를 호출하여 새로운 todo를 추가 */}
<ul>
{/* todos state 배열을 map 함수를 이용하여 리스트 형태로 렌더링 */}
{todos.map((todo) => (
<li
key={todo.id} // key 값으로 각 todo의 id를 사용하여 React가 컴포넌트를 효율적으로 렌더링할 수 있도록 함
>
<span>{todo.title}</span> {/* todo의 title 값을 렌더링 */}
<div>
<span completed={todo.completed}>
{todo.completed ? 'Completed' : 'InComplete'} {/* todo의 completed 상태에 따라 Completed 또는 InComplete를 렌더링 */}
</span>
<button onClick={() => handleEditTodo(todo.id, todo.title, todo.completed)}>수정</button> {/* 수정 버튼을 누르면 handleEditTodo 함수를 호출하여 현재 todo를 수정 모드로 변경 */}
<button onClick={() => handleDeleteTodo(todo.id)}>삭제</button> {/* 삭제 버튼을 누르면 handleDeleteTodo 함수를 호출하여 현재 todo를 삭제 */}
</div>
</li>
))}
</ul>
</div>
);
};
export default TodoList;
export const DELETE_TODO = 'DELETE_TODO';
export const DELETE_TODO_SUCCESS = 'DELETE_TODO_SUCCESS';
export const deleteTodo = (id) => ({ // todo 삭제 action 생성자 함수
type: DELETE_TODO,
payload: id
});
export const deleteTodoSuccess = (id) => ({ // todo 삭제 성공 action 생성자 함수
type: DELETE_TODO_SUCCESS,
payload: id
})
const initialState = {
todos: [],
}
export default function todoReducer(state=initialState, action) { // todoReducer 함수 정의
switch (action.type) {
case DELETE_TODO_SUCCESS: // DELETE_TODO_SUCCESS action 처리
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id), // 삭제된 todo 객체를 제외한 todos 배열을 반환
};
default:
return state;
}
}
import { takeLatest, call, put } from 'redux-saga/effects';
import {
DELETE_TODO,
DELETE_TODO_SUCCESS,
} from '../actions/todoAction';
import {
deleteTodoApi,
} from '../api/todoApi';
function* deleteTodo(action) { // deleteTodo saga 함수 정의
try {
yield call(deleteTodoApi, action.payload); // deleteTodoApi 함수 호출하여 해당 id를 가진 todo 객체 삭제
yield put({ type: DELETE_TODO_SUCCESS, payload: { id: action.payload }}); // DELETE_TODO_SUCCESS action dispatch
} catch (error) {
console.log(error);
}
}
export default function* rootSaga() { // rootSaga 함수 정의
yield takeLatest(DELETE_TODO, deleteTodo); // DELETE_TODO action이 dispatch 될 때, deleteTodo saga 함수 호출
}
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { toggleTodo } from "../actions/todoAction";
const TodoList = () => {
const dispatch = useDispatch(); // useDispatch 훅으로 dispatch 함수 사용
const [editId, setEditId] = useState(null); // 수정할 todo의 id 값을 저장하는 상태
const [completed, setCompleted] = useState(false); // 수정할 todo의 completed 값을 저장하는 상태
const [inputValue, setInputValue] = useState(""); // 새로운 todo의 title 값을 저장하는 상태
const todos = useSelector((state) => state.todos.todos); // todos state를 useSelector 훅을 이용하여 가져옴
const handleToggleTodo = (todo) => { // todo의 completed 값을 토글하는 함수
dispatch(toggleTodo(todo.id, !todo.completed, todo.title))
}
useEffect(() => { // 컴포넌트가 렌더링된 후에 todos 데이터를 불러오는 효과를 내기 위해 useEffect 훅 사용
dispatch(getTodos());
}, [dispatch])
return (
<div>
<h1>ToDo List</h1>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> {/* 새로운 todo의 title 값을 저장하는 input */}
<button onClick={handleAddTodo}>{editId ? '수정' : "추가"}</button> {/* 새로운 todo를 추가 또는 수정 버튼으로 변경 */}
<ul>
{todos.map((todo) => (
<li
key={todo.id}
>
<input type="checkbox" checked={todo.completed} onChange={() => handleToggleTodo(todo)} /> {/* todo의 completed 값을 변경하는 checkbox */}
<span>{todo.title}</span> {/* todo의 title 값을 렌더링 */}
<div>
<span completed={todo.completed}>
{todo.completed ? 'Completed' : 'InComplete'} {/* todo의 completed 상태에 따라 Completed 또는 InComplete를 렌더링 */}
</span>
<button onClick={() => handleEditTodo(todo.id, todo.title, todo.completed)}>수정</button> {/* 수정 버튼을 누르면 handleEditTodo 함수를 호출하여 현재 todo를 수정 모드로 변경 */}
<button onClick={() => handleDeleteTodo(todo.id)}>삭제</button> {/* 삭제 버튼을 누르면 handleDeleteTodo 함수를 호출하여 현재 todo를 삭제 */}
</div>
</li>
))}
</ul>
</div>
);
};
export default TodoList;
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const TOGGLE_TODO_SUCCESS = 'TOGGLE_TODO_SUCCESS';
export const toggleTodo = (id, completed, title) => ({ // toggleTodo action creator 함수 정의
type: TOGGLE_TODO, // action 객체의 type 값
payload: { id, completed, title } // action 객체의 payload 값
});
export const toggleTodoSuccess = (todo) => ({ // toggleTodoSuccess action creator 함수 정의
type: TOGGLE_TODO_SUCCESS, // action 객체의 type 값
payload: { todo } // action 객체의 payload 값
});
const initialState = { // 초기 상태 정의
todos: [], // todos 배열
}
export default function todoReducer(state=initialState, action) { // todoReducer 정의
switch (action.type) {
case TOGGLE_TODO_SUCCESS: // TOGGLE_TODO_SUCCESS case
return {
...state, // 기존 state 객체를 전개 연산자를 이용하여 복사
todos: state.todos.map((todo) => todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo), // todos 배열을 전개 연산자를 이용하여 복사하고, map 함수를 이용하여 todo.id가 action.payload.id와 같으면 completed 값을 반전시켜서 새로운 todo 객체를 생성하고, 그렇지 않으면 기존 todo 객체를 반환하여 배열을 업데이트
};
default:
return state; // 기존 상태를 반환
}
}
import { takeLatest, call, put } from 'redux-saga/effects';
import {
TOGGLE_TODO, // 할 일의 완료/미완료 상태를 전환하는 액션
TOGGLE_TODO_SUCCESS, // 할 일의 완료/미완료 상태를 전환하는 액션의 성공 상태
} from '../actions/todoAction';
import {
toggleTodoApi // 할 일의 완료/미완료 상태를 전환하는 API 함수
} from '../api/todoApi';
function* toggleTodo(action) {
try {
// API를 호출하여 todo 항목의 id, title, completed를 반환받음
const { title, id, completed} = yield call(toggleTodoApi, action.payload);
// TOGGLE_TODO_SUCCESS 액션을 dispatch 함
yield put({ type: TOGGLE_TODO_SUCCESS, payload: { title, id, completed }})
} catch (error) {
console.log(error)
}
}
// TOGGLE_TODO 액션을 감시하고, 액션이 발생하면 toggleTodo 함수를 실행함
export default function* rootSaga() {
yield takeLatest(TOGGLE_TODO, toggleTodo);
}
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, getTodos, editTodo, deleteTodo, toggleTodo } from "../actions/todoAction";
import styled from '@emotion/styled';
const Container = styled.div`
margin: 0 auto;
max-width: 600px;
`;
const Title = styled.h1`
font-size: 2rem;
text-align: center;
margin-top: 2rem;
margin-bottom: 2rem;
`;
const Input = styled.input`
font-size: 1rem;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 3px;
width: 100%;
margin-bottom: 1rem;
`;
const Button = styled.button`
font-size: 1rem;
padding: 0.5rem 1rem;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: #0062cc;
}
`
const TodoItemContainer = styled.ul`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-left: 1rem;
`
const TodoItem = styled.li`
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
border: 1px solid #ccc;
border-radius: 3px;
margin-bottom: 0.5rem;
`;
const TodoTitle = styled.span`
font-size: 1rem;
`;
const TodoStatus = styled.span`
font-size: 1rem;
font-weight: bold;
color: ${(props) => (props.completed ? '#28a745' : '#dc3545')};
`;
const EditButton = styled.button`
font-size: 1rem;
padding: 0.5rem 1rem;
background-color: #ffc107;
color: #212529;
border: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: #e0a800;
}
`;
const DeleteButton = styled.button`
font-size: 1rem;
padding: 0.5rem 1rem;
background-color: #dc3545;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: #c82333;
}
`;
const CheckBox = styled.input`
margin-right: 8px;
`
const TodoList = () => {
const dispatch = useDispatch();
const [editId, setEditId] = useState(null);
const [completed, setCompleted] = useState(false);
const [inputValue, setInputValue] = useState("");
const todos = useSelector((state) => state.todos.todos);
const handleAddTodo = () => {
if (!inputValue.trim()) {
return;
}
if (editId) {
dispatch(editTodo(editId, inputValue, completed));
setEditId(null);
} else {
dispatch(addTodo(inputValue, completed))
}
setInputValue('');
}
const handleEditTodo = (id, title, completed) => {
setEditId(id);
setInputValue(title);
setCompleted(completed || false);
}
const handleToggleTodo = (todo) => {
dispatch(toggleTodo(todo.id, !todo.completed, todo.title))
}
const handleDeleteTodo = (id) => {
dispatch(deleteTodo(id));
}
useEffect(() => {
dispatch(getTodos());
}, [dispatch])
return (
<Container>
<Title>ToDo List</Title>
<Input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<Button onClick={handleAddTodo}>{editId ? '수정' : "추가"}</Button>
<TodoItemContainer>
{todos.map((todo) => (
<TodoItem
key={todo.id}
>
<CheckBox type="checkbox" checked={todo.completed} onChange={() => handleToggleTodo(todo)} />
<TodoTitle>{todo.title}</TodoTitle>
<div>
<TodoStatus completed={todo.completed}>
{todo.completed ? 'Completed' : 'InComplete'}
</TodoStatus>
<EditButton onClick={() => handleEditTodo(todo.id, todo.title, todo.completed)}>수정</EditButton>
<DeleteButton onClick={() => handleDeleteTodo(todo.id)}>삭제</DeleteButton>
</div>
</TodoItem>
))}
</TodoItemContainer>
</Container>
);
};
export default TodoList;
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, getTodos, editTodo, deleteTodo, toggleTodo } from "../actions/todoAction";
import styled from '@emotion/styled';
// styled-components로 만든 컴포넌트들입니다.
const Container = styled.div`
margin: 0 auto;
max-width: 600px;
`;
const Title = styled.h1`
font-size: 2rem;
text-align: center;
margin-top: 2rem;
margin-bottom: 2rem;
`;
const Input = styled.input`
font-size: 1rem;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 3px;
width: 100%;
margin-bottom: 1rem;
`;
const Button = styled.button`
font-size: 1rem;
padding: 0.5rem 1rem;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: #0062cc;
}
`
const TodoItemContainer = styled.ul`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-left: 1rem;
`
const TodoItem = styled.li`
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
border: 1px solid #ccc;
border-radius: 3px;
margin-bottom: 0.5rem;
`;
const TodoTitle = styled.span`
font-size: 1rem;
`;
const TodoStatus = styled.span`
font-size: 1rem;
font-weight: bold;
color: ${(props) => (props.completed ? '#28a745' : '#dc3545')};
`;
const EditButton = styled.button`
font-size: 1rem;
padding: 0.5rem 1rem;
background-color: #ffc107;
color: #212529;
border: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: #e0a800;
}
`;
const DeleteButton = styled.button`
font-size: 1rem;
padding: 0.5rem 1rem;
background-color: #dc3545;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: #c82333;
}
`;
const CheckBox = styled.input`
margin-right: 8px;
`
const TodoList = () => {
const dispatch = useDispatch();
const [editId, setEditId] = useState(null); // 수정할 todo의 id를 저장할 상태
const [completed, setCompleted] = useState(false); // 수정할 todo의 completed 상태를 저장할 상태
const [inputValue, setInputValue] = useState(""); // 새로운 todo를 입력할 때 사용자의 입력 값을 저장하는 상태 변수
const todos = useSelector((state) => state.todos.todos); // store에서 todos 배열을 가져와서 사용하는 hook
const handleAddTodo = () => { // '추가' 버튼 클릭 시 실행되는 함수
if (!inputValue.trim()) { // 입력 값이 없으면 함수를 종료
return;
}
if (editId) { // editId 값이 있으면 수정 모드로 판단
dispatch(editTodo(editId, inputValue, completed)); // editTodo action을 dispatch하고, 기존 todo의 id, 수정된 title과 completed 값을 전달
setEditId(null); // editId를 초기화하여 추가 모드로 변경
} else {
dispatch(addTodo(inputValue, completed)) // addTodo action을 dispatch하고, 새로운 todo의 title과 completed 값을 전달
}
setInputValue(''); // 입력 값을 초기화
}
const handleEditTodo = (id, title, completed) => { // '수정' 버튼 클릭 시 실행되는 함수
setEditId(id); // 현재 수정하고 있는 todo의 id 값을 상태 변수에 저장
setInputValue(title); // 수정하고 있는 todo의 title 값을 input 상태 값에 설정하여 수정할 수 있도록 함
setCompleted(completed || false); // 수정하고 있는 todo의 completed 값을 상태 변수에 저장하여 수정할 수 있도록 함
}
const handleToggleTodo = (todo) => { // todo의 completed 값을 변경하는 함수
dispatch(toggleTodo(todo.id, !todo.completed, todo.title)) // toggleTodo action을 dispatch하고, todo의 id와 반전된 completed 값을 전달
}
const handleDeleteTodo = (id) => { // '삭제' 버튼 클릭 시 실행되는 함수
dispatch(deleteTodo(id)); // deleteTodo action을 dispatch하고, todo의 id 값을 전달
}
useEffect(() => { // 컴포넌트가 렌더링될 때 실행되는 useEffect hook
dispatch(getTodos()); // getTodos action을 dispatch하여 초기에 todos 값을 가져옴
}, [dispatch])
return (
<Container>
<Title>ToDo List</Title>
<Input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<Button onClick={handleAddTodo}>{editId ? '수정' : "추가"}</Button>
<TodoItemContainer>
{todos.map((todo) => (
<TodoItem
key={todo.id}
>
<CheckBox type="checkbox" checked={todo.completed} onChange={() => handleToggleTodo(todo)} />
<TodoTitle>{todo.title}</TodoTitle>
<div>
<TodoStatus completed={todo.completed}>
{todo.completed ? 'Completed' : 'InComplete'}
</TodoStatus>
<EditButton onClick={() => handleEditTodo(todo.id, todo.title, todo.completed)}>수정</EditButton>
<DeleteButton onClick={() => handleDeleteTodo(todo.id)}>삭제</DeleteButton>
</div>
</TodoItem>
))}
</TodoItemContainer>
</Container>
);
};
export default TodoList;
// 액션 타입 상수 선언
export const GET_TODOS = 'GET_TODOS';
export const GET_TODOS_SUCCESS = 'GET_TODOS_SUCCESS';
export const ADD_TODO = 'ADD_TODO';
export const ADD_TODO_SUCCESS = 'ADD_TODO_SUCCESS';
export const EDIT_TODO = 'EDIT_TODO';
export const EDIT_TODO_SUCCESS = 'EDIT_TODO_SUCCESS';
export const DELETE_TODO = 'DELETE_TODO';
export const DELETE_TODO_SUCCESS = 'DELETE_TODO_SUCCESS';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const TOGGLE_TODO_SUCCESS = 'TOGGLE_TODO_SUCCESS';
// 액션 생성자 함수 정의
export const getTodos = () => ({
type: GET_TODOS
});
export const getTodosSuccess = (todos) => ({
type: GET_TODOS_SUCCESS,
payload: todos,
})
export const addTodo = (title, completed) => ({
type: ADD_TODO,
payload: { title, completed }
});
export const addTodoSuccess = (todo) => ({
type: ADD_TODO_SUCCESS,
payload: { todo }
});
export const editTodo = (id, title, completed) => ({
type: EDIT_TODO,
payload: { id, title, completed }
});
export const editTodoSuccess = (todo) => (
{
type: EDIT_TODO_SUCCESS,
payload: { todo },
});
export const deleteTodo = (id) => ({
type: DELETE_TODO,
payload: id
});
export const deleteTodoSuccess = (id) => ({
type: DELETE_TODO_SUCCESS,
payload: id
})
export const toggleTodo = (id, completed, title) => ({
type: TOGGLE_TODO,
payload: { id, completed, title }
});
export const toggleTodoSuccess = (todo) => ({
type: TOGGLE_TODO_SUCCESS,
payload: { todo }
});
// 초기 상태 정의
const initialState = {
todos: [],
}
// 리듀서 함수 정의
export default function todoReducer(state=initialState, action) {
switch (action.type) {
case GET_TODOS_SUCCESS:
return { ...state, todos: action.payload.todos } // 받아온 todo 리스트로 상태 업데이트
case ADD_TODO_SUCCESS:
return { ...state, todos: [ ...state.todos, action.payload.todo ]}; // 추가된 todo를 리스트에 추가하여 상태 업데이트
case EDIT_TODO_SUCCESS:
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload.todo.id ? action.payload.todo : todo, // 수정된 todo를 포함하여 리스트를 업데이트
),
};
case DELETE_TODO_SUCCESS:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id), // 삭제된 todo를 제외한 리스트를 상태 업데이트
};
case TOGGLE_TODO_SUCCESS:
return {
...state,
todos: state.todos.map((todo) => todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo), // 토글된 todo를 포함하여 리스트를 업데이트
};
default:
return state; // 이외의 액션 타입에 대해서는 초기 상태 반환
}
}
import { takeLatest, call, put } from 'redux-saga/effects';
import {
GET_TODOS,
GET_TODOS_SUCCESS,
ADD_TODO,
ADD_TODO_SUCCESS,
EDIT_TODO,
EDIT_TODO_SUCCESS,
DELETE_TODO,
DELETE_TODO_SUCCESS,
TOGGLE_TODO,
TOGGLE_TODO_SUCCESS,
} from '../actions/todoAction';
import {
getTodosApi,
addTodoApi,
editTodoApi,
deleteTodoApi,
toggleTodoApi
} from '../api/todoApi';
// GET_TODOS 액션에 대한 비동기 API 콜을 처리하는 제너레이터 함수
function* getTodos() {
try {
// getTodosApi 비동기 함수 호출
const todos = yield call(getTodosApi);
// GET_TODOS_SUCCESS 액션 디스패치
yield put({ type: GET_TODOS_SUCCESS, payload: { todos }});
} catch (error) {
console.log(error);
}
}
// ADD_TODO 액션에 대한 비동기 API 콜을 처리하는 제너레이터 함수
function* addTodo(action) {
try {
// addTodoApi 비동기 함수 호출
const todo = yield call(addTodoApi, action.payload.title);
// ADD_TODO_SUCCESS 액션 디스패치
yield put({ type: ADD_TODO_SUCCESS, payload: { todo }});
} catch (error) {
console.log(error);
}
}
// EDIT_TODO 액션에 대한 비동기 API 콜을 처리하는 제너레이터 함수
function* editTodo(action) {
try {
// editTodoApi 비동기 함수 호출
const todo = yield call(editTodoApi, action.payload.id, action.payload.title, action.payload.completed);
// EDIT_TODO_SUCCESS 액션 디스패치
yield put({ type: EDIT_TODO_SUCCESS, payload: { todo }});
} catch (error) {
console.log(error);
}
}
// DELETE_TODO 액션에 대한 비동기 API 콜을 처리하는 제너레이터 함수
function* deleteTodo(action) {
try {
// deleteTodoApi 비동기 함수 호출
yield call(deleteTodoApi, action.payload);
// DELETE_TODO_SUCCESS 액션 디스패치
yield put({ type: DELETE_TODO_SUCCESS, payload: { id: action.payload }});
} catch (error) {
console.log(error);
}
}
// TOGGLE_TODO 액션에 대한 비동기 API 콜을 처리하는 제너레이터 함수
function* toggleTodo(action) {
try {
// toggleTodoApi 비동기 함수 호출
const { title, id, completed} = yield call(toggleTodoApi, action.payload);
// TOGGLE_TODO_SUCCESS 액션 디스패치
yield put({ type: TOGGLE_TODO_SUCCESS, payload: { title, id, completed }})
} catch (error) {
console.log(error)
}
}
// rootSaga 제너레이터 함수, 각각의 액션에 대한 제너레이터 함수 실행
export default function* rootSaga() {
yield takeLatest(GET_TODOS, getTodos);
yield takeLatest(ADD_TODO, addTodo);
yield takeLatest(EDIT_TODO, editTodo);
yield takeLatest(DELETE_TODO, deleteTodo);
yield takeLatest(TOGGLE_TODO, toggleTodo);
}
이렇게 Redux-saga로 TodoList를 만들어봤다. 사실 다음 프로젝트에서 타입스크립트 기반의 Redux-saga를 이용해서 로그인 및 회원가입 기능을 3일 만에 구현해야한다. 하지만 Redux-saga 의 구조 자체를 몰랐기 때문에 이렇게 작업을 했다.
이제는 typescript 기반의 Redux-saga를 도전해야할 차례이다. 한동안 프로젝트에 집중한다고 블로그를 게시할 수 있을지 모르겠다. 하지만 이게 다 나의 포트폴리오라고 생각하고 쓰려고 노력해야겠다!