본문 바로가기

javascript/react

[React] useReducer Hook

https://ko.reactjs.org/docs/hooks-reference.html#usereducer

useReducer hook은 useState와 같이 상태를 관리하기 위해 사용될 수 있는 hook이다. useState에 비해 다양한 값을 동시에 처리하거나, 다양한 action을 정의해 둘 수 있다는 장점이 존재한다. 

useReducer 의 사용은 다음과 같은 상황에서 고려할 수 있다.

  • 연관성이 높은 변수들을 함께 처리하고 싶은 경우
  • 특정 state가 자신의 이전 값 기반이 아니라, 별개의 다른 state들의 값 기반으로 업데이트 되는 경우
  • 업데이트 하는데 있어서 다양한 로직이 존재하는 경우

해당 hook은 다음과 같은 방식으로 사용할 수 있다.

const [state, dispatch] = useReducer(reducer, initialArg, init);
  • reducer : 입력받은 action에 대해 특정 동작을 수행하도록 구현된 함수
  • initialArg : state의 초기값
  • init : state의 초기값을 만드는데 사용되는 함수. initialArg와 동일하게 초기값을 설정하는 용도를 가진다.

reducer의 구조는 다음과 같다.

const reducer = (state, action) => {
	switch(action.type) // action에 따라 동작을 구분한다.
    {
    case 'case_A':
    	... // some code
        return {arg1: state.arg1 + 1, arg2: action.newValue };
    case 'case_A':
    	... // some code
        return {arg1: ~, arg2: ~};
    case 'case_A':
    	... // some code
        return {arg1: ~, arg2: ~};
    default:
    	throw new Error("Cannot find action");
        //존재하지 않는 action 은 예외처리
    }
};


// example

const emailReducer = (state, action) => {
  if (action.type === 'USER_INPUT') {
    return { value: action.value, isValid: action.value.includes('@') };
  }
  else if (action.type === 'INPUT_BLUR')
    return { value: state.value, isValid: state.value.includes('@') };
};
  • state : useState의 prev 에 대응되는 값으로, 이전 state을 가리킨다.
  • action : 현재 동작을 나타내는 것으로, 개발자 마음대로 구현하면 된다.

state는 자신이 생성한 변수와 관련되어 있기 때문에 당연히 자기 마음대로 구현해도 된다. 그런데, action 역시 마음대로 구현해도 된다는 것은 구체적으로 어떤 뜻일까?

let input = 1;

const action = { type: 'USER_INPUT', value: input };
// type 및 value는 개발자 마음대로 구현 가능. reducer 함수가 동작하기만 하면 됨.

const action2 = 'SHOW_DATABASE';
// 그냥 문자열로 정의해도 전혀 상관 없음. 그냥 reducer 함수만 동작하면 ok

const action3 = 3;
// 숫자여도 상관 없음

const action4 = { myType: Symbol.for('USER_INPUT'), value: input }
// 당연히 symbol이어도 전혀 상관 없음.

action에는 다양한 값이 올 수 있다. 정확히 말하자면, action의 프로퍼티 혹은 action 자체의 값이 reducer 함수에서 제대로 각 동작을 구분할 수만 있다면 구현 상의 제약은 없는 편이다. 다만 일관성을 위해 객체 내부에 프로퍼티 형태로 넣거나, 문자열 등의 단일 타입으로 지정하는 것이 좋다. 그럼에도, 다양한 타입을 동시에 사용해도 전혀 문제는 없다.

이때 reducer에 정의된 각 동작은 initial State와 동일한 구조를 가진 객체를 반환하도록 구현해야 한다. 만약 각각의 동작마다 서로 다른 구조를 가진 객체를 반환하게 만든다면, 해당 객체에 대한 유지보수가 힘들어질 수 있다. 

useReducer Hook을 이용한 예시는 다음과 같다.

https://github.com/blaxsior/JS_Study/tree/react-usereducer-example-lock

interface NumberState {
    value: number;
    min: number;
    max: number;
}

type ACTIONTYPE =
    { type: "INCREMENT" }
    | { type: "DECREMENT" };

const numChangeReducer = (state: NumberState, action: ACTIONTYPE): NumberState => {
    switch (action.type) {
        case 'INCREMENT':
            return { ...state, value: (state.value + 1 <= state.max) ? state.value + 1 : state.min };
        case 'DECREMENT':
            return { ...state, value: (state.value - 1 >= state.min) ? state.value - 1 : state.max };
    }
    throw new Error("cannot reach here!");
}
 
 ... // 여러 코드들
 
 const [numState, numDispatch] = useReducer(
        numChangeReducer,
        { value: minNum, min: minNum, max: maxNum } as NumberState
    );
    //useReducer을 이용하여 state을 생성
    
    const { value } = numState;
    
    useEffect(() => {
        onChange(value, index);
    }, [onChange, value, index]);
    // state가 변경되면, 값을 Lock에 전달하도록 코드 작성

const numIncHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
	numDispatch({ type: "INCREMENT" });
};

const numDecHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
	numDispatch({ type: "DECREMENT" });
};
// handler 콜백을 구성하여 버튼을 누를 때 활성화 되도록 구현

return (
        <div>
            <button
                onClick={numIncHandler} // 콜백1
                id={`inc_button_index${index}`}
                className={styles.button_inc}>▲</button>
            <div className={styles.lock_number}>{numState.value}</div>
            <button
                onClick={numDecHandler} // 콜백2
                id={`dec_button_index${index}`}
                className={styles.button_dec}>▼</button>
        </div>
    );

 

위 코드에서는 이전 useEffect에 대해 구현한 Lock 컴포넌트의 코드 중 LockNumber.tsx에서 useState로 구현되어 있던 내용을 useReducer에 대해 수정했다. 숫자를 select 태그 대신 button을 통해 변경하도록 구성했으며, 값은 1 단위로 증가하거나 감소하도록 구현했다.

useReducer로 전달되는 state는 { value : number } 의 구조를 가지고 있다. reducer의 역할을 수행하는 nameChangeReducer 함수는 입력받은 action : ACTIONTYPE의 type 프로퍼티의 값에 따라 증가 또는 감소 동작을 수행할 수 있도록 구현했는데, 이는 두개의 버튼과 연결된 num~Handler에서 numDispatch을 실행시킴으로써 작동한다.

 

초기에 비밀번호가 73943으로 설정되어 있다.

초기 좌물쇠 컴포넌트의 모습

숫자는 Lock 컴포넌트에 지정된 0 ~ 9 사이의 값을 가진다.

비밀번호를 입력하면 좌물쇠가 열린다.

 

결론

useReducer은 여러개의 state의 관리 또는 여러개의 action이 필요하거나, 다른 값에 기반하여 업데이트가 진행되는 등 일반적인 state 관리 방식으로는 복잡한 문제일 때 사용을 고려할 수 있다. useReducer hook은 reducer function 및 initialState을 받고, reducer function은 이전 state 및 action을 입력받는다. 값의 갱신은 dispatch를 통해 가능하다