본문 바로가기

React

state를 reducer로 작성하기

참고) https://react.dev/learn/extracting-state-logic-into-a-reducer

 

Extracting State Logic into a Reducer – React

The library for web and native user interfaces

react.dev


리액트 프로젝트 규모가 커지다보면, useState만으로 상태를 관리하기에 꽤나 까다롭다.
어떠한 반응이 일어나기 위해 setter 함수를 호출하기까지의 로직을 하나의 이벤트 핸들러에 작성해야하는데,
코드량이 많아지면서 가독성은 떨어지게 된다.
로직을 한 눈에 알아보기 힘들어 오류가 발생하면 어디서 일어났는지 찾기도 힘들어진다.

이러한 가독성과 디버깅 간편화를 위해 컴포넌트 내에 로직을 몰아넣기 대신,
컴포넌트 내부에선 '무엇이 일어났는지'에 대해서만 정의하고(action의 type)
action별 로직은 reducer라는 단일 함수에 정의해, 해당 함수를 컴포넌트 외부로 분리시킬 수 있다.

 


useState를 useReducer로 리팩토링하기

 

다음 세 가지 단계를 통해 useState를 이용한 코드를 useReducer 코드로 바꿀 수 있다.

1. state를 설정하는 것에서 action을 dispatch 함수로 전달하는 것으로 바꾸기.
2. reducer 함수 작성하기.
3. 컴포넌트에서 reducer 사용하기.

 

 

function handleAddTask(text) {
	setTasks([...tasks, {
		id: nextId++,
		text: text,
		done: false
	}]);
}

function handleChangeTask(task) {
		setTasks(tasks.map(t => {
			if (t.id === task.id) {
            	return task;
            }	else {
            	return t;
            }	
		}));
}

function handleDeleteTask(taskId) {
	setTasks(
    	tasks.filter(t => t.id !== taskId)
    );
}

 

위 코드는 세 가지 타입의 action으로 구별될 수 있다.

- "Add"를 눌렀을 때: handleAddTask(text)
- "Save"를 눌렀을 때: handleChangeTask(task)

- "Delete"를 눌렀을 때: handleDeleteTask(task)

 

1. state를 설정하는 것에서 action을 dispatch 함수로 전달하는 것으로 바꾸기.

 

이벤트 핸들러에서 state 업데이트를 통해 'task를 설정'하는 것이 아니라,

'task를 추가/변경/삭제'하는 action을 전달한다.

dispatch 함수"action"이라는 객체를 넣어준다.

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
    });
}

 

action 객체는 형태에 자유롭지만,

일반적으로 '어떤 상황이 발생하는지'에 대한 최소한의 정보를 담고있다.

 

보통 type이라는 필드에 발생한 일을 값으로 담아서 전달한다.

이에 따라 사용자의 의도를 명확하게 파악할 수 있다.

 

2. reducer 함수 작성하기

 

state 설정과 관련된 로직은 reducer 함수에 적는다.

현재의 state 값action 객체를 인자로 받으며, 다음 state 값을 반환한다.

처음 코드의 이벤트 핸들러에 있던 로직을 reducer함수에 정의하자. 

 

function tasksReducer(tasks, action) {
	switch (action.type) {
    	case 'added':
            return [...tasks, {
                id: action.id,
                text: action.text,
                done: false
            }];        
    	}
        case 'changed': {
        	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}`);
        }
    }

 

reducer 함수는 컴포넌트 외부에서 선언할 수 있다. 

또한 reducer 함수 안에서는 swithch문을 사용하는 게 일반적이다.

이에 따라 가독성을 높인다.

 

3. 컴포넌트에서 reducer 사용하기

 

const [tasks, setTasks] = useState(initialTasks);

 

위를 아래와 같이 바꾸었다.

const [tasks, dispatch] = useReducer(taskReducer, initailTasks);

 

useReducer 훅은 reducer함수, 초기 state 값을 인자로 받아,

state를 담을 수 있는 값dispatch 함수(사용자의 action을 reducer 함수에게 “전달하게 될”)를 반환한다.

 


Immer로 더 간결한 Reducer 작성하기

 

 

useImmerReducer를 통해 reducer를 더 간결하게 작성하자.

 

import { useImmerReducer } from 'use-immer';

function tasksReducer(draft, action) {
  switch (action.type) {
    case 'added': {
      draft.push({
        id: action.id,
        text: action.text,
        done: false
      });
      break;
    }
    case 'changed': {
      const index = draft.findIndex(t =>
        t.id === action.task.id
      );
      draft[index] = action.task;
      break;
    }
    case 'deleted': {
      return draft.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

export default function TaskApp() {
  const [tasks, dispatch] = useImmerReducer(
    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
    });
  }

// 생략

}

 

useImmerReducer는

state 대신 draft 객체를 reducer 함수에 전달하므로, 

 새로운 state 값을 반환할 필요 없이 state를 변형할 수 있다.