본문 바로가기
🌳 React

[React] useReducer로 정보 추가, 수정, 삭제 기능 만들어보자.

by ._.sori 2026. 1. 14.

 

 

리액트에서 제공하는 설명을 보면
useReducer를 이용하여
정보 삭제와 수정 기능을 만든 예제를 볼 수 있다.
어떻게 만든건지 알아보자!



1. useReducer

  • useReducer는 컴포넌트의 최상위에 호출하고 reducer를 이용해 state를 관리한다.
  • 상태를 바꾸는 규칙(로직)을 한곳에 모아두고 이벤트(액션)가 오면 그 규칙대로 상태를 업데이트한다.
  • useState는 상태를 하나만 바꿀 때 쓰기 좋다면, useReducer은 상태가 복잡해질 때 사용하기 좋다.

 

 

 


 

 

 

 

2. useReducer를 이용한 정보 추가와 수정, 삭제 기능 예제

 

[과정]

  • Add 혹은 Edit 혹은 Delete 이벤트가 발생
  • dispatch(action) 호출
  • Reducer 실행
  • State 업데이트
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );
해당 코드에서 useReducer은 두 개의 매개변수를 가진다. state가 어떻게 업데이트 되는 지정하는 Reducer 함수이다. state와 action을 인수로 받아야하고, 다음 state를 반환한다. initalTasks는 초기 state가 계산되는 값이다.

useReducer은 2개의 엘리먼트로 구성된 배열을 반환다. tasks는 현재 state로 최초에는 사용자가 제공한 초기 state로 초기화된다. dispatch는 상호작용에 대응하여 state를 변경한다. 즉, state를 새로운 값으로 업데이트하고 리렌더링을 일으킨다. 

 

 

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }
...
사용자가 버튼을 누르면 각 버튼에 맞는 기능이 수행될 것이다. 만약 사용자가 Add 버튼을 눌렀다고 가정해보자. 그러면 handleAddTask 함수가 시행될 것이다. 이 함수 안을 보면 dispatch가 있다. 사용자가 Add 버튼을 누르면 dispatch는 이 기능이 무엇인지 들어있는 액션 객체를 들고 taskReducer로 향한다. 이때 action이 {type: 'added', id: nextId++, text: text}가 된다.

 

 

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
React는 taskReducer를 실행시키며, 현재 tasks랑 방금 온 action을 taskReducer로 넘겨준다. action의 type을 꺼내 맞는 case를 확인한다. 사용자가 Add 버튼을 눌렀다면 type: 'added'가 넘겨질테니 그에 맞는 case가 실행된다. 

case 'added'를 보면 [...tasks]를 사용했다. [...tasks]는 자바스크립트의 spread 연산자이다. 짧게 설명하면 원래의 배열 내용을 가지고 가면서 그 안에 요소를 더 추가하여 새로운 배열을 만든다.(약간 복사 후 내용 추가한 느낌?) 이 spread 연산자 부분은 나중에 더 자세히 살펴보기로 하고 왜 이렇게 코드를 작성했는지를 먼저 알아보겠다. React는 기존의 tasks 배열이 수정되었는지 확인 할 때  주소값(참조값)이 바뀌었는지 확인한다. 만약 직접 수정으로 tasks.push(newTask)를 적었다면 배열의 내용은 바뀌었지만, 메모리 주소는 그대로이기 때문에 React가 배열을 새로 그리지 않게된다. 그래서 spread 연산자를 사용해야 추가된 정보가 화면에 그려진다.

case 'changed'는 tasks.map( )을 사용했다. action의 아이디가 같다면, 새로 전달 받은 action.task로 교체한다. 그러나 아이디가 다르다면 원래 내용을 그대로 유지한다. 이해를 돕기 위해 추가 설명을 해보겠다. 만약에 { id: 0, text: '나무'} {id: 1, text: '고래'} {id: 2, text: '바다'}가 있다고 가정하자. 여기서 id: 1의 text가 '돌고래'로 변경되었다면, 첫번째 바퀴에서 id: 0번은 action.task.id와 다르기 때문에 넘어갈 것이다. 그래서 return t인 원래 갖고 있던 0번의 객체 t를 그대로 리턴시킨다. 그다음 바퀴에서 id: 1이 들어온 action.task.id와 같아서 action.task를 리턴한다.

case 'deleted'는 tasks.filter를 사용했다. filter 함수는 조건식에 맞는 것만 놔두고 조건식에 맞지 않으면 제외시킨다. 그렇게 삭제 버튼을 누른 task의 id와 비교하여 같지 않은 것들은 놔두고 같은 것만 없앤다.

 

 

 


 

 

 

3. 전체 코드

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false }
];

 

 

- useReducer

 

useReducer – React

The library for web and native user interfaces

ko.react.dev

 

 

 


 

 

 

4. 초기 state 재생성 방지

 

1.

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, createInitialState(username));
  // ...

vs

2.

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, username, createInitialState);
  // ...
두 코드가 있다. 두 코드의 차이점은 인수를 2개 갖느냐, 3개 갖느냐이다. 1번 코드는 두번째 인자에 직접 함수를 썼다. 위의 코드와 같은 형태인데 이런 경우 리렌더링 때마다 자바스크립트가 무조건 createInitialState 함수를 실행하고 지나간다. 위의 코드는 함수가 크지 않아서 문제가 없었지만, 함수가 큰 경우 효율이 떨어지게 된다.

그래서 2번 코드처럼 인수를 3개로 만드는 방법이 있다. 그러면 자바스크립트는 createIntialState라는 함수 이름만 전달하고 실행하진 않는다. 리렌더링 때 실행하지 않고, 첫 렌더링 때 한 번만 실행하기 때문에 매우 효율적이다.