4. useReducer 로 상태관리하기

신태현·2023년 4월 4일

리액트 입문

목록 보기
4/6
post-thumbnail

그전 시간 까지 CRUD 구현한 것의 최적화에 관련된 훅들을 배웠다.
이번에는 UseState 를 대신하여 사용할 수 있는 useReducer 라는 훅과, 커스텀 훅, 그리고 전역 상태관리가 가능한 context API 까지 배워본다.

1. useReducer

useState 는 state 를 관리할 때 정말 좋은 훅이다.
상태관리할 state가 적으면 정말 좋지만... 많아지면 조금 불편해진다
또한 상태 업데이트를 무조건 컴포넌트 안에서만 해야하는 단점이 있다.
이를 해결하는 다른 state 훅이 존재한다.

바로 useReducer!

상태 업데이트 로직을 컴포넌트로부터 분리할 수 있게 되고, state 들을 집합해서 관리 할 수 있다.

useReducer 면 reducer 를 쓴다는 것인데 reducer 가 뭘까


reducer 는 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수이다.
현재 상태에 어떠한 액션을 통해 다른 상태로 바꿔주는 느낌이다.

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

리턴 된 상태는 컴포넌트가 지닐 새로운 상태가 된다.

action 은 이러한 state 를 업데이트하기 위한 정보들을 가지는데, 주로 type 프로퍼티를 가지고있고, 상태 업데이트에 관련된 다른 프로퍼티 값들도 가지고 있다.

// 카운터에 1을 더하는 액션
{
  type: 'INCREMENT'
}

이러한 타입과

// input 값을 바꾸는 액션
{
  type: 'CHANGE_INPUT',
  key: 'email',
  value: 'tester@react.com'
}

타입 + 필요 프로퍼티 값들을 지닌다. type 은 주로 대문자와 _ 로 구성한다.


reducer 를 배웠으니 useReducer 를 알아보자

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

state : 우리가 컴포넌트에서 사용할 상태(전체)
dispatch : reducer의 액션을 발생시키는 함수 (dispatch(action) 으로 사용)
reducer : 아까처럼 액션에 의해 state를 변형해 리턴하는 함수
initialState : 컴포넌트 상태의 default

이렇게 된다.

Counter.js 라는 increase 버튼을 누를 때 수 증가, decrease 버튼 누를 때 수 감소하는 로직을 useReducer 로 구성해보자.

먼저 컴포넌트에 reducer 를 사용해보자

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>
  );
}

쓰이는 state는 현재 수인 number 가 끝이니 number 로 다이렉트로 정의
initialState 는 0으로 설정해서

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

로 해주고

onIncrease, onDecrease 함수를 정의해줄 때 reducer 의 적용될 action을 dispatch 에 넣어준다.

따로 다른 컴포넌트가 필요하지 않으니, type 만 전달하여 어떤 case 인지 전달해준다.

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

그리고 컴포넌트 외부에 reducer 함수를 만든다.
reducer 함수는 action.type 에 따른 switch 문으로 구성된다.
dispatch 함수로 reducer 에 전달된 type에 따라 state를 1씩 증감하고 리턴한다.

이러면 간단히 counter 컴포넌트를 구성할 수 있다!



이제 실전으로 CRUD에서 했던 App component 의 내부 로직을 모두 useReducer 로 치환해야 한다.

막막할 수 있다. 하지만 state를 모아서 case by case로 관리하니 매우 가독성이 좋아진다.

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;
}

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 App() {
  return (
    <>
      <CreateUser />
      <UserList users={[]} />
      <div>활성사용자 수 : 0</div>
    </>
  );
}

export default App;

먼저 app 내부 로직을 다 갈아치우고 시작하자.(내부 함수도 다 dispatch 형식으로 바뀔 예정이다.)

initialState 로 컴포넌트에 쓰이는 상태를 한곳에 넣는다. 쓰이는 state가 원래 users와 inputs 였기에 형태 그대로 프로퍼티에 추가해준다.


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) {
  return state;
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { users } = state;
  const { username, email } = state.inputs;

  return (
    <>
      <CreateUser username={username} email={email} />
      <UserList users={users} />
      <div>활성사용자 수 : 0</div>
    </>
  );
}

export default App;

그리고 기본 reducer와 useReducer 를 정의해준다.

또 state.뭐시기 로 접근하면 코드가 길어지니 비구조화 할당으로 state 내부 프로퍼티들을 추출해서 컴포넌트에 props 로 넣어준다.

그리고 이제 차례차례 onChange, onCreate, onToggle을 만들어줄 차례다.

onChange 먼저 드가자

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
        }
      };
    default:
      return state;
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { users } = state;
  const { username, email } = state.inputs;

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

  return (
    <>
      <CreateUser username={username} email={email} onChange={onChange} />
      <UserList users={users} />
      <div>활성사용자 수 : 0</div>
    </>
  );
}

export default App;

먼저 type 을 CHANGE_INPUT 으로 정의한다.
그리고 onChange 함수를 정의해야한다.

const { name, value } = e.target;
dispatch({
      type: 'CHANGE_INPUT',
      name,
      value
    });
  }

그전과 같이 타켓이 변해서 받은 e.target 의 name 과 value 를 비구조화 할당으로 받고
dispatch를 해줄 때 action 에 타입과 필요한 변수인 name, value를 전달해준다.

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

그 이후 reducer 에 인풋이 type 으로 바뀔 때 리턴할 것들을 정의해준다.
...state,
inputs: {
...state.inputs,

      [action.name]: action.value
    }

불변성 지키면서 state 를 복사해서, 그안의 input의 정보도 복사하고 action.name 으로 받은 username 이나 email 프로퍼티의 값을 action.value 로 바꿔준다.

<CreateUser username={username} email={email} onChange={onChange} />

그리고 CreateUser에 props 를 비구조화 할당으로 전달해준다.

다음은 onCreate이다.

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)
      };
    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]);

  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} />
      <div>활성사용자 수 : 0</div>
    </>
  );
}

export default App;

먼저 action 에서 type 을 "CREATE_USER" 로 정의한다.
그리고 onCreate 함수를 만들어보자.

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

현재 가지고 있는 usernmae과 email 을 그대로 사용하면 된다.
dispatch 안의 action은 CREATE_USER 라는 타입과 함께, 생성할 user 를 id, username, email 을 가지게 객체로 만든다음 action 에 담아 전송한다.
그리고 id를 증가시켜준다.
다음은 reducer이다.

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

생성하면 input은 초기화 시켜야 하므로, 초기화상태의 inputs 를 initialState 에서 받아와서 할당해주고, users 는 불변성 유지를 위해 concat 함수를 써서 action 을 통해 받아온 user 를 users에 추가해준다.

<CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />

그리고 props 에 onCreate 를 넣어준다.

다음은 onToggle, onRemove 이다.

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
    });
  }, []);

  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onToggle={onToggle} onRemove={onRemove} />
      <div>활성사용자 수 : 0</div>
    </>
  );
}

export default App;

이전과 같이 onToggle과 onRemove 의 타입을 각각 TOGGLE_USER, REMOVE_USER 로 정의해준다.
그리고 onToggle onRemove 함수를 정의해준다.

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

onToggle 함수는 현재 내가 토글 한 user 의 id 가 필요하기에 type과 함께 id를 action 에 넣어준다.

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

onRemove 도 동일하다.

그 이후 reducer 를 작업해준다.

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)
      )

이러면 userReducer 만으로 컴포넌트 상태관리를 할 수 있다. 굿!!!!!!!!!!!!!!!!!!!

profile
무지막지한사람이될거야

0개의 댓글