글 작성자: bbangson
반응형

useReducer - 기초

 

 상태를 업데이트할 때에는 useState를 사용해서 새로운 상태를 설정해주었습니다. 하지만 useState 말고 또 다른 방법이 있습니다. 

 

 useReducer Hook 함수입니다. 이 함수를 사용하면 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있습니다. 상태 업데이트 로직을 컴포넌트 바깥에 작성할 수 있고, 다른 파일에 작성 후 불러와서 사용할 수도 있습니다. 

 

 reducer : 상태를 업데이트하는 함수입니다. 즉, 현재 상태액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수입니다. 

function reducer(state, action) {
  // 새로운 상태를 만드는 로직
  switch (action.type) {
  	case 'INCREMENT' : 
    		return state + 1;
    	case 'DECREMENT' :
    		return state - 1;
    	default :
    		return state;
 	}
}

reducer에서 반환하는 상태는 곧 컴포넌트가 지닐 새로운 상태가 됩니다. 

 

여기서 action은 업데이트를 위한 정보를 가지고 있습니다. 주로 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,
  }
}

 action 객체의 형태는 자유입니다. type 값을 대문자와 _로 구성하는 관습이 존재하지만, 꼭 따라야 할 필요는 없습니다. 

 

 이제 useReducer 함수를 사용하는 방법을 알아보겠습니다. 

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

 여기서 state는 우리가 앞으로 컴포넌트에서 사용할 수 있는 상태를 가리키게 되고, dispatch는 액션을 발생시키는 함수라고 이해하면 됩니다. 

 

 dispatch 함수 사용법은 dispatch({ type : 'INCREMENT' }) 이런 식으로 사용하면 됩니다. 

 

그리고 useReducer에 넣는 첫 번째 파라미터는 reducer 함수이고 두 번째 파라미터는 기본 값(초기 상태)입니다. 

 

지난 포스팅을 통해 만들었던 Counter.js를 useReducer로 구현해보겠습니다. 

 

Counter.js

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;

 

App.js 대신 index.js 파일에서 직접 Counter 컴포넌트를 렌더링 하겠습니다. 

 

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import Counter from './Counter';

ReactDOM.render(<Counter />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

 

 

 

useReducer - App.js에서 useReducer 사용하기

이번에는 App.js 컴포넌트에 있던 상태 업데이트 로직들을 useState가 아닌 useReducer를 사용하여 구현해보겠습니다. 

 

기존 App.js

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

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

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setInputs(inputs => ({
      ...inputs,
      [name]: value
    }));
  }, []);
  const [users, setUsers] = useState([
    {
      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
    }
  ]);

  const nextId = useRef(4);
  const onCreate = useCallback(() => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users => users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  }, [username, email]);

  const onRemove = useCallback(id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users => users.filter(user => user.id !== id));
  }, []);
  const onToggle = useCallback(id => {
    setUsers(users =>
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  }, []);
  const count = useMemo(() => countActiveUsers(users), [users]);
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;

 

useReducer를 적용한 App.js

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

function countActiveUsers(users) {
  console.log('활성 사용자 수를 세는중...');
  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;

useReducer 함수를 사용한 코드만 적용한 것이라 크게 설명한 것은 없습니다. 

 

그럼 useReducer와 useState는 어떤 차이가 있고 어떤 상황에 맞게 사용해야 할까요?

 

일단, 정해진 답은 없습니다. 상황에 따라 유연하게 사용하는 것이 맞습니다. 

 

예를 들어서 컴포넌트에서 관리하는 값이 딱 하나고 그 값이 단순한 숫자, 문자열 혹은 boolean 값이라면 확실히 useState로 관리하는 것이 편합니다. 

 

벨로퍼트님은 setter를 한 함수에서 여러 번 사용해야 하는 일이 발생한다면 

setUsers(users => users.concat(user));
setInputs({
  username: '',
  email: ''
});

그때부터 useReducer를 쓸까? 에 대한 고민을 시작한다고 합니다. 

쉽게 말해서, 상태 관리가 간단할 것 같다 하면 useState, 복잡할 것 같다 싶으면 useReducer가 낫다고 합니다. 

 

 

 

커스텀 Hook 만들어서 사용하기 

커스텀 Hook이라는 것은 말 그대로 자주 반복적으로 사용되는 컴포넌트나 로직을 커스텀 Hook으로 만들어서 재사용하는 것을 뜻합니다.

 

커스텀 Hook을 만들 때에는 보통 'use'라는 키워드로 시작하는 파일을 만들고 그 안에 함수를 작성합니다. 

 

커스텀 Hook을 만드는 방법은 useState, useEffect, useReducer, useCallback 등을 사용하여 원하는 기능을 구현하고 컴포넌트에서 사용하고 싶은 값들을 반환하면 됩니다. 

 

자주 사용되는 컴포넌트 중 하나인 input을 관리하는 useInputs 커스텀 Hook 함수를 만들어보겠습니다. 

 

useInputs.js

import { useState, useCallback } from 'react';

function useInputs(initialForm) {
  const [form, setForm] = useState(initialForm);
  // change
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setForm(form => ({ ...form, [name]: value }));
  }, []);
  const reset = useCallback(() => setForm(initialForm), [initialForm]);
  return [form, onChange, reset];
}

export default useInputs;

간단하게 코드를 설명드리면, 먼저 form이라는 새로운 상태를 useState를 통해 선언하게 되는데 이 상태의 초기값은 파라미터로 받아온 initialForm입니다. 

 

onChange 함수에서는 useCallback을 이용합니다. setForm 함수를 사용하여 form을 업데이트합니다. Spread연산자를 사용하여 기존 상태를 가져오지만 name 값을 value로 업데이트합니다. 그리고 의존하는 따른 상태가 없으므로 두 번째 파라미터인 deps에는 빈 배열을 넣어줍니다. 

 

reset 함수는 form을 초기화시키는 역할을 합니다. 초기값을 받아온 것을 설정해주는 것이므로 그 값을 setForm에 넣어줍니다. 그리고 파라미터로 가져온 것을 의존하고 있으므로 deps 배열에는 initialForm을 넣어줍니다. 

 

 

이제 만든 useInputs Hook을 App.js에서 사용해보겠습니다. 이 작업을 하기 위해서는 먼저 useReducer 쪽에서 사용하는 inputs를 없애고 이에 관련된 작업을 useInputs로 대체해야 합니다. 새로운 항목을 추가할 때 input 값을 초기화해야 하므로 데이터 등록 후 reset()을 호출합니다. 

 

App.js 

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

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

const initialState = {
  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 'CREATE_USER':
      return {
        users: state.users.concat(action.user)
      };
    case 'TOGGLE_USER':
      return {
        users: state.users.map(user =>
          user.id === action.id ? { ...user, active: !user.active } : user
        )
      };
    case 'REMOVE_USER':
      return {
        users: state.users.filter(user => user.id !== action.id)
      };
    default:
      return state;
  }
}

function App() {
  const [{ username, email }, onChange, reset] = useInputs({
    username: '',
    email: ''
  });
  const [state, dispatch] = useReducer(reducer, initialState);
  const nextId = useRef(4);

  const { users } = state;

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

  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;

 

숙제 - useInputs 커스텀 Hook을 useReducer로 구현하기.

 

useInputs.js

import { useReducer, useCallback } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'CHANGE':
      return {
        ...state,
        [action.name]: action.value
      };
    case 'RESET':
      return Object.keys(state).reduce((acc, current) => {
        acc[current] = '';
        return acc;
      }, {});
    default:
      return state;
  }
}

function useInputs(initialForm) {
  const [form, dispatch] = useReducer(reducer, initialForm);
  // change
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    dispatch({ type: 'CHANGE', name, value });
  }, []);
  const reset = useCallback(() => dispatch({ type: 'RESET' }), []);
  return [form, onChange, reset];
}

export default useInputs;

 

 

 

강의 링크

 

프론트엔드 개발 올인원 패키지 with React Online. | 패스트캠퍼스 (fastcampus.co.kr)

 

프론트엔드 개발 올인원 패키지 with React Online. | 패스트캠퍼스

프론트엔드 개발 러닝패스, 이 강의 패키지 하나로 끝낼 수 있습니다. 총 90시간 분량의 평생 소장 온라인 강의로 프론트엔드 개발자가 되세요.

www.fastcampus.co.kr

 

반응형