본문 바로가기

카테고리 없음

[React] React로 할 일 관리 앱 만들기: 8장 - 비동기 작업과 미들웨어

반응형

 

안녕하세요! 이번 포스트에서는 우리의 할 일 관리 앱에 비동기 작업 처리 기능을 추가하고, Redux Thunk 미들웨어를 사용하는 방법을 알아보겠습니다. 이를 통해 서버와의 통신을 시뮬레이션하고, 실제 애플리케이션에서 API 호출을 어떻게 처리할 수 있는지 배워보겠습니다.

1. Redux Thunk 설치하기

먼저 Redux Thunk를 설치합니다. 터미널에서 다음 명령어를 실행하세요:

npm install redux-thunk

2. 가짜 API 서비스 만들기

실제 서버 없이 비동기 작업을 시뮬레이션하기 위해 가짜 API 서비스를 만들어 보겠습니다. src 폴더에 api 폴더를 만들고, 그 안에 todoApi.js 파일을 생성합니다:

// src/api/todoApi.js

// 가짜 지연 함수
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

// 가짜 데이터베이스
let todos = [];

export const fetchTodos = async () => {
  await delay(500); // 0.5초 지연
  return [...todos];
};

export const addTodoApi = async (text) => {
  await delay(500);
  const newTodo = { id: Date.now(), text, completed: false };
  todos.push(newTodo);
  return newTodo;
};

export const toggleTodoApi = async (id) => {
  await delay(500);
  const todo = todos.find(t => t.id === id);
  if (todo) {
    todo.completed = !todo.completed;
    return { ...todo };
  }
  throw new Error('Todo not found');
};

export const deleteTodoApi = async (id) => {
  await delay(500);
  const index = todos.findIndex(t => t.id === id);
  if (index !== -1) {
    todos = todos.filter(t => t.id !== id);
    return id;
  }
  throw new Error('Todo not found');
};

export const editTodoApi = async (id, text) => {
  await delay(500);
  const todo = todos.find(t => t.id === id);
  if (todo) {
    todo.text = text;
    return { ...todo };
  }
  throw new Error('Todo not found');
};

3. Redux Thunk 액션 생성하기

이제 todoSlice.js 파일을 수정하여 비동기 액션을 추가합니다:

// src/store/todoSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { fetchTodos, addTodoApi, toggleTodoApi, deleteTodoApi, editTodoApi } from '../api/todoApi';

export const fetchTodosAsync = createAsyncThunk(
  'todos/fetchTodos',
  async () => {
    const response = await fetchTodos();
    return response;
  }
);

export const addTodoAsync = createAsyncThunk(
  'todos/addTodo',
  async (text) => {
    const response = await addTodoApi(text);
    return response;
  }
);

export const toggleTodoAsync = createAsyncThunk(
  'todos/toggleTodo',
  async (id) => {
    const response = await toggleTodoApi(id);
    return response;
  }
);

export const deleteTodoAsync = createAsyncThunk(
  'todos/deleteTodo',
  async (id) => {
    await deleteTodoApi(id);
    return id;
  }
);

export const editTodoAsync = createAsyncThunk(
  'todos/editTodo',
  async ({ id, text }) => {
    const response = await editTodoApi(id, text);
    return response;
  }
);

const todoSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    status: 'idle',
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodosAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchTodosAsync.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchTodosAsync.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      })
      .addCase(addTodoAsync.fulfilled, (state, action) => {
        state.items.push(action.payload);
      })
      .addCase(toggleTodoAsync.fulfilled, (state, action) => {
        const todo = state.items.find(todo => todo.id === action.payload.id);
        if (todo) {
          todo.completed = action.payload.completed;
        }
      })
      .addCase(deleteTodoAsync.fulfilled, (state, action) => {
        state.items = state.items.filter(todo => todo.id !== action.payload);
      })
      .addCase(editTodoAsync.fulfilled, (state, action) => {
        const todo = state.items.find(todo => todo.id === action.payload.id);
        if (todo) {
          todo.text = action.payload.text;
        }
      });
  },
});

export default todoSlice.reducer;

4. 컴포넌트 수정하기

이제 각 컴포넌트를 수정하여 새로운 비동기 액션을 사용하도록 합니다.

TodoList 컴포넌트

// src/components/TodoList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchTodosAsync } from '../store/todoSlice';
import TodoForm from './TodoForm';
import TodoItem from './TodoItem';
import TodoFilters from './TodoFilters';
import TodoStats from './TodoStats';
import './TodoList.css';

function TodoList() {
  const dispatch = useDispatch();
  const { items: todos, status, error } = useSelector(state => state.todos);
  const [filter, setFilter] = React.useState('all');

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchTodosAsync());
    }
  }, [status, dispatch]);

  const filteredTodos = todos.filter((todo) => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  if (status === 'loading') {
    return <div>로딩 중...</div>;
  }

  if (status === 'failed') {
    return <div>에러: {error}</div>;
  }

  return (
    <div className="todo-app">
      <h2>할 일 관리 앱</h2>
      <TodoForm />
      <TodoFilters filter={filter} setFilter={setFilter} />
      <ul className="todo-list">
        {filteredTodos.map((todo) => (
          <TodoItem key={todo.id} todo={todo} />
        ))}
      </ul>
      <TodoStats />
    </div>
  );
}

export default TodoList;

TodoForm 컴포넌트

// src/components/TodoForm.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodoAsync } from '../store/todoSlice';

function TodoForm() {
  const [inputValue, setInputValue] = useState('');
  const dispatch = useDispatch();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim() !== '') {
      dispatch(addTodoAsync(inputValue));
      setInputValue('');
    }
  };

  return (
    <form className="todo-form" onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="새로운 할 일을 입력하세요"
      />
      <button type="submit">추가</button>
    </form>
  );
}

export default TodoForm;

TodoItem 컴포넌트

// src/components/TodoItem.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { toggleTodoAsync, deleteTodoAsync, editTodoAsync } from '../store/todoSlice';

function TodoItem({ todo }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editValue, setEditValue] = useState(todo.text);
  const dispatch = useDispatch();

  const handleEdit = () => {
    dispatch(editTodoAsync({ id: todo.id, text: editValue }));
    setIsEditing(false);
  };

  return (
    <li className="todo-item">
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => dispatch(toggleTodoAsync(todo.id))}
      />
      {isEditing ? (
        <input
          type="text"
          value={editValue}
          onChange={(e) => setEditValue(e.target.value)}
          onBlur={handleEdit}
          autoFocus
        />
      ) : (
        <span
          style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          onDoubleClick={() => setIsEditing(true)}
        >
          {todo.text}
        </span>
      )}
      <button onClick={() => dispatch(deleteTodoAsync(todo.id))}>삭제</button>
    </li>
  );
}

export default TodoItem;

TodoStats 컴포넌트

TodoStats 컴포넌트는 변경할 필요가 없습니다.

5. 스토어 설정 수정하기

src/store/store.js 파일을 수정하여 Redux Thunk 미들웨어를 추가합니다:

// src/store/store.js
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from './todoSlice';

const store = configureStore({
  reducer: {
    todos: todoReducer,
  },
  // Redux Toolkit의 configureStore는 기본적으로 Redux Thunk를 포함합니다.
});

export default store;

실행 결과

이제 npm start 명령어로 앱을 실행하면, 비동기 작업을 처리하는 할 일 관리 앱을 볼 수 있습니다. 각 작업(추가, 삭제, 수정, 완료 상태 변경)에 약간의 지연이 있을 것입니다. 이는 실제 서버 통신을 시뮬레이션한 것입니다.

마무리

이번 챕터에서는 Redux Thunk를 사용하여 비동기 작업을 처리하는 방법을 배웠습니다. 이를 통해 얻을 수 있는 장점은 다음과 같습니다:

  1. 비동기 로직 분리: 컴포넌트에서 비동기 로직을 분리하여 관리할 수 있습니다.
  2. 상태 업데이트 일관성: 서버의 응답을 기반으로 상태를 업데이트하여 일관성을 유지할 수 있습니다.
  3. 에러 처리: 비동기 작업의 실패를 쉽게 처리하고 사용자에게 알릴 수 있습니다.
  4. 로딩 상태 관리: 데이터를 불러오는 동안 로딩 상태를 쉽게 관리할 수 있습니다.

Redux Thunk를 사용하면서 느낀 점이 있나요? 비동기 작업 처리가 더 쉬워졌나요, 아니면 더 복잡해졌나요? 여러분의 경험을 댓글로 공유해 주세요!

다음 챕터에서는 단위 테스트 작성 방법에 대해 알아보겠습니다. React, Redux, 그리고 비동기 작업을 포함한 애플리케이션을 어떻게 테스트할 수 있는지 살펴볼 예정입니다. React와 Redux로 앱을 개발하면서 궁금한 점이 있다면 언제든 질문해 주세요!

반응형