개발놀이터

리액트 useReducer 본문

기타/리액트

리액트 useReducer

마늘냄새폴폴 2024. 6. 15. 11:46

최근 회사에서 리액트를 사용하면서 공부를 해야겠다고 생각이 들었습니다. 리액트의 생명주기나 useState, useEffect, useRef 같은 기본적인 hook은 자세히 공부하지 않고 감으로도 충분히 알겠더라구요. 

 

하지만 회사 서비스에서 사용중인 코드 중에 useReducer라는 hook이 있었는데 이놈을 공부해보았습니다. 

 

useReducer

useReducer는 useState를 대체하는 hook으로서 state, dispatch, reducer, initialize 이렇게 네가지로 이루어져있습니다. 

 

기본적인 구조는 이렇게 생겼습니다. 

 

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

ex)
const [number, sendValue] = useReducer(setNumber, initNumber);

 

state는 useState에서 사용하던 상태값입니다. 그리고 reducer가 useState에서 자주 사용하던 set~ 에 해당하는 함수입니다. 이 함수를 이용해서 상태값을 변경해줄 수 있습니다. 

 

그리고 dispatch는 reducer로 값을 전달해주는 용도로 사용되는 것 같습니다. dispatch가 호출되면 reducer가 호출되는 것 같은데 자세한건 잘 모르겠네요. 마지막으로 initialize는 state의 초기값을 설정해주는 것이죠. 

 

그럼 이제 사용법에 대해서 알아보도록 하겠습니다. 

 

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

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

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

 

우선 initialState로 초기에 state의 포맷을 결정합니다. 그리고 버튼을 클릭할 때마다 dispatch로 reducer에 데이터를 전달합니다. reducer는 두개의 인자를 받도록 되어있는데 첫번째는 state이고 두번째는 dispatch로 넘어오는 데이터값입니다. 

 

보통 action이라고 많이 사용하는 듯 합니다. 

 

위의 예시는 onClick 이벤트로 dispatch를 호출하고 그에따른 값을 세팅해주는 코드입니다. 

 

그런데 이런 예시로는 조금 부족하더군요. 왜냐하면 이정도 예시는 useState를 사용하는 것이 더 깔끔하고 간단하기 때문입니다. 

 

상태의 초기값 세팅해주고, dispatch에 reducer까지 세팅해줘야해서 보일러플레이트가 너무 높습니다. 

 

useReducer는 useState보다 더 유연하게 상태를 관리할 수 있다는 것이 장점입니다. 이러한 장점을 극대화할 수 있는 예제를 만들어봤습니다. 

 

import {useReducer} from 'react'

function Input() {
    const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
    const SET_ERRORS = 'SET_ERRORS';
    const SET_INITIALIZE = 'SET_INITIALIZE';
    const initialValue = {
        name: '',
        password: '',
        email: '',
        errors: {}
    }

    function reducer(state, action) {
        switch (action.type) {
            case SET_FIELD_VALUE:
                return {
                    ...state,
                    [action.field]: action.value
                };
            case SET_ERRORS:
                return {
                    ...state,
                    errors: action.errors
                };
            case SET_INITIALIZE:
                return initialValue;
            default:
                throw new Error('Unknown Action Type');
        }
    }

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

    const handleChange = (e) => {
        dispatch({type: SET_FIELD_VALUE, field: e.target.name, value: e.target.value});
    }

    const handleSubmit = (e) => {
        e.preventDefault();
        const errors = validateForm(state);
        if (Object.keys(errors).length > 0) {
            dispatch({type: SET_ERRORS, errors});
        }
        else {
            console.log('Form submit successfully');
            dispatch({type: SET_INITIALIZE});
        }
    }

    const validateForm = (state) => {
        const errors = {};
        if (!state.name) errors.name = 'Name is required';
        if (!state.email) errors.email = 'Email is required';
        if (!state.password) errors.password = 'Password is required';
        return errors;
    }

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>Name : </label><input type='text' name='name' value={state.name} onChange={handleChange}/>
            </div>
            {state.errors.name && <p>{state.errors.name}</p>}
            <div>
                <label>Password : </label><input type='password' name='password' value={state.password} onChange={handleChange}/>
            </div>
            {state.errors.password && <p>{state.errors.password}</p>}
            <div>
                <label>Email : </label><input type='text' name='email' value={state.email} onChange={handleChange}/>
            </div>
            {state.errors.email && <p>{state.errors.email}</p>}
            <input type='submit' value='전송'/>
        </form>
    )
}

export default Input;

 

이 예제는 흔히 회원가입에서 사용할 수 있는 예제입니다. 이름, 패스워드, 이메일을 입력받고 해당 값이 비어있는 경우 메세지를 띄워주는 예제이죠. 

 

흐름도는 아래와 같습니다. 

 

  1. 사용자가 input 태그에 값을 입력한다. 
  2. onChange 이벤트로 dispatch를 이용해 input 태그의 name, value를 넘겨주고 type을 설정해 넘겨줍니다. 
  3. reducer가 호출되고 dispatch에서 넘어온 데이터를 기반으로 state를 세팅해줍니다.
  4. 사용자가 submit 버튼을 누르면 onSubmit 이벤트가 발생합니다. 
  5. onSubmit 이벤트의 콜백 함수인 handleSubmit이 현재 input태그의 값을 검증합니다. 
  6. 이 과정에서 검증에 실패하면 SET_ERROR type의 dispatch를 보내 다시 reducer를 호출하고 에러 타입과 메세지를 설정합니다. 성공하면 SET_INITIALIZE를 호출하여 모든 상태값을 초기화해줍니다. 

 

마치며

이렇게 useReducer를 이용해서 회원가입 예제를 만들어봤습니다. 사실 useReducer의 상태관리는 useState에 비해 보일러플레이트가 높습니다. 

 

하지만, 조금 더 객체지향적이라고 생각이 드네요. 원래 객체지향이라는 것이 보일러플레이트가 높습니다. 스프링만하더라도 객체지향을 위해 생성해야하는 클래스가 정말 많고 사용해야하는 기능도 정말 많습니다. 

 

프론트보다 백을 더 많이 공부한 제 눈엔 조금 더 보기 편하고 이해하기 쉬웠습니다. 하지만 useReducer가 어느 부분에서 useState에 비해 유연한 상태관리를 할 수 있는지는 아직 감이 잘 안잡혔습니다. 

 

dispatch로 다양한 값을 전달할 수 있고 이를 기반으로 상황을 분리하고 각각에 상황에 맞는 상태를 관리하는 부분이 유연하다고 하는건가 아리송합니다. 

 

보통 백엔드에서 유연하다는 말을 잘 사용하지 않아서 그런가 싶기도 합니다. 해봐야 NoSQL이 RDBMS보다 제약조건이 적어 유연하다 이정도는 들어봤지만말이죠. 백엔드에선 가용성, 확장성같은 말을 더 많이 써서 아직 잘 와닿지않네요. 

 

아무튼 오늘은 리액트의 useReducer를 배워보고 정리하고 공유해봤습니다. 오늘 공부하면서 많은 것을 배운 것 같습니다. 다양한 자바스크립트 문법이나 리액트 문법을 익히는데 아주 유익한 시간이었습니다. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~