[React] useReducer 이해하기

Noma·2021년 9월 27일
0
post-custom-banner

이 글은 velopert님의 useReducer를 사용하여 상태 업데이트 로직 분리하기 부분을 공부하고 정리한 글입니다.

1. useReducer 개념

상태를 관리할 때 사용하는 hook 함수로, useReducer를 사용하면 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있다.

상태 업데이트 로직을 컴포넌트 바깥에서 작성할 수도 있고, 심지어 다른 파일에 작성 후 불러와서 사용할 수도 있다.

본격적으로 useReducer를 사용해보기 전에, 먼저 reducer와 dispatch가 무엇인지 알아야 한다. (Redux에서 쓰이는 용어임)

1.1 reducer

reducer란? 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수이다.

function reducer(state,action){
	//새로운 상태를 만드는 로직
  	// const nextState = ...
  return nextState;
}

reducer가 반환하는 값이 곧 컴포넌트가 지닐 새로운 상태이다.

  • state : 현재 상태
  • action : 업데이트를 위한 정보를 가지고 있다. 주로 type 값을 지닌 객체 형태로 사용한다.
    • type 값을 대문자와 _로 구성한다는 관습이 존재
    • action 예시
    // 카운터에 1을 더하는 액션
    {
      type: 'INCREMENT'
    }
    // 카운터에 1을 빼는 액션
    {
      type: 'DECREMENT'
    }
    // input 값을 바꾸는 액션
    {
      type: 'CHANGE_INPUT',
      key: 'email',
      value: 'tester@react.com'
    }
    // 새 할 일을 등록하는 액션
    {
      type: 'ADD_TODO',
      todo: {
        id: 1,
        text: 'useReducer 배우기',
        done: false,
      }
    }

1.2 dispatch

dispatch는 액션을 발생시키는 함수 로, dispatch({type:'INCREMENT'}) 와 같이 안에 액션을 넣어 호출하면 된다.

2. useReducer 사용하기

useReducer 함수에는 첫번째 인자로 reducer 함수를, 두번째 인자로 초기 상태값을 넣어주면 된다.

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

위의 state가 앞으로 컴포넌트에서 사용할 수 있는 상태를 가리킨다.

예시1
state가 간단한 숫자인 경우 아래와 같이 사용한다.

import React, { useReducer } from 'react';

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

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;

예시2
state가 복잡한 객체인 경우 아래와 같이 사용한다.

import React, { useRef, useReducer, useMemo, useCallback } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function countActiveUsers(users) {
  return users.filter(user => user.active).length;
}

const initialState = {
  inputs: {
    username: '',
    email: ''
  },
  users: [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]
};

function reducer(state, action) {
  switch (action.type) {
    case 'CHANGE_INPUT':
      return {
        ...state,
        inputs: {
          ...state.inputs,
          [action.name]: action.value
        }
      };
    case 'CREATE_USER':
      return {
        inputs: initialState.inputs,
        users: state.users.concat(action.user)
      };
    case 'TOGGLE_USER':
      return {
        ...state,
        users: state.users.map(user =>
          user.id === action.id ? { ...user, active: !user.active } : user
        )
      };
    case 'REMOVE_USER':
      return {
        ...state,
        users: state.users.filter(user => user.id !== action.id)
      };
    default:
      return state;
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const nextId = useRef(4);

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

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

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

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

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

  const count = useMemo(() => countActiveUsers(users), [users]);
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onToggle={onToggle} onRemove={onRemove} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;

3. 언제 사용하는가?

만약 컴포넌트에서 관리하는 값이 딱 하나이고, 그 값이 단순한 숫자, 문자열 또는 boolean 값이라면 useState으로 관리하는 것이 편하다.

하지만, 컴포넌트에서 관리하는 값이 여러 개가 되어서 상태의 구조가 복잡해진다면 useReducer로 관리하는 것이 편할 수 있다.

이에 대한 결정은, useState와 useReducer를 자주 사용해보고 맘에드는 방식으로 선택하면 된다.

벨로퍼트님의 경우, setter를 한 함수에서 여러번 사용해야 하는 일이 발생하면 그때부터 useReducer를 써볼까 고민하기 시작한다고 한다.

profile
오히려 좋아
post-custom-banner

0개의 댓글