리액트 강좌: To Do 앱 useReducer 사용 앱 업그레이드

리액트는 현대 웹 개발의 중요한 도구로 자리잡았습니다. 특히, 상태 관리에 있어 다양한 방법이 있지만, 그 중 useReducer는 더 복잡한 상태 로직을 효과적으로 관리할 수 있도록 돕는 React 훅입니다. 본 강좌에서는 기본적인 To Do 앱을 사용하여 useReducer를 활용하는 방법을 구체적으로 다루겠습니다.

1. 프로젝트 구조

우선, 프로젝트의 기본 구조를 살펴보겠습니다. 일반적으로 To Do 앱의 구조는 다음과 같이 구성됩니다:


/to-do-app
|-- /src
|   |-- /components
|   |   |-- TodoItem.js
|   |   |-- TodoList.js
|   |   └-- TodoForm.js
|   |
|   |-- App.js
|   └-- index.js
|-- package.json
|-- README.md
        

위 구조에서 TodoItem은 개별 할 일 항목을 나타내고, TodoList는 모든 할 일 목록을 보여줍니다. TodoForm을 사용하여 새로운 할 일을 추가합니다.

2. useReducer 소개

useReducer는 복잡한 상태를 다룰 때 유용하며, 다음과 같은 형태로 사용할 수 있습니다:


const [state, dispatch] = useReducer(reducer, initialState);
        

state: 현재 상태를 나타내며, dispatch: 상태 업데이트를 위한 함수입니다. reducer는 상태를 어떻게 변화시킬지를 정의하는 함수입니다.

3. 기본 To Do 앱 구현

먼저 기본 To Do 앱을 구현해 보겠습니다. 아래 코드는 간단한 리액트 앱으로, 사용자가 할 일을 추가하고 목록을 보여주는 기능을 포함합니다.


import React, { useState } from 'react';

const App = () => {
    const [todos, setTodos] = useState([]);
    const [input, setInput] = useState('');

    const addTodo = () => {
        if (input.trim() !== '') {
            setTodos([...todos, { text: input, completed: false }]);
            setInput('');
        }
    };

    const toggleTodo = (index) => {
        setTodos(todos.map((todo, i) => 
            i === index ? { ...todo, completed: !todo.completed } : todo
        ));
    };

    return (
        

나의 To Do 리스트

setInput(e.target.value)} placeholder="할 일을 입력하세요" />
    {todos.map((todo, index) => (
  • toggleTodo(index)} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}> {todo.text}
  • ))}
); }; export default App;

4. useReducer로 상태 관리하기

이제 위의 코드를 useReducer로 개선해보겠습니다. 상태를 복잡하게 관리할 필요가 있을 때, useReducer가 더 유용할 수 있습니다.


import React, { useReducer } from 'react';

const initialState = { todos: [] };

const reducer = (state, action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return { 
                ...state, 
                todos: [...state.todos, { text: action.payload, completed: false }] 
            };
        case 'TOGGLE_TODO':
            return {
                ...state,
                todos: state.todos.map((todo, index) =>
                    index === action.payload ? { ...todo, completed: !todo.completed } : todo
                ),
            };
        default:
            return state;
    }
};

const App = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    const [input, setInput] = React.useState('');

    const addTodo = () => {
        if (input.trim() !== '') {
            dispatch({ type: 'ADD_TODO', payload: input });
            setInput('');
        }
    };

    return (
        

나의 To Do 리스트 (useReducer)

setInput(e.target.value)} placeholder="할 일을 입력하세요" />
    {state.todos.map((todo, index) => (
  • dispatch({ type: 'TOGGLE_TODO', payload: index })} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}> {todo.text}
  • ))}
); }; export default App;

5. useReducer의 장점

useReducer를 사용하면, 다음과 같은 장점이 있습니다:

  • 복잡한 상태 로직: 여러 상태 전환을 독립적인 행동으로 관리할 수 있어 복잡한 상태 로직을 명확하게 유지할 수 있습니다.
  • 중앙 집중식 관리: 여러 상태 업데이트가 있을 때, 하나의 reducer 함수로 모든 상태 변경을 처리할 수 있습니다.
  • 성능 향상: 불변성 유지가 용이하여 성능 최적화에 도움이 됩니다.

6. 컴포넌트 분리

이제 App 컴포넌트를 더 작은 컴포넌트로 나누어 코드의 가독성을 높여 보겠습니다. TodoList와 TodoForm 컴포넌트를 만들어보겠습니다.


const TodoList = ({ todos, toggleTodo }) => (
    
    {todos.map((todo, index) => (
  • toggleTodo(index)} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} > {todo.text}
  • ))}
); const TodoForm = ({ addTodo, input, setInput }) => (
setInput(e.target.value)} placeholder="할 일을 입력하세요" />
);

7. 최종 코드


import React, { useReducer, useState } from 'react';

const initialState = { todos: [] };

const reducer = (state, action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return { 
                ...state, 
                todos: [...state.todos, { text: action.payload, completed: false }] 
            };
        case 'TOGGLE_TODO':
            return {
                ...state,
                todos: state.todos.map((todo, index) =>
                    index === action.payload ? { ...todo, completed: !todo.completed } : todo
                ),
            };
        default:
            return state;
    }
};

const TodoList = ({ todos, toggleTodo }) => (
    
    {todos.map((todo, index) => (
  • toggleTodo(index)} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} > {todo.text}
  • ))}
); const TodoForm = ({ addTodo, input, setInput }) => (
setInput(e.target.value)} placeholder="할 일을 입력하세요" />
); const App = () => { const [state, dispatch] = useReducer(reducer, initialState); const [input, setInput] = useState(''); const addTodo = () => { if (input.trim() !== '') { dispatch({ type: 'ADD_TODO', payload: input }); setInput(''); } }; return (

나의 To Do 리스트 (useReducer)

dispatch({ type: 'TOGGLE_TODO', payload: index })} />
); }; export default App;

8. 마치며

이번 강좌에서는 To Do 앱을 통해 useReducer를 적용하는 방법을 알아보았습니다. useReducer는 복잡한 상태 로직을 간결하게 관리할 수 있도록 도와주며, 가독성과 유지보수성 또한 높입니다. 더 나아가 여러 컴포넌트로 구성하여 코드의 구조를 개선할 수 있었습니다. 이러한 패턴은 더욱 복잡한 애플리케이션에서도 유용하게 사용될 수 있습니다. 다음 시간에는 useContext를 활용하여 전역 상태 관리에 대해 알아보겠습니다.