본문 바로가기

React.js

React(리액트): useReducer (컴포넌트 상태 관리, 업데이트)

반응형

이번에는 useReducer라는 Hook에 대해 알아보자.

이전에는 컴포넌트의 상태를 업데이트해야 할 때는 useState를 사용해서 새로운 상태를 설정해주었는데 useState 말고

useReducer를 사용해서도 상태를 업데이트해줄 수 있다.

 

이 둘의 차이점에 대해 살펴보자.

먼저 useState는 설정하고 싶은 다음 상태를 직접 지정해주는 방식으로 상태를 업데이트해주는 반면에

useReducer'action'이라는 객체를 기반으로 상태를 업데이트한다.

여기서 'action' 객체는 업데이트할 때 참조하는 객체이다.

 

 useReducer라는 Hook 함수를 사용하면 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시키는 것이 가능하다.

심지어 다른 파일에 작성 후 불러올 수도 있다.

여기서 reducer라는 개념이 있는데 reducer란 상태를 업데이트하는 함수를 의미한다.

 

function reducer(state, action){
    switch (action.type){
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
}

reducer 함수는 현재 상태와 action을 파라미터로 받아와서 새로운 상태를 반환해주는 형태를 갖추고 있어야 한다.

코드를 보면 action.type을 읽어온 다음 읽어온 값이 'INCREMENT'면 기존 상태에 1을 더하고 'DECREMENT'면 기존 

상태에서 1을 빼는 작업을 한다. 코드처럼 현재 상태와 action 객체를 받아와서 업데이트된 상태를 반환하는 형태의 

함수가 바로 reducer이다. 

 

const [number,dispatch] = useReducer(reducer, 0);

userReducer를 사용할 때는 다음과 같이 사용한다. useReducer의 첫 번째 파라미터에는 reducer 함수가 들어가고, 

두 번째에는 기본 값을 넣어준다. 기본 값은 숫자가 될 수도 있고, 객체가 될 수도 있다.

'number'는 현재 상태를 의미하고 'dispatch'는 action을 발생시키는 함수이다. dispatch 단어의 의미는 '보내다.'라는 

뜻이고 여기서는 action을 발생시킨다 정도의 의미로 이해를 하면 되겠다.

 

Counter.js

import { useReducer } from "react";

function reducer(state, action){
  switch(action.type){
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      throw new Error('Unhandled action');
  }
}

function Counter() {
  const [number, dispatch] = useReducer(reducer, 0)

  const onIncrease = () => {
    dispatch({
      type: 'INCREMENT'
    })
  };
  const onDecrease = () => {
    dispatch({
      type: 'DECREMENT'
    })
  };
  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;

useState로 상태 관리를 하던 Counter 컴포넌트를 useReducer를 사용한 상태 관리로 바꿔주었다.

reducer 함수를 작성하고, useReducer 함수의 첫 번째 파라미터로 넣어주었다. 두 번째 파라미터에는 useReducer의 

기본 값이 들어간다. onIncrease와 onDecrease도 dispatch로 내용을 바꾸어 주었다. 

이 코드를 보면 상태의 업데이트 로직이 컴포넌트 밖에 있는 것을 확인할 수 있다. 

 

useReducer가 어떻게 작동하는지 간략하게 알아보았고, 이번엔 App.js에서 useState를 사용했던 것들을 useReducer를

사용해서 구현해 보자. 아래의 코드들은 모두 App 컴포넌트에서 작성한 코드이다. 

 

const initialState= {
  inputs: {
    username: '',
    email: '',
  },
  users: [
    {
      id: 1,
      username: 'paboke22',
      email: 'paboke22@gmail.com',
      active: true
  },
  {
      id:2,
      username: 'tester',
      email: 'tester@example.com',
      active: false,
  },
  {
      id: 3,
      username: 'guest',
      email: 'guest@example.com',
      active: false,
  }

  ]
}

가장 첫 번째로 해야 할 작업은 App 컴포넌트에서 사용할 초기 상태를 컴포넌트 바깥에 선언해 주는 것이다. 

 

function reducer(state, action) {
  return state;
}

state, action을 받아와서 작업을 하는 reducer 함수를 선언해준다. 안의 내용은 아래에서 작성하도록 하겠다.

 

const [state, dispatch] = useReducer(reducer, initialState);

state는 현재 상태고 dispatch는 action을 발생시키는 함수다. useReducer에는 reducer를 첫 번째 파라미터로 넣고 

두 번째에는 위에서 선언했던 initialState를 넣어준다.

 

const {users} = state;
const {username, email} = state.inputs;

return (
  <>
    <CreateUser 
      username={username} 
      email={email} 
    />
    <UserList users={users} />

지금 state 안에는 inputs와 users가 들어있는데,  들어있는 값들을 비구조화 할당으로 추출해준 다음 props로

전달해 주도록 하자.

 

const onChange = useCallback(e => {
    const {name, value} = e.target;
    dispatch({
      type: 'CHANGE_INPUT',
      name,
      value
    })
  }, []);

다음으로 onChange 함수를 구현했다. useCallback을 미리 사용해주었기 때문에 이 함수는 컴포넌트가 처음 렌더링 

될 때 만들어지고 그다음은 재사용된다. 이벤트 객체 'e'는 input 창을 수정하게 됐을 때 발생하는 이벤트 객체이다.

dispatch의 type은 'CHANGE_INPUT'이고 name과 value는 이벤트 객체로 받아온 name과 value를 그대로 사용한다.

 

function reducer(state, action){
  switch (action.type){
    case 'CHANGE_INPUT':
      return {
        ...state,
        inputs: {
          ...state.inputs,
          [action.name]: action.value
        }
      };

reducer 함수에서는 type이 'CHANGE_INPUT'일 경우 ...state로 기존의 상태를 가져오고 inputs값을 덮어쓴다. 

이렇게 하는 이유는 불변성을 지키기 위해서다. inputs에서도 기존의 inputs를 가져오고 새로 받아온 name과 value를

덮어쓴다. 

 

const onCreate = useCallback(() => {
    dispatch({
      type:'CREATE_USER',
      user:{
        id: nextId.current,
        username,
        email,
      }
    });
    nextId.current += 1;
  },[username, email])

다음은 onCreate 함수를 구현해보자. 여기서도 useCallback 함수를 미리 사용해주었고 user 내부의 id 값은 useRef로 

관리해주는 값을 넣어준다. username과 email은 위에서 받아온 것을 넣어주고 deps에 기존 상태에 의존하고 있는

username과 email을 넣어준다.

 

case 'CREATE_USER':
      return{
        inputs: initialState.inputs,
        users: state.users.concat(action.user)
      };

reducer에서 CREATE_USER 타입을 가진 action을 처리해주도록 작성했다. inputs의 경우는 초기값으로 바꿔주고 

users 배열은 새로 입력된 객체를 추가하여 업데이트한다. 기존에 useState를 사용하여 구현할 때는 inputs를 날리는 

작업 따로 하고 users 배열을 업데이트하는 작업을 따로 했었는데 이번에는 'CREATE_USER'라는 액션이 발생하면 두 가지

작업을 동시에 할 수가 있게 된다.

 

case 'TOGGLE_USER':
      return {
        ...state,
        users: state.users.map(user => 
          user.id === action.id 
            ? {...user, active: !user.active}
            : user
          )
      }

이번에는 reducer 먼저 작성해 주었다. action이 'TOGGLE_USER'인 경우에는 기존의 상태를 가져오고 그중에 users 배열을 업데이트해준다. map을 사용해서 user.id와 action을 통해 받아온 action.id와 같은지 비교를 하고 같다면 새로운 

객체를 만들어 넣어준 다음에 active값을 반전시켜 주고 같지 않다면 기존의 user객체를 그대로 유지해준다.

 

case 'REMOVE_USER':
      return {
        ...state,
        users: state.users.filter(user => 
          user.id !== action.id)
      }
      default:
        throw new Error('Unhandled action')
  }

action이 'REMOVE_USER'인 경우 filter 함수를 사용해서 user.id와 action으로 받아온 action.id가 일치하지 않으면 

유지하고, 일치한다면 배열에서 사라지게 만들어준다. 

 

const onToggle = useCallback(id => {
    dispatch({
      type: 'TOGGLE_USER',
      id
    });
  }, []);
  
const onRemove = useCallback(id => {
    dispatch({
      type: 'REMOVE_USER',
      id
    });
  }, []);

onToggle, onRemove함수를 구현해 주었다. deps 배열은 비어있다. 함수를 컴포넌트가 생성될 때 한번 만들어지고 

계속 재사용하겠다는 뜻.

 

function countActiveUsers(users){
  console.log('활성 사용자 수를 세는중....');
  return users.filter(user=> user.active).length;
}

const count = useMemo(() => countActiveUsers(users),[users])

마지막으로 useMemo를 사용해서 '활성 사용자 수'도 구현해주자. 이 함수는 이전에 사용했던 코드를 그대로 사용했다.

 

useReducer VS useState

우리가 기존에 useState를 사용해서 구현했던 코드를 모두 useReducer를 사용하도록 전환해주었다. 

여기서 궁금해지는 사항이 하나 있을 것이다. 어떨 때 useReducer를 쓰고 어떨 때 useState를 써야 할까

일단 정해진 정답은 없다. 상황에 따라 불편할 수도, 편할 수도 있다. 

 

이것에 대한 결정은 앞으로 useReducer, useState를 앞으로 자주 사용해보고 맘에 드는 방식을 사용하면 되겠다.

쉽게 결정을 내리는 방법은 좀 간단하겠다 싶으면 useState를 사용하고 좀 복잡할 것 같으면 useReducer를 사용하도록 하자.

반응형