책 읽다가 코딩하다 죽을래

React useRedcuer를 실습으로 알아보기 (useReducer) 본문

코딩/react

React useRedcuer를 실습으로 알아보기 (useReducer)

ABlue 2021. 7. 4. 16:55

오늘은 setState의 대체 함수인 useReducer에 대해 배워보겠다.

 

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

useReducer의 원형은 이렇게 생겼으며

첫 번째 인자인 reducer는 action이며

두 번째 인자인 initalArg은 초기 state 값이다.

세 번째 인자는 state를 지연시켜서 설정하는데 쓰이는 인자인데

첫 번째 인자와 두 번째 인자만 알아도 useReducer를 사용할 수 있으니 이번 수업에는 세 번째 인자는 다루지 않을 것이다.

 

우리는 오늘 회원가입 폼을 활용함으로써 useReducer를 실습해볼 것이다.

일단 회원가입 폼을 만들어보자

일일이 다 만들 생각은 없고 antd design에서 폼을 빌려올 것이다.

 

 

 

import React from 'react';
import { Form, Input, Button } from 'antd';

const LoginForm = () => {
  let inputIDValue = '';
  let inputPasswordValue = '';
  const inputID = useRef('');
  const inputPassword = useRef('');

  const onChangeID = (e) => {
    inputIDValue = e.target.value;
  }
  const onChangePassword = (e) => {
    inputPasswordValue = e.target.value;
  }

  return (
    <>
      <Form
        layout={'horizontal'}
        style={{ display:'flex', flexDirection:'column', justifyContent:"center", alignItems:"center", width: '100vw', height:'100vh'}}
      >
        <Form.Item label="회원가입" name="layout">
        </Form.Item>
        <Form.Item label="아이디">
          <Input onChange={onChangeID} ref={inputID} placeholder="아이디를 입력하세요." />
        </Form.Item>
        <Form.Item label="비밀번호">
          <Input onChange={onChangePassword} ref={inputPassword} placeholder="비밀번호를 입력하세요" />
        </Form.Item>
        <Form.Item>
          <Button style={{ margin: '10px' }} type="primary">Submit</Button>
          <Button style={{ margin: '10px' }} type="primary">reset</Button>
        </Form.Item>
      </Form>
    </>
  );
};
export default LoginForm

 

 

 

결과물

코드의 결과물은 이렇게 한가운데에 회원가입 폼이 만들어진다.

 

우리는 여기서 state를 두 개 만들 것이다.

바로 ID값과 Password 이 두 가지 값들을 저장할

myID와 myPassword이다.

이 두가지 값들은 초기에는 빈 값인 '' 로 설정해야 한다.

여기서 초기 state값은 아까 뭐라고 했었는가

Redeurd의 두 번째 인자였으니 두번째 인자의 이름을 사용해서 initialState를 객체형으로 컴포넌트 바깥에서 선언해준다.

 

const initialState = {
  myID: '',
  myPassword: '',
}

그리고 다음은 reducer를 선언할 차례이다.

 

const reducer = (state, action) => {
    switch(action.type){
      
    }
  }

reducer는 보통 switch문으로 구성된다.

if문으로 설정할 수도 있지만 각 action마다 수행할 로직이 다르므로

switch가 더 가독성이 있기 때문에 이를 더 선호한다.

break문은 필요 없다. 밑을 보면 나오겠지만 return이 대신해 줄 것이다.

 

우리는 reducer에서 state가 어떻게 바뀔지 정하는 것이다.

그런데 여기서 바뀐다는 것은

state.myID = 'abc@naver.com'

이런 형식으로 직접 대입하는 것이 아니다.

action을 통해 기존 state는 불변성을 지키면서

바꿔져야 할 state만 바뀌는 형식이다.

 

그래서 우리는 초기 state를 정하였으면 어떤 action을 통해 

state가 어떻게 바뀌는지를 정해야 한다.

 

그럼 action이란 것은 무엇인가? action은 이 컴포넌트를 사용하는 사용자의 행동을 말한다. 회원가입 폼에서의 action은유저가 회원가입 양식을 다 완성한 후 submit 버튼을 누르는 action과 양식을 잘못 입력해서 reset 버튼을 누르는 action이 있다.

 

이 action을 통해 전자는 state가 input 태그의 입력한 정보로 초기화해야 하며

후자는 input태그의 입력한 정보를 지우고 빈칸으로 초기화해야 한다.

 

우리는 지금 이 두 가지 action을 useReducer를 통해 구현해 볼 것이다.

 

const SUBMIT_FORM = 'SUBMIT_FORM';
const RESET_FORM = 'RESET_FORM';
const reducer = (state, action) => {
  switch(action.type){
    case SUBMIT_FORM:
      return{
        ...state,
        myID: action.myID,
        myPassword: action.myPassword,
      }
    case RESET_FORM:
      return{
        ...state,
        myID: '',
        myPassword: '',
      }
      default: return state;
    }
  };

 

 

 

action 설정 방법이다. reducer의 입력 인자는 state와 action이 있는데 state는 현재 state를 말한다. 

action은 사용자의 행동을 정의한 것이다.

여기서 각 case문의 return값을 보자면 ...state가 각각 들어있는데

이는 변하지 않는 state들을 말한다.

reducer은 불변성을 지양한다.

그렇기 때문에 사용자의 행동으로 인해 변하는 state들을 제외한 다른 state들은

변하지 않는다는 것을 표현하고 그것을 지키기 위해 ...state 라는 spread 표현식을 써야 한다.

그리고 그 밑에 변하는 state들을 정의해야 한다.

 

action을 정의했으니 이제 이벤트 처리를 정의해주자

 

 

<Button onClick={onSubmitForm} style={{ margin: '10px' }} type="primary">Submit</Button>
<Button onClick={onResetForm} style={{ margin: '10px' }} type="primary">reset</Button>

각각 Submit버튼과 reset버튼의 이벤트를 달아주고

 

 const onSubmitForm = () => {
    dispatch({ type: SUBMIT_FORM, myID: inputIDValue, myPassword: inputPasswordValue })
    inputID.current.state.value = '';
    inputPassword.current.state.value = '';
  }
  const onResetForm = () => {
    dispatch({ type: RESET_FORM })
    inputID.current.state.value = '';
    inputPassword.current.state.value = '';
  }

이벤트 처리를 이렇게 하면 된다.

dispatch 이후에 input창을 깨끗이 하는 로직인데

여기서 dispatch는 비동기 함수이다.

안에 들어가 인자를 reducer에 전달해주는 역할인데,

type은 action의 type을 정하며 그 뒤에 있는 인자들은 어떤 state가 어떤 값으로 변하는지 정의하면 된다.

 

처리과정에 대해 설명하자면

Submit버튼을 누르면

onSubmitForm이벤트가 일어나

dispatch가 비동기적으로 실행되며 안에 인자들이

reducer로 가는 것이다.

state는 현재 state들이고

action.type은 SUBMIT_FORM이기 때문에

case SUBMIT_FORM에 멈춰

변하지 않는 state는 가만히 놔두고

myID와 myPassword만 변하고 나오는 것이다.

지금은 2개의 state밖에 없어서 안보이겠지만 

2개의 state 외에 다른 state들이 있다면 그것들이

...state에 포함이 되는 것이다.

 

실제로 submit버튼을 누르면 react devtools chrome앱을 통해

state가 변하는 것을 볼 수 있다.

 

마찬가지로 reset버튼을 누르면 state가 빈 값으로 변한다

이렇게 사용자의 action을 통해 state들을 변하게 하는 것이 useReducer이다.

정리하자면 이렇다.

사용자의 Action이 생기면 Dispatch가 일어나 다음엔 어떤 로직이 실행되야할지

reducer에서 해당 action을 찾아 실행하는 것이다.

state들을 action별로 나눈 것이라고 생각해도 된다.

 

 

그렇다면 여기서 이런 질문을 할 수 있다.

 

아 그러면 그냥 useState랑 똑같은 거 아닌가요? 뭐 이리 복잡하게 코딩하나요?

 

나도 처음엔 그렇게 생각했지만 useReducer는 보기보다 굉장히 유용하게 쓰인다.

유용한 이점은 여러 가지 있지만 두 가지를 설명해보겠다.

 

첫째 만약 한 파일 안에 state가 10개 20개 이런 식으로 많이 있는 경우를 생각해보자

state가 많이 있는 상태에서 모든 state의 변화를 useState로 사용하면은 보기보다 코드 가독성이 떨어진다.

하지만 action 단위로 state를 바꿔주는 useReducer는 아무리 state들이 많이 있다 해도

가독성이 좋을 수밖에 없다.

가독성이 좋다는 것은 유지보수하기도 쉽다는 뜻이다.

사용자의 행동으로 나뉘기 때문이다.(그래서 보기 좋게 상수로 취급한 이유도 그거이다.)

모듈들이 많은 프로젝트에선 아예 reducer 파일이 따로 있을 정도이다.

 

 

두 번째는 컴포넌트에서 action을 통해 다른 컴포넌트에 state를 바꿀 수 있다.

몰론 dispatch를 props나 redux를 통해 이어줘야겠지만

바꿀 수 있다는 그 방법을 제공하면서 유지보수도 쉽다는 것이다.

 

또한 이 reducer는 redux에 쓰이는 중요한 개념이므로

react를 쓰는 사람은 필히 배워가야한다.

 

단지 단점은 dispatch가 비동기적이다 보니 잘못 다루면 원치 않은 결과가 이루어진다.

그래서 state들이 10개 이하 5개 이하 정도의 수준이면 그냥 useState를 사용하는 것이 편하다.

 

 

 

 

 

 

 

전체 코드

import React, { useReducer, useRef } from 'react';
import { Form, Input, Button } from 'antd';

const initialState = {
  myID: '',
  myPassword: '',
}

const SUBMIT_FORM = 'SUBMIT_FORM';
const RESET_FORM = 'RESET_FORM';
const reducer = (state, action) => {
  switch(action.type){
    case SUBMIT_FORM:
      return{
        ...state,
        myID: action.myID,
        myPassword: action.myPassword,
      }
    case RESET_FORM:
      return{
        ...state,
        myID: '',
        myPassword: '',
      }
      default: return state;
    }
  };
const LoginForm = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  let inputIDValue = '';
  let inputPasswordValue = '';
  const inputID = useRef('');
  const inputPassword = useRef('');

  
  const onChangeID = (e) => {
    inputIDValue = e.target.value;
  }
  const onChangePassword = (e) => {
    inputPasswordValue = e.target.value;
  }

  const onSubmitForm = () => {
    dispatch({ type: SUBMIT_FORM, myID: inputIDValue, myPassword: inputPasswordValue })
    inputID.current.state.value = '';
    inputPassword.current.state.value = '';
  }
  const onResetForm = () => {
    dispatch({ type: RESET_FORM })
    inputID.current.state.value = '';
    inputPassword.current.state.value = '';
  }

  return (
    <>
      <Form
        layout={'horizontal'}
        style={{ display:'flex', flexDirection:'column', justifyContent:"center", alignItems:"center", width: '100vw', height:'100vh'}}
      >
        <Form.Item label="회원가입" name="layout">
        </Form.Item>
        <Form.Item label="아이디">
          <Input onChange={onChangeID} ref={inputID} placeholder="아이디를 입력하세요." />
        </Form.Item>
        <Form.Item label="비밀번호">
          <Input onChange={onChangePassword} ref={inputPassword} placeholder="비밀번호를 입력하세요" />
        </Form.Item>
        <Form.Item>
          <Button onClick={onSubmitForm} style={{ margin: '10px' }} type="primary">Submit</Button>
          <Button onClick={onResetForm} style={{ margin: '10px' }} type="primary">reset</Button>
        </Form.Item>
      </Form>
    </>
  );
};
export default LoginForm