v19.2Latest

상태 로직을 리듀서로 추출하기

여러 이벤트 핸들러에 걸쳐 분산된 많은 상태 업데이트를 가진 컴포넌트는 복잡해질 수 있습니다. 이러한 경우, 컴포넌트 외부의 단일 함수에서 모든 상태 업데이트 로직을 통합할 수 있으며, 이를리듀서라고 합니다.

배울 내용
  • 리듀서 함수란 무엇인가
  • 어떻게 useStateuseReducer로 리팩터링하는가
  • 리듀서를 언제 사용하는가
  • 리듀서를 잘 작성하는 방법

리듀서로 상태 로직 통합하기

컴포넌트가 복잡해질수록, 컴포넌트의 상태가 업데이트되는 다양한 방식을 한눈에 파악하기 어려워질 수 있습니다. 예를 들어, 아래의TaskApp 컴포넌트는 상태에 tasks배열을 보유하고 있으며, 작업을 추가, 삭제, 수정하기 위해 세 가지 다른 이벤트 핸들러를 사용합니다:

각 이벤트 핸들러는 상태를 업데이트하기 위해setTasks를 호출합니다. 이 컴포넌트가 커질수록, 전체에 흩어져 있는 상태 로직의 양도 늘어납니다. 이러한 복잡성을 줄이고 모든 로직을 한 곳에 쉽게 접근할 수 있도록 하려면, 해당 상태 로직을 컴포넌트 외부의 단일 함수로 옮길 수 있으며, 이를“리듀서”라고 합니다.

리듀서는 상태를 처리하는 다른 방식입니다.useState에서useReducer로 세 단계로 마이그레이션할 수 있습니다:

  1. 상태 설정에서 액션 디스패치로 이동합니다.
  2. 리듀서 함수를 작성합니다.
  3. 컴포넌트에서 리듀서를 사용합니다.

1단계: 상태 설정에서 액션 디스패치로 이동

현재 이벤트 핸들러는 상태를 설정하여무엇을 할지지정합니다:

모든 상태 설정 로직을 제거하세요. 남는 것은 세 개의 이벤트 핸들러입니다:

  • handleAddTask(text)는 사용자가 "추가"를 누를 때 호출됩니다.
  • handleChangeTask(task)는 사용자가 작업을 토글하거나 "저장"을 누를 때 호출됩니다.
  • handleDeleteTask(taskId)는 사용자가 "삭제"를 누를 때 호출됩니다.

리듀서로 상태를 관리하는 것은 상태를 직접 설정하는 것과 약간 다릅니다. 상태를 설정하여 React에 "무엇을 할지" 알려주는 대신, 이벤트 핸들러에서 "액션"을 디스패치하여 "사용자가 방금 무엇을 했는지" 지정합니다. (상태 업데이트 로직은 다른 곳에 있을 것입니다!) 따라서 이벤트 핸들러를 통해tasks를 "설정"하는 대신, "작업 추가/변경/삭제" 액션을 디스패치합니다. 이는 사용자의 의도를 더 잘 설명합니다.

여러분이 dispatch에 전달하는 객체를 "액션"이라고 합니다:

이는 일반 JavaScript 객체입니다. 무엇을 넣을지는 여러분이 결정하지만, 일반적으로무슨 일이 일어났는지에 대한 최소한의 정보를 포함해야 합니다. (dispatch함수 자체는 나중 단계에서 추가할 것입니다.)

참고

액션 객체는 어떤 형태든 가질 수 있습니다.

일반적으로 무슨 일이 일어났는지 설명하는 문자열type을 주고, 다른 필드에 추가 정보를 전달하는 것이 관례입니다.type은 컴포넌트에 따라 다르므로, 이 예시에서는'added''added_task'둘 다 괜찮습니다. 무슨 일이 일어났는지 알려주는 이름을 선택하세요!

2단계: 리듀서 함수 작성하기

리듀서 함수는 상태 로직을 넣는 곳입니다. 현재 상태와 액션 객체라는 두 가지 인수를 받아 다음 상태를 반환합니다:

React는 리듀서에서 반환하는 값으로 상태를 설정합니다.

이 예제에서 상태 설정 로직을 이벤트 핸들러에서 리듀서 함수로 옮기려면 다음을 수행합니다:

  1. 현재 상태(tasks)를 첫 번째 인수로 선언합니다.
  2. 두 번째 인수로action객체를 선언합니다.
  3. 리듀서에서다음상태를 반환합니다(React가 이 값을 상태로 설정합니다).

다음은 모든 상태 설정 로직을 리듀서 함수로 마이그레이션한 예입니다:

리듀서 함수는 상태(tasks)를 인수로 받기 때문에, 컴포넌트외부에서 선언할 수 있습니다.이렇게 하면 들여쓰기 수준이 줄어들고 코드를 더 쉽게 읽을 수 있습니다.

참고

위 코드는 if/else 문을 사용하지만, 리듀서 내부에서는switch 문을 사용하는 것이 관례입니다. 결과는 동일하지만, switch 문을 한눈에 읽기 더 쉬울 수 있습니다.

이 문서의 나머지 부분에서는 다음과 같이 switch 문을 사용할 것입니다:

case 블록을 {}중괄호로 감싸는 것을 권장합니다. 이렇게 하면 서로 다른case내부에서 선언된 변수들이 충돌하지 않습니다. 또한,case는 일반적으로 return으로 끝나야 합니다.return을 잊어버리면 코드가 다음case로 "떨어져" 실수로 이어질 수 있습니다!

아직 switch 문에 익숙하지 않다면, if/else를 사용해도 완전히 괜찮습니다.

3단계: 컴포넌트에서 리듀서 사용하기

마지막으로, tasksReducer를 컴포넌트에 연결해야 합니다. React에서useReducerHook을 가져옵니다:

그런 다음 useState를 대체할 수 있습니다:

다음과 같이useReducer로 바꿉니다:

TheuseReducer Hook은 useState와 유사합니다—초기 상태를 전달해야 하며, 상태 값을 가진 값과 상태를 설정하는 방법(이 경우 디스패치 함수)을 반환합니다. 하지만 약간 다릅니다.

TheuseReducerHook은 두 개의 인수를 받습니다:

  1. 리듀서 함수
  2. 초기 상태

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

  1. 상태 값을 가진 값
  2. 디스패치 함수(사용자 작업을 리듀서로 "디스패치"하기 위해)

이제 완전히 연결되었습니다! 여기서 리듀서는 컴포넌트 파일 하단에 선언되어 있습니다:

원한다면 리듀서를 다른 파일로 옮길 수도 있습니다:

이렇게 관심사를 분리하면 컴포넌트 로직을 더 쉽게 읽을 수 있습니다. 이제 이벤트 핸들러는 액션을 디스패치하여무슨 일이 일어났는지만 지정하고, 리듀서 함수는 이에 대한 응답으로상태가 어떻게 업데이트되는지 결정합니다.

비교:useStateuseReducer

리듀서에도 단점이 있습니다! 다음은 두 가지를 비교하는 몇 가지 방법입니다:

  • 코드 크기: 일반적으로 useState를 사용하면 처음에 작성해야 할 코드가 더 적습니다.useReducer를 사용하려면 리듀서 함수그리고디스패치 액션을 모두 작성해야 합니다. 그러나 많은 이벤트 핸들러가 비슷한 방식으로 상태를 수정하는 경우useReducer는 코드를 줄이는 데 도움이 될 수 있습니다.
  • 가독성:useState는 상태 업데이트가 간단할 때 매우 읽기 쉽습니다. 상태 업데이트가 더 복잡해지면 컴포넌트의 코드를 부풀리고 스캔하기 어렵게 만들 수 있습니다. 이 경우useReducer를 사용하면 업데이트 로직의방법을 이벤트 핸들러의무슨 일이 일어났는지와 깔끔하게 분리할 수 있습니다.
  • 디버깅: useState에 버그가 있을 때 상태가어디서잘못 설정되었는지, 그리고그런지 알기 어려울 수 있습니다.useReducer를 사용하면 리듀서에 콘솔 로그를 추가하여 모든 상태 업데이트와 발생했는지(어떤 action때문인지)를 볼 수 있습니다. 각action이 올바르다면 실수가 리듀서 로직 자체에 있다는 것을 알게 될 것입니다. 그러나useState보다 더 많은 코드를 단계별로 살펴봐야 합니다.
  • 테스트:리듀서는 컴포넌트에 의존하지 않는 순수 함수입니다. 즉, 별도로 분리하여 내보내고 테스트할 수 있습니다. 일반적으로 컴포넌트를 보다 현실적인 환경에서 테스트하는 것이 좋지만, 복잡한 상태 업데이트 로직의 경우 특정 초기 상태와 액션에 대해 리듀서가 특정 상태를 반환한다고 단언하는 것이 유용할 수 있습니다.
  • 개인적 선호:어떤 사람들은 리듀서를 좋아하고, 다른 사람들은 그렇지 않습니다. 괜찮습니다. 선호도의 문제입니다. 항상useStateuseReducer사이를 오갈 수 있습니다: 둘은 동등합니다!

특정 컴포넌트에서 잘못된 상태 업데이트로 인해 버그를 자주 마주치고 코드에 더 많은 구조를 도입하고 싶다면 리듀서 사용을 권장합니다. 모든 것에 리듀서를 사용할 필요는 없습니다: 자유롭게 혼합하고 매치하세요! 심지어 같은 컴포넌트에서useStateuseReducer를 함께 사용할 수도 있습니다.

리듀서 잘 작성하기

리듀서를 작성할 때 다음 두 가지 팁을 명심하세요:

  • 리듀서는 순수해야 합니다. 상태 업데이터 함수와 유사하게, 리듀서는 렌더링 중에 실행됩니다! (액션은 다음 렌더링까지 큐에 들어갑니다.) 이는 리듀서가순수해야 함을 의미합니다—동일한 입력은 항상 동일한 출력을 생성해야 합니다. 리듀서는 요청을 보내거나, 타이머를 설정하거나, 컴포넌트 외부에 영향을 미치는 작업(사이드 이펙트)을 수행해서는 안 됩니다. 리듀서는 변이 없이객체배열를 업데이트해야 합니다.
  • 각 액션은 단일 사용자 상호작용을 설명해야 합니다. 해당 상호작용이 데이터에 여러 변경을 초래하더라도 마찬가지입니다.예를 들어, 리듀서가 관리하는 다섯 개의 필드가 있는 폼에서 사용자가 "초기화"를 누른 경우, 다섯 개의 별도set_field 액션보다는 하나의 reset_form액션을 디스패치하는 것이 더 합리적입니다. 리듀서에서 모든 액션을 기록한다면, 그 기록은 어떤 상호작용이나 응답이 어떤 순서로 발생했는지 재구성할 수 있을 만큼 명확해야 합니다. 이는 디버깅에 도움이 됩니다!

Immer를 사용하여 간결한 리듀서 작성하기

일반 상태에서객체 업데이트배열 업데이트와 마찬가지로, Immer 라이브러리를 사용하여 리듀서를 더 간결하게 만들 수 있습니다. 여기서useImmerReducer를 사용하면 push또는arr[i] =할당으로 상태를 변이할 수 있습니다:

리듀서는 순수해야 하므로 상태를 변이해서는 안 됩니다. 하지만 Immer는 변이해도 안전한 특별한draft 객체를 제공합니다. 내부적으로 Immer는 draft에 가한 변경 사항으로 상태의 복사본을 생성합니다. 이것이useImmerReducer가 관리하는 리듀서가 첫 번째 인수를 변이할 수 있고 상태를 반환할 필요가 없는 이유입니다.

요약

  • 다음과 같이useState에서useReducer로 변환합니다:
    1. 이벤트 핸들러에서 액션을 디스패치합니다.
    2. 주어진 상태와 액션에 대해 다음 상태를 반환하는 리듀서 함수를 작성합니다.
    3. 다음과 같이useStateuseReducer로 교체합니다.
  • 리듀서는 코드를 조금 더 작성해야 하지만, 디버깅과 테스트에 도움이 됩니다.
  • 리듀서는 순수해야 합니다.
  • 각 액션은 단일 사용자 상호작용을 설명합니다.
  • 변이 스타일로 리듀서를 작성하려면 Immer를 사용하세요.

Try out some challenges

Challenge 1 of 4:Dispatch actions from event handlers #

Currently, the event handlers in ContactList.js and Chat.js have // TODO comments. This is why typing into the input doesn’t work, and clicking on the buttons doesn’t change the selected recipient.

Replace these two // TODOs with the code to dispatch the corresponding actions. To see the expected shape and the type of the actions, check the reducer in messengerReducer.js. The reducer is already written so you won’t need to change it. You only need to dispatch the actions in ContactList.js and Chat.js.