[React] useState와 useReducer 언제 사용할까?

@eunjios·2023년 10월 2일
1
post-thumbnail

왜 useReducer를 쓸까?

useStateuseReducer 모두 상태 관리를 위해 사용한다. 그러면 구현이 간단한 useState 를 쓰는 것이 좋지 않을까? 왜 굳이 useReducer를 써야할까?

1. 복잡한 상태 관리가 가능하다.

useState 는 하나의 상태만 관리한다. 그러나 useReducer 는 여러 상태를 함께 관리한다. 따라서 상태가 서로 dependent 할 경우 useReducer 를 사용하는 것이 좋다.

ex) form의 입력 값과 그 입력 값의 유효성을 모두 상태로 관리해야 하는 경우


2. 최신 상태를 보장한다.

useState 는 화살표 함수로 이전 상태를 받아서 현재 상태를 업데이트 해야 최신 상태를 보장할 수 있었다. 반면 useReducer 는 최신 상태를 보장하기 때문에 더 간단히 코드를 작성할 수 있다.


3. How 와 What 을 분리할 수 있다.

예를 들어 login 이라는 기능을 구현한다고 해보자.

login 과 관련된 상태를 useState 로 관리할 경우, 다음과 같이 login이 성공할 때와 실패할 때 각각의 경우에 대해 상태를 업데이트 하는 로직을 컴포넌트 내에 작성하게 된다.

function Login() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [isLoading, showLoader] = useState(false);
  const [error, setError] = useState('');
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  const onSubmit = async (e) => {
    e.preventDefault();
	
    // 로그인 상태 초기화
    setError('');
    showLoader(true);

    try {
      await login({ username, password });
      // 로그인 성공
      setIsLoggedIn(true);
    } catch (error) {
      // 로그인 실패
      setError('Incorrect username or password!');
      showLoader(false);
      setUsername('');
      setPassword('');
    }
  };

  return; // UI 관련 코드 
}

반면, useReducer 를 사용하면 다음과 같이 코드를 작성할 수 있다. 즉, Login 이라는 컴포넌트에는 상태를 어떻게 업데이트 할지에 관한 로직을 없앨 수 있다.

function Login() {
  const [state, dispatch] = useReducer(loginReducer, initialState);
  const { username, password, isLoading, error, isLoggedIn } = state;

  const onSubmit = async (e) => {
    e.preventDefault();
    
    // 로그인 상태 초기화
    dispatch({ type: 'login' });

    try {
      await login({ username, password });
      // 로그인 성공 
      dispatch({ type: 'success' });
    } catch (error) {
      // 로그인 실패
      dispatch({ type: 'error' });
    }
  };

  return; // UI 관련 코드 
}

리듀서는 별도의 함수로 다음과 같이 구현된다.

function loginReducer(state, action) {
  switch (action.type) {
    case 'field': {
      return {
        ...state,
        [action.fieldName]: action.payload,
      };
    }
    case 'login': {
      return {
        ...state,
        error: '',
        isLoading: true,
      };
    }
    case 'success': {
      return {
        ...state,
        isLoggedIn: true,
        isLoading: false,
      };
    }
    case 'error': {
      return {
        ...state,
        error: 'Incorrect username or password!',
        isLoggedIn: false,
        isLoading: false,
        username: '',
        password: '',
      };
    }
    case 'logOut': {
      return {
        ...state,
        isLoggedIn: false,
      };
    }
    default:
      return state;
  }
}

const initialState = {
  username: '',
  password: '',
  isLoading: false,
  error: '',
  isLoggedIn: false,
};

React는 선언적 프로그래밍 방식을 따른다. 즉, 어떻게 (how) 가 아니라 무엇 (what) 을 구현할 것인지가 중요하다. 이러한 관점에서 useReducer 는 how 와 what 을 분리하여 더 나은 프로그래밍을 가능하게 한다.

예제 코드

코드로 어떤 차이가 있는지 알아보자.

(1) 카운터

-1 버튼을 누르면 빼기 1,
+1 버튼을 누르면 더하기 1이 되는 간단한 카운터 예제

counter-preview

useState

import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  const plusClickHandler = () => {
    setCount((prev) => prev + 1);
  };

  const minusClickHandler = () => {
    setCount((prev) => prev - 1);
  };

  return (
    <div className="App">
      <h2>{count}</h2>
      <button onClick={minusClickHandler}>- 1</button>
      <button onClick={plusClickHandler}>+ 1</button>
    </div>
  );
};

export default Counter;

useReducer

import React, { useReducer } from "react";

const countReducer = (state, action) => {
  switch (action.type) {
    case "ADD":
      return state + 1;
    case "SUB":
      return state - 1;
    default:
      throw new Error("error");
  }
};

const Counter = () => {
  const [countState, dispatchCount] = useReducer(countReducer, 0);

  const plusClickHandler = () => {
    dispatchCount({ type: "ADD" });
  };

  const minusClickHandler = () => {
    dispatchCount({ type: "SUB" });
  };

  return (
    <div className="App">
      <h2>{countState}</h2>
      <button onClick={minusClickHandler}>- 1</button>
      <button onClick={plusClickHandler}>+ 1</button>
    </div>
  );
};

export default Counter;

(2) 모달창

버튼에 따라 다른 모달창을 띄우는 예제

modal-preview

useState

App.js

import { useState } from "react";
import "./styles.css";
import Modal from "./Modal";

export default function App() {
  const [isOpen, setIsOpen] = useState(false);
  const [modalType, setModalType] = useState("");
  const [modalContent, setModalContent] = useState("");

  const openHandler = (type, content) => {
    setIsOpen(true);
    setModalType(type);
    setModalContent(content);
  };

  const closeHandler = () => {
    setIsOpen(false);
    setModalType("");
    setModalContent("");
  };

  return (
    <>
      <button onClick={() => openHandler("error", "에러 발생")}>
        에러 모달창 열기
      </button>
      <button onClick={() => openHandler("confirm", "확인")}>
        확인 모달창 열기
      </button>
      {isOpen && (
        <Modal
          title={modalType}
          content={modalContent}
          closeHandler={closeHandler}
        />
      )}
    </>
  );
}

Modal.js

import React from "react";
import "./styles.css";

const Modal = ({ title, content, closeHandler }) => {
  console.log(title, content);
  return (
    <>
      <div className="backdrop" />
      <div className="modal">
        <h2>{title}</h2>
        <div className="content">{content}</div>
        <button onClick={closeHandler}>닫기</button>
      </div>
    </>
  );
};

export default Modal;

useReducer

App.js

import { useReducer } from "react";
import "./styles.css";
import Modal from "./Modal";

const initialModalState = {
  isOpen: false,
  modalType: "",
  modalContent: ""
};

const modalReducer = (state, action) => {
  switch (action.type) {
    case "OPEN_MODAL":
      return {
        isOpen: true,
        modalType: action.modalType,
        modalContent: action.modalContent
      };
    case "CLOSE_MODAL":
      return {
        isOpen: false,
        modalType: "",
        modalContent: ""
      };
    default:
      throw new Error("error");
  }
};

export default function App() {
  const [modalState, dispatchModal] = useReducer(
    modalReducer,
    initialModalState
  );

  const openHandler = (type, content) => {
    dispatchModal({
      type: "OPEN_MODAL",
      modalType: type,
      modalContent: content
    });
  };

  const closeHandler = () => {
    dispatchModal({ type: "CLOSE_MODAL" });
  };

  return (
    <>
      <button onClick={() => openHandler("error", "에러 발생")}>
        에러 모달창 열기
      </button>
      <button onClick={() => openHandler("confirm", "확인")}>
        확인 모달창 열기
      </button>
      {modalState.isOpen && (
        <Modal
          title={modalState.modalType}
          content={modalState.modalContent}
          closeHandler={closeHandler}
        />
      )}
    </>
  );
}

Modal.js

import React from "react";
import "./styles.css";

const Modal = ({ title, content, closeHandler }) => {
  console.log(title, content);
  return (
    <>
      <div className="backdrop" />
      <div className="modal">
        <h2>{title}</h2>
        <div className="content">{content}</div>
        <button onClick={closeHandler}>닫기</button>
      </div>
    </>
  );
};

export default Modal;

References

profile
growth

0개의 댓글