본문 바로가기

FrontEnd

새로운 리액트 공식문서로 배우는 Reducer

반응형

원문 : https://beta.reactjs.org/learn/extracting-state-logic-into-a-reducer

 

Extracting State Logic into a Reducer

A JavaScript library for building user interfaces

beta.reactjs.org

Reducer

reducer의 명칭은 배열의 reduce(누적) 연산에서 비롯되었습니다.

reduce는 전체 배열을 다른 값으로 누적(reduce)합니다.

  • 우리의 리듀서는 지금까지 변화해 온 상태와 액션을 취하고 다음 상태를 반환합니다.
  • 우리의 리듀서는 시간이 지남에 따라 액션을 상태로 누적합니다.

리듀서는 상태 관리 로직을 추출하기 위해 사용합니다.

useReducer를 사용하면 업데이트 로직과 이벤트 핸들러에서 발생한 액션을 명확하게 구분할 수 있습니다.

즉, 이벤트 핸들러의 변경을 예방합니다.

  • useState를 useReducer로 리팩토링하는 방법
  • useReducer의 사용사례
  • useReducer를 잘 쓰는 법

Reducer는 상태를 처리하는 다른 방법입니다.

다음 세 단계로 useState에서 useReducer로 마이그레이션할 수 있습니다.

  • state setter > action dispatcher
  • reducer function 작성
  • 컴포넌트에서 reducer 사용

Step 1: Move from setting state to dispatching actions

아래와 같이 이벤트 핸들러를 전부 setter로 작성하였습니다.

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

리듀서로 상태를 관리하는 것은 상태를 직접 설정하는 것과 약간 다릅니다.
상태를 설정하여 React에 "할 일"을 알려주는 대신 (what to do)
이벤트 핸들러에서 "액션"을 전달하여 "사용자가 방금 한 일(what the user just did)"을 말해줍니다.
(상태 업데이트 로직은 다른 곳에 있을 것입니다!)
이벤트 핸들러를 통해 "테스크 데이터를 set하라는 메세지" 대신
"작업 추가/변경/삭제" 액션을 전달합니다. 이것은 사용자의 의도를 더 잘 설명합니다.
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
  });
}

디스패치에 전달하는 객체를 "액션"이라고 합니다.
function handleDeleteTask(taskId) {
  dispatch(
    // "action" object:
    {
      type: 'deleted',
      id: taskId
    }
  );
}

그냥 JavaScript 객체입니다.

일반적으로 발생한 일에 대한 최소한의 정보를 포함해야 합니다. 


컨벤션

액션 객체는 모든 모양을 가질 수 있습니다.

관례에 따라 발생한 일을 설명하는 문자열 타입을 설정하고 다른 필드에 추가 정보를 전달하는 것이 일반적입니다.

타입은 컴포넌트에 따라 다르므로 이 예에서는 'added' 또는 'added_task'가 적합합니다.

어떤 일이 발생했는지를 설명하는 이름을 지정합니다!

dispatch({
  // specific to component
  type: 'what_happened',
  // other fields go here
});

Step 2: Write a reducer function

리듀서 함수는 상태 로직을 넣을 위치입니다. 현재 상태와 액션 개체의 두 아규먼트(인자)를 사용하여 다음 상태를 반환합니다.
function yourReducer(state, action) {
  // return next state for React to set
}
React는 리듀서에서 반환한 다음 상태로 상태를 설정합니다.
이 예제에서 이벤트 핸들러에서 리듀서 함수로 상태 설정 로직을 이동하려면 다음을 수행합니다.
// 이전 상태 > 액션 > 다음 상태
function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    return [...tasks, {
      id: action.id,
      text: action.text,
      done: false
    }];
  } else if (action.type === 'changed') {
    return tasks.map(t => {
      if (t.id === action.task.id) {
        return action.task;
      } else {
        return t;
      }
    });
  } else if (action.type === 'deleted') {
    return tasks.filter(t => t.id !== action.id);
  } else {
    throw Error('Unknown action: ' + action.type);
  }
}
리듀서 함수는 상태를 인수로 취하기 때문에 컴포넌트 외부에서 선언할 수 있습니다.
이렇게 하면 들여쓰기 수준이 줄어들고 코드를 더 쉽게 읽을 수 있습니다.
리듀서는 심지어 다른 파일로 옮길 수도 있습니다.

컨벤션

위의 코드는 if/else 문을 사용하지만 reducer 내부에서 switch 문을 사용하는 것이 관례입니다.
결과는 같지만 switch 문을 한 눈에 읽는 것이 더 쉬울 수 있습니다. 이 문서의 나머지 부분에서 다음과 같이 사용할 것입니다.
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);
    }
  }
}
다른 케이스 내부에 선언된 변수가 서로 충돌하지 않도록 각 케이스 블록을 {  } (중괄호로_ 묶는 것이 좋습니다.
또한 케이스는 일반적으로 return으로 끝나야 합니다.
return 하는 것을 잊어버리면 코드가 다음 케이스로 "떨어져(fall through)" 실수로 이어질 수 있습니다!
아직 switch 문에 익숙하지 않다면 if/else를 사용하는 것이 좋습니다.

Step 3: Use the reducer from your component

마지막으로 useState를 useReducer로 변경합니다.

const [tasks, setTasks] = useState(initialTasks);
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer Hook은 두 개의 인수를 취합니다.
  • reducer함수
  • 초기 상태

그리고 다음을 반환합니다.

  • 상태
  • 디스패치 함수(액션을 리듀서에 디스패치(전파) 하는 데 사용)
이와 같이 관심사를 분리하면 컴포넌트 로직을 더 쉽게 읽을 수 있습니다.
이제 이벤트 핸들러는 액션을 디스패치하여 발생한 사건(액션)만 지정하고
리듀서 함수는 이에 대한 응답으로 상태가 업데이트되는 방식을 결정합니다.
 
 

Comparing useState and useReducer

코드 크기

  • 일반적으로 useState를 사용하면 초기에 적은 코드를 작성합니다.
  • useReducer를 사용하면 리듀서 함수와 디스패치 작업을 모두 작성해야 합니다.
  • 그러나 useReducer는 이벤트 핸들 코드를 줄이는 데 도움이 될 수 있습니다.

가독성

  • useState는 상태 업데이트가 단순할 때 매우 읽기 쉽습니다..
  • useReducer를 사용하면 업데이트 로직과 이벤트 핸들러에서 발생한 일을 명확하게 구분할 수 있습니다.

디버깅

useState에 버그가 있는 경우 상태가 잘못 설정된 위치와 이유를 파악하기 어려울 수 있습니다.
useReducer를 사용하면 콘솔 로그를 리듀서에 추가하여 모든 상태 업데이트와 발생한 이유(어떤 액션으로 인한 업데이트인지)를 볼 수 있습니다.
각 액션이 정확하면 실수가 리듀서 로직 자체에 있음을 알 수 있습니다.
그러나 useState보다 더 많은 코드를 단계별로 실행해야 합니다.

테스팅

리듀서는 컴포넌트에 의존하지 않는 순수 함수입니다. 즉, 별도로 내보내고 테스트할 수 있습니다.
일반적으로 보다 현실적인 환경에서 컴포넌트를 테스트하는 것이 가장 좋지만 복잡한 상태 업데이트 논리의 경우
리듀서가 특정 초기 상태 및 액션에 대해 특정 상태를 반환한다고 주장하는 것이 유용할 수 있습니다.
 
일부 컴포넌트에서 잘못된 상태 업데이트로 인해 버그가 자주 발생하고
해당 코드에 더 많은 구조를 도입하려는 경우 리듀서를 사용하는 것이 좋습니다. 

Writing reducers well

리듀서를 작성할 때 다음 두 가지 팁을 염두에 두십시오.
  • 리듀서는 순수해야 합니다.
상태 업데이터 기능과 유사하게 리듀서는 렌더링 중에 실행됩니다! (액션은 다음 렌더링까지 대기됩니다.)
이것은 리듀서가 순수해야 함을 의미합니다.
동일한 입력은 항상 동일한 출력을 의미합니다.
요청을 보내거나 시간 초과를 예약하거나 사이드 이펙트(컴포넌트 외부에 영향을 미치는 작업)을 수행해서는 안 됩니다.
그들은 돌연변이 없이 객체와 배열을 업데이트해야 합니다.
  • 각 작업은 단일 사용자 인터랙션을 설명합니다.
데이터가 여러 번 변경되는 경우에도 마찬가지입니다.
예를 들어 사용자가 리듀서에서 관리하는 5개의 필드가 있는 양식에서 "재설정"을 눌렀을 때
5개의 개별 set_field 작업보다 하나의 reset_form 작업을 전달하는 것이 더 합리적입니다.
리듀서의 모든 액션을 기록하는 경우
로그는 어떤 상호 작용이나 응답이 어떤 순서로 발생했는지 재구성할 수 있을 만큼 충분히 명확해야 합니다.
이것은 디버깅에 도움이 됩니다!

요약

요약 useState에서 useReducer로 변환하려면:
  • 이벤트 핸들러에서 액션을 디스패치합니다.
  • 주어진 상태와 동작에 대해 다음 상태를 반환하는 리듀서 함수를 작성하십시오.
  • useState를 useReducer로 바꿉니다.
리듀서는 좀 더 많은 코드를 작성해야 하지만 디버깅 및 테스트에 도움이 됩니다.
  • 리듀서는 순수해야 합니다.
    • alert 같은거 배치하지 마세요
    • 개발 환경에서 경고 2번 실행하는 이유입니다.
    • 순수하기 때문에 의존성 없이 쉽게 테스트 할 수 있습니다.
  • 각 액션은 단일 사용자 인터랙션을 설명합니다.
  • 뮤테이션 스타일로 리듀서를 작성하려면 Immer를 사용하십시오.
  • 리듀서가 없으면 상태를 업데이트하는 모든 이벤트 핸들러를 변경해야 합니다.

챌린지 요약

alert 하고싶으면 리듀서에 넣지 말고 얼럿 후 디스패치

액션 타입은 "상태를 변경하려는 방법"보다 "사용자가 수행한 작업"을 이상적으로 설명해야 합니다.

이렇게 하면 나중에 더 많은 기능을 더 쉽게 추가할 수 있습니다.

      <button onClick={() => {
        alert(`Sending "${message}" to ${contact.email}`);
        dispatch({
          type: 'sent_message',
        });
      }}>Send to {contact.email}</button>

setState로 간단하게 리듀서 만들기

export function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }
  // 디스패치된 작업은 업데이터 함수과 유사하게 다음 렌더까지 큐에서 대기합니다.
  function dispatch(action) {
  	setState(s => reducer(s, action));
  }

  return [state, dispatch];
}
반응형