[React Basic] React의 Hooks (feat. useReducer)

Joah·2022년 9월 26일
0

React Basic

목록 보기
18/25
post-thumbnail

출처 : 강의

useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트해 주고 싶을 때 사용하는 hook 이다.

리듀서는 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션 값을 전달받아 새로운 상태를 반환하는 함수이다.

리듀서 함수에서 새로운 상태를 만들 때는 반드시 불변성을 지켜주어야 한다.

useReducer는 state처럼 상태를 생성하고 관리하는 훅이다.


🛼 useReducer 언제 써야할까?

여러개의 하위 값을 포함하는 복잡한 state를 다루어야 할 때

state = {
	teacher: 'James',
  	students: ['Kim', 'Ann', 'John'],
  	count: 3,
  	locations: [
      {country: 'Korea', name: 'A'},
      {country: 'Aus', name: 'B'},
    ]
}

state가 이런 값을 가질때, useReducer를 사용하면 가독성도 높아지고 유지보수도 편해진다.


🛼 useReducer 구성요소

Dispatch

Action이 담겨 있는 "요구 사항"이다. Action을 Reducer에게 전달하는 역할
ex) 철수가 은행에 200원 출금을 요구한다.

Action

Action은 요구 내용이다.
ex) "200원 출금"

Reducer

컴포넌트의 state를 업데이트 시켜준다.
컴포넌트의 state를 업데이트 시키기 위해서 꼭 reducer에게 요구해야 한다.
ex) 철수의 계좌를 업데이트 시키는 은행

📍 리액트로 정리하면

state를 업데이트 시키기 위해 Dispatch 함수에 인자로 Action을 넣어서 Reducer에게 전달한다. 그럼 Reducer는 컴포넌트의 state를 Action의 내용대로 업데이트 시켜준다.


🛼 간단한 은행앱으로 예시 적용하기

📍 useReducer 선언하기

const ReducerBank = () => {
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      <h2>welcome to Reducer Bank</h2>
      <p>잔고: {money}</p>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value))}
        step="1000"
      />
      <button>예금</button>
      <button>출금</button>
    </div>
  );
};

export default ReducerBank;
  • useReducer는 배열을 반환하는데

    • 첫 번째 요소 : 새로 만들어진 state
    • 두 번째 요소 : dispatch 함수
  • useReducer는 두 가지의 인자를 받는데

    • 첫 번째 인자 : reducer => 추후 작성할 예정
    • 두 번째 인자 : money state의 초깃값

📍 reducer 함수 만들기

컴포넌트 밖에 함수 선언한다.

const reducer = (state, action) => {};

const ReducerBank = () => {
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);

  return (
    <div>
    	...
		<p>잔고 : {money}</p>
    </div>
  );
};

export default ReducerBank;
  • reducer 함수는 두 개의 인자를 받는데
    • 첫 번째 인자 : useReducer에서 선언한 현재 state 즉, money
    • 두 번째 인자 : action

useReducer를 통해서 money라는 state를 만들었고 money state는 선언한 reducer 함수에 의해서만 업데이트가 가능하다.

useReducer의 인자로 reducer 함수가 들어가져 있으며 money 를 업데이트 할때마다 dispatch에게 action을 전달해달라고 부를 것이다.

dispatch를 부르면 reducer 함수가 호출된다. reducer에는 action이 전달되기 때문에 money를 action대로 업데이트 시킬 수 있다.


📍 reducer 함수 작동 확인하기

//dispatch가 호출되면서 reducer 함수 호출
const reducer = (state, action) => {
  console.log("reducer가 일을 합니다.", state, action);
};

const ReducerBank = () => {
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      ...
      //버튼클릭해서 dispatch 호출
      <button
        onClick={() => {
          dispatch();
        }}
      >
        예금
      </button>
      <button>출금</button>
    </div>
  );
};

export default ReducerBank;

  • 예금 버튼을 클릭하면 dispatch가 호출되고 reducer가 호출되어 콘솔에 현재 state 값인 0과 아무런 정의를 하지 않은 action이 출력되도록 작성했다. 왜냐하면 dispatch의 인자로 아무것도 넣어주지 않았기 때문!

  • 그 후 한 번 더 예금 버튼을 클릭하면 state도 undefined로 출력된다. 왜냐하면 state는 action대로 업데이트 되는데 action이 undefined였기 때문이다.


📍 action 전달하기

action은 보통 객체 형태로 전달된다.

const reducer = (state, action) => {
  console.log("reducer가 일을 합니다.", state, action);
};

const ReducerBank = () => {
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      <h2>welcome to Reducer Bank</h2>
      <p>잔고: {money}</p>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value))}
        step="1000"
      />
      <button
        onClick={() => {
          dispatch({ type: "deposit", payload: number });
        }}
      >
        예금
      </button>
      <button>출금</button>
    </div>
  );
};

export default ReducerBank;

action : {type : "deposit", payload: number}

type은 action 행위에 대한 정의이다.
여기서는 예금이기 때문에 "deposit" 이라고 정의한다.

payload는 action에서 전송되는 데이터를 의미한다.
현재 input에 있는 값을 전달해야하기 때문에 number


📍 reducer로 state 업데이트 하기

const reducer = (state, action) => {
  console.log("reducer가 일을 합니다.", state, action);
  return state + action.payload;
};

const ReducerBank = () => {
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);

  return (
    <div>
		...
      <button
        onClick={() => {
          dispatch({ type: "deposit", payload: number });
        }}
      >
        예금
      </button>
      <button>출금</button>
    </div>
  );
};

export default ReducerBank;

reducer가 리턴하는 값은 새로 업데이트 될 state의 값이다.

예금같은 경우에는 현재 state인 money에서 action의 payload 즉, input에서 전달받은 number를 더해주는 것!
return state + action.payload;

현재 money의 초깃값이 0이기 때문에 input에 3000을 입력하여 예금 버튼을 누르면 money의 값이 3000으로 업데이트 된다.

useReducer도 useState와 마찬가지로 state가 업데이트 되면 화면을 렌더링한다.


📍 출금하기

return state + action.payload;
이렇게 작성하면 항상 예금만 리턴하기 때문에 출금을 할 수 없다.

따라서 action의 type에 따라 달리 적용되게 한다.

보통 reducer 함수 안에 if ~ else문을 사용하거나 switch 문을 사용한다.

const reducer = (state, action) => {
  //action type에 따라 조건문 작성
  switch (action.type) {
    case "deposit":
      return state + action.payload;
    case "withdraw":
      return state - action.payload;
    default:
      return state;
  }
};

const ReducerBank = () => {
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      <h2>welcome to Reducer Bank</h2>
      <p>잔고: {money}</p>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value))}
        step="1000"
      />
      <button
        onClick={() => {
          dispatch({ type: "deposit", payload: number });
        }}
      >
        예금
      </button>
      <button
        onClick={() => {
          dispatch({ type: "withdraw", payload: number });
        }}
      >
        출금
      </button>
    </div>
  );
};

export default ReducerBank;

만약 타입을 deposit, widthdraw 외에 지정하게 되면 default 값으로 현재 state값이 화면에 렌더링 된다.

useReducer의 강점은 reducer가 action 대로만 state를 업데이트 시켜주는 것이다. 만약 엉뚱한 type의 action을 보낸다면 reducer는 아무일도 하지 않고 이전의 state를 렌더링하여 실수를 방지할 수 있다.


📍 깔끔한 코드로 리팩토링

action의 type을 상수 데이터로 정의한다.

const ACTION_TYPE = {
  deposit: "deposit",
  withdraw: "withdraw",
};

const reducer = (state, action) => {
  console.log("reducer가 일합니다.", state, action);
  switch (action.type) {
    case ACTION_TYPE.deposit:
      return state + action.payload;
    case ACTION_TYPE.withdraw:
      return state - action.payload;
    default:
      return state;
  }
};

const ReducerBank = () => {
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      <h2>welcome to Reducer Bank</h2>
      <p>잔고: {money}</p>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value))}
        step="1000"
      />
      <button
        onClick={() => {
          dispatch({ type: ACTION_TYPE.deposit, payload: number });
        }}
      >
        예금
      </button>
      <button
        onClick={() => {
          dispatch({ type: ACTION_TYPE.withdraw, payload: number });
        }}
      >
        출금
      </button>
    </div>
  );
};

export default ReducerBank;

이렇게 수정하면 "deposit" 값을 수정하면 나머지 값들도 자동으로 수정되기 때문에 관리하기 쉽다.

사실 money는 useReducer를 사용하기에는 굳이..? 라고 할 정도로 작은 데이터이다. 차라리 useState를 사용하는 편이 나을 수도 있다.

profile
Front-end Developer

0개의 댓글