React Learn - 이벤트 핸들링과 인터렉션 분해의 필요성 , useReducer

ChoiYongHyeun·2024년 2월 24일
1

리액트

목록 보기
7/31

공식문서를 크게 크게 한 챕터씩 정리하다가

3번째 챕터인 Managing State 부분부터는 한 강의 내용이 슬슬 깊어지는 듯 하여

한 강씩 정리하려고 한다.

Extracting State Logic into a Reducer


Consolidatate state logic with a reducer

인터렉션과 상태 관리 분리의 필요성

이전 챕터들에서 상태 관리와 렌더링 로직 분리의 중요성에 대해 공부했었다.

아키텍쳐들은 각 구성 요소가 독립적인 구조로 맞물려 구성되어 있을 때

유지보수 및 확장이 쉽고 읽기 쉬우며 디버깅 하기 쉽다는 이야기를 했었다.

useState 를 이용하면 렌더링 로직과 상태 관리가 구분된다는 장점이 존재하지만

여전히 혼재하는 구조가 존재한다.

useState 에서는 인터렉티브한 행위가 직접적으로 상태에 관여하도록 한다.

예를 들어 생각해보자

예시를 들어 생각해보기

function Counter({ color }) {
  const [num, setNum] = useState(0); // 1. num , setNum 이라는 상태가 존재함 

  return (
    <div className='container' style={{ backgroundColor: color }}>
      <p>{num}</p>
      <button // 2. 버튼이 눌리면 
        onClick={() => { 
          setNum(num + 1);  // 3. 상태를 변경하도록 한다. 
        }}
      >
        Increase Number
      </button>
    </div>
  );
}

Counter 컴포넌트의 상태 관리의 주체는 button 컴포넌트이다.

컴포넌트의 상태 관리 로직이 단순하게 양이 적을 때에는 useState 를 이용하는 것에 불편함이 없다.

하지만 컴포넌트에 다양한 이벤트 핸들러가 존재하고 , 이벤트 핸들러가 모두 동일한 상태 관리에 관여하게 된다면 어떨까 ?

하나의 상태를 여러 이벤트 핸들러가 변경하는 경우

function Counter({ color }) {
  const [num, setNum] = useState(0);

  function addNum() {
    setNum(num + 1);
  }

  function subtractNum() {
    setNum(num - 1);
  }

  function resetNum() {
    setNum(0);
  }

  return (
    <div className='container' style={{ backgroundColor: color }}>
      <p>{num}</p>
      <div className='button-wrapper'>
        <button onClick={addNum}>Increase Number</button>
        <button onClick={subtractNum}>Subtract Number</button>
        <button onClick={resetNum}>Reset Number</button>
      </div>
    </div>
  );
}

이벤트에 따른 상태 변경 로직이 비교적 간단하여 코드를 읽는데에 있어서 크게 문제는 없지만

addNum , subtractNum , resetNum 모두 setNum 을 이용해 동일한 state 를 변경하는 함수들이기 때문에

컴포넌트 안에서 혼재되어 흩뿌리게 되어져있기 보다 하나로 캡슐화 하여 관리하는게 더 나을 것 같다.

캡슐화를 해보자

캡슐화

function Counter({ color }) {
  const [num, setNum] = useState(0);
  
  const calculator = {
    add() {
      setNum(num + 1);
    },

    subtract() {
      setNum(num - 1);
    },

    reset() {
      setNum(0);
    },
  };

  return (
    <div className='container' style={{ backgroundColor: color }}>
      <p>{num}</p>
      <div className='button-wrapper'>
        <button onClick={calculator.add}>Increase Number</button>
        <button onClick={calculator.subtract}>Subtract Number</button>
        <button onClick={calculator.reset}>Reset Number</button>
      </div>
    </div>
  );
}

통일되는 로직을 캡슐화 하여 객체 형태로 감싸 준 후 객체의 메소드로 호출해주는 형태로 변경해주었다.

이를 통해 num 상태를 변화시키는 메소드를 한 객체에서 관리하게 됨으로서 메소드들의 관리가 더욱 편해졌으며

코드의 흐름 또한 좋아졌다.

하지만 여전히 불편한 점이 있다.

이벤트가 갖는 의미가 무엇일까 ?

	...
        <button onClick={calculator.add}>Increase Number</button>
        <button onClick={calculator.subtract}>Subtract Number</button>
        <button onClick={calculator.reset}>Reset Number</button>
    ...

현재의 로직들에서는 버튼이 눌리면 직접적으로 상태를 변화시키도록 하고 있다.

이전에 아키텍쳐를 이루는 구조물들은 각자가 가지고 있는 기능이 단순하고, 서로가 계층적으로 맞물려 있을 때

관리가 편하다고 하였는데 현재는 그렇지 않다.

버튼이 갖는 의미는 상태를 변화시키고자 하는 의미를 갖는 구조물일뿐 상태 변화와 직접적으로 연관 시킬 필요가 없다.

예시를 들어 이야기 하자면 내가 핵폭탄 발사 버튼을 눌렀을 때 내가 직접 핵폭탄을 들고 날리는 것이 아니라

여러 버튼 중 발사 버튼이 눌리면 (이벤트 발생) -> 눌린 버튼이 발사니까 (의미 파악) -> 핵폭탄 발사 (인터렉션)

이런 식으로 관리하는 방식도 존재한다는 것이다.

이런 방식을 이용하면 각 구성요소를 더욱 단순하게 구성하여 의미를 명확하게 하고

계층적인 구조를 이룸으로서 디버깅도 쉬워진다.

만약 발사 버튼을 눌렀는데 핵폭탄이 발사되지 않는다면 의미 파악 단계를 기준으로 의미 파악 단계 이전이 문제인지, 이후가 문제인지를 확인 할 수 있다.

만약 의미파악 단계 없이 이벤트 발생과 인터렉션 단계가 직접적으로 연결되었다면 이벤트 발생 부분이 문제인지 , 인터렉션 부분이 문제인지 파악하기가 어렵다.

이벤트와 상태 관리를 분해해보자

탑다운 방식으로 흘러가는 것이 이해하기 더 쉬운 것 같다. 그럼으로 탑다운 방식으로 가보자 !@!@!@

이벤트 핸들러들은 인터렉션이 일어났을 때 상태를 직접적으로 변경 하는 것이 아니라

해당 이벤트의 의미를 dispatch 하기만 한다.

   ...
  function dispatchAdd() {
    dispatch({ type: 'add', currentNum: num });
  }

  function dispatchSubtract() {
    dispatch({ type: 'subtract', currentNum: num });
  }

  function dispatchReset() {
    dispatch({ type: 'reset', currentNum: num });
  }

  return (
    <div className='container' style={{ backgroundColor: color }}>
      <p>{num}</p>
      <div className='button-wrapper'>
        <button onClick={dispatchAdd}>Increase Number</button>
        <button onClick={dispatchSubtract}>Subtract Number</button>
        <button onClick={dispatchReset}>Reset Number</button>
      </div>
    </div>
  );

이로서 button 컴포넌트에 있는 이벤트 핸들러들의 역할이 단순해졌다.

dispatch({type : ...}) 을 통해 발생한 이벤트에 대한 정보를 담아 dispatch 함수에게 건내준다.

이 때 dispatch 에게 인수로 전달되는 객체는 action 객체라고 하며, 어떻게 생기든지 상관은 없지만

관례적으로 type 프로퍼티에 발생한 이벤트 대한 정보를 담도록 한다.

객체 형태로 관리해주는 이유는 액션 객체 내에서 인터렉션에 필요한 인수들을 추가적인 프로퍼티로 담아 제공해줄 수 있기 때문이다.

  /**
   * @param {object} action 발생한 이벤트의 의미 파악을 위한 객체
   * 의미를 캐치하여 의미 별 이벤트를 발생시키게 해주는 중개자 역할
   */
  function dispatch(action) {
    taskHandler(action);
  }
...

dispatch 함수는 받은 action 객체를 받아 taskHandler 라는 함수에게 전달해주는 중개자 역할을 한다.

그럼 taskHandler 는 어떻게 생겼을까 ?

  /**
   *
   * @param {object} action dispatch 로 부터 받아온 action 객체
   * action에 담긴 정보를 이용해 의미 별 이벤트를 발생 시킴
   */
  function taskHandler(action) {

    switch (action.type) {
      case 'add': {
        setNum(action.currentNum + 1);
        break;
      }

      case 'subtract': {
        setNum(action.currentNum - 1);
        break;
      }

      case 'reset': {
        setNum(0);
        break;
      }
      default: {
        throw new Error('그런 타입은 없어 ! ');
      }
    }
  }

taskHandler 함수는 받은 action 객체를 이용해 type 에 명시 된 값 별 다른 방식으로

상태를 업데이트 한다.

function Counter({ color }) {
  const [num, setNum] = useState(0);

  /**
   * @param {object} action 발생한 이벤트의 의미 파악을 위한 객체
   * 의미를 캐치하여 의미 별 이벤트를 발생시키게 해주는 중개자 역할
   */
  function dispatch(action) {
    taskHandler(action);
  }

  /**
   *
   * @param {object} action dispatch 로 부터 받아온 action 객체
   * action에 담긴 정보를 이용해 의미 별 이벤트를 발생 시킴
   */
  function taskHandler(action) {
    console.log(action);

    switch (action.type) {
      case 'add': {
        setNum(action.currentNum + 1);
        break;
      }

      case 'subtract': {
        setNum(action.currentNum - 1);
        break;
      }

      case 'reset': {
        setNum(0);
        break;
      }
      default: {
        throw new Error('그런 타입은 없어 ! ');
      }
    }
  }

  /**
   * 각 컴포넌트들이 갖는 의미 부여
   * 이벤트 핸들러는 상태를 변경시키는 것이 아니라 , 눌린 버튼의 의미를 dispatch 하도록 함
   */
  function dispatchAdd() {
    dispatch({ type: 'add', currentNum: num });
  }

  function dispatchSubtract() {
    dispatch({ type: 'subtract', currentNum: num });
  }

  function dispatchReset() {
    dispatch({ type: 'reset', currentNum: num });
  }

  return (
    <div className='container' style={{ backgroundColor: color }}>
      <p>{num}</p>
      <div className='button-wrapper'>
        <button onClick={dispatchAdd}>Increase Number</button>
        <button onClick={dispatchSubtract}>Subtract Number</button>
        <button onClick={dispatchReset}>Reset Number</button>
      </div>
    </div>
  );
}

전체 코드량은 늘어났지만

아키텍쳐의 구조가 단순하고 명확해진 것을 볼 수 있다.

예전 인터렉티브한 UI 구성을 상태 관리와 상태 별 렌더링 로직으로 나눴을 때 얻었던 이점을

상태 관리를 이벤트 핸들러와 인터렉션으로 나눴을 때에도 동일하게 얻을 수 있다.

action 객체에 따라 다른 인터렉션을 시행하는 것은 마치 상태에 따라 다른 렌더링 로직을 보이는 것과 유사하다.

인터렉션은 action 객체에 따라만 결정되기 때문에 새로운 인터렉션을 추가 하고 싶다면 새로운 action 객체와 새로운 로직을 구성해주기만 하면 된다.

또한 taskHandler 부분에서 액션 객체를 받기 때문에 만약 디버깅을 할 때에는

액션객체를 로깅하면 되기 때문에 디버깅도 매우 쉬워진다.

이걸 해주는게 useReducer 라고 ~~!

리액트에서는 이런 기능을 Hook 으로 제공해주고 있다.

그것은 useReducer 이다.

function Counter({ color }) {
  const [num, dispatch] = useReducer(taskHandler, 0);

  /**
   * useReducer 에 들어갈 Reducer function 으로 action 객체의 type에 따라 state 를 새롭게 갱신
   * @param {Number} num 렌더링 로직과 연결된 state 값
   * @param {Object} action dispatch 함수를 통해 전달 받은 action 객체
   * @returns 업데이트 할 새로운 state
   */
  function taskHandler(num, action) {
    switch (action.type) {
      case 'add': {
        return num + 1;
      }

      case 'subtract': {
        return num - 1;
      }

      case 'reset': {
        return 0;
      }

      default: {
        throw new Error('그런 타입은 없어요 ~!');
      }
    }
  }

  function dispatchAdd() {
    dispatch({ type: 'add' });
  }

  function dispatchSubtract() {
    dispatch({ type: 'subtract' });
  }

  function dispatchReset() {
    dispatch({ type: 'reset' });
  }

  return (
    <div className='container' style={{ backgroundColor: color }}>
      <p>{num}</p>
      <div className='button-wrapper'>
        <button onClick={dispatchAdd}>Increase Number</button>
        <button onClick={dispatchSubtract}>Subtract Number</button>
        <button onClick={dispatchReset}>Reset Number</button>
      </div>
    </div>
  );
}
const [state , action 객체를 생성하기 위한 function] =
      useReducer(action 객체에 따른 setterfunc 모음,state 초기값)

다음과 같은 모습으로 사용한다.

useReducer 의 첫 번째 인수는 action 객체의 type 별로 다른 반환값을 제공하는데

각 반환값은 업데이트 될 새로운 state 가 된다.

필요에 따라 taskHandler 부분은 다른 폴더에 모듈로 만들어두어 임포트하여 사용하면

코드의 가독성이 더욱 올라갈 것이다.

useReducer 를 이용하면 얻는 이점을 정리하면 다음과 같다.

  • 상태 변경을 action 객체 값에 따라 결정 하기 때문에 상태 변경을 예측 가능하며 상태가 어떻게 변하는지 더 쉽게 이해 할 수 있다.
  • 복잡한 상태 관리 로직을 하나의 함수로 관리하기 때문에 중앙 집중식 로직을 사용 가능하며 로직 관리와 확장이 더욱 쉽다.
profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글