[React] useState, useReducer

SNXWXH·2025년 4월 13일

React

목록 보기
3/3
post-thumbnail

useState

함수형 컴포넌트 내부에서 상태를 정의하고, 해당 상태를 관리할 수 있게 해주는 훅

import { useState } from 'react’

const [state, setState] = useState(initialState);
  • 아무런 값을 넘겨주지 않으면 초기값은 undefined
  • 반환값은 배열
  • 배열의 첫 번째 원소로 state 값 자체를 사용
  • 두번째 원소인 setState 함수를 사용해 해당 state의 값 변경 가능
  • 클로저를 사용하여 setState가 호출된 이후에도 지역변수인 state 참조 가능
  • setState를 사용할 때는 함수형 업데이트 방식을 사용하는 것이 좋음

함수형 업데이트 (Functional Updates)

상태를 업데이트할 때 이전 상태값에 기반하여 새로운 값을 계산하는 방식

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

// 일반적인 업데이트 방식
setCount(1);

// 함수형 업데이트 방식
setCount((prevCount) => prevCount + 1);
  • setState는 비동기적으로 실행되기 때문에, 이전 상태를 참조해서 업데이트할 필요가 있을 때 함수형 업데이트 방식이 유용
  • 동시에 여러 상태 업데이트가 발생하거나, 상태 업데이트가 스케줄링되어 실행되는 상황에서 정확한 이전 값을 기반으로 상태를 변경 가능
  • React는 setState에 함수를 전달하면 자동으로 이전 상태값을 인자로 넘겨줌

함수형 업데이트를 사용하지 않았을 때

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

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
};

// 기대값: 클릭 시 count 2씩 증가
// 실제값: count는 1씩 증가함
  • react는 성능 최적화를 위해 여러 개의 상태 업데이트를 한 번에 처리

    batch 처리(블로그에 자세히 정리해둠)

    ⇒ 동일한 이벤트 루프 안에서 발생하는 상태 업데이트는 하나로 묶어서 처리

  • setState 호출 직후에 state 값이 곧바로 업데이트되지 않기 때문에 바로 state 값을 참조하면 예기치 않은 동작이 생길 수 있음

  • 특히 상태가 빠르게 연속해서 변경되거나, 이벤트 핸들러 안에서 여러 번 상태 변경이 일어나는 경우에는 이전 상태값이 꼬일 수 있음

배열 상태에서의 함수형 업데이트

const [items, setItems] = useState([]);

const addItem = (newItem) => {
  setItems((prevItems) => [...prevItems, newItem]);
};
  • 배열을 업데이트할 때, 이전 배열을 복사해서 새로운 요소 추가
  • 불변성을 유지하면서 업데이트 가능

객체 상태에서의 함수형 업데이트

const [user, setUser] = useState({ name: 'Lee', age: 25 });

const updateAge = () => {
  setUser((prevUser) => ({
    ...prevUser,
    age: prevUser.age + 1,
  }));
};
  • 이전 객체를 spread 연산자로 복사 후 필요한 속성만 업데이트
  • 객체 상태를 안전하게 수정 가능

복잡한 상태(폼 등)에서의 함수형 업데이트

const [form, setForm] = useState({
  name: '',
  email: '',
  password: '',
});

const handleChange = (e) => {
  const { name, value } = e.target;

  setForm((prevForm) => ({
    ...prevForm,
    [name]: value,
  }));
};
  • 여러 필드를 가진 객체 상태를 업데이트할 때 유용
  • 필요한 필드만 선택적으로 수정 가능

게으른 초기화(lazy initialization)

useState에 변수 대신 함수를 넘기는 것

// 일반적인 useState사용
// 바로 값을 집어넣음
const [count, setCount] = useState(
	Number.parselnt(window.localStorage.getltem(cacheKey)),
)

// 게으른 초기화
// 위 코드와의 차이접은 함수를 실행해 값을 반환
const [count , setCount] = useState ( () =>
	Number.parselnt(window.localStorage.getltem(cacheKey)),
)
  • useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용 - 공식문서
  • 해당 함수는 오로지 state가 처음 만들어질 때만 사용

useReducer

useState를 대체할 수 있는 함수로 복잡한 상태관리가 필요한 경우 사용

import React, { useReducer } from "react";

const [state, dispatch] = useReducer(reducer, initialState, init);
  • state 값을 변경하는 시나리오를 제한적으로 두고 이에 대한 변경을 빠르게 확인할 수 있게 하는 것

용어 정리

용어설명
state컴포넌트에서 사용할 상태
dispatch 함수reducer 함수를 실행하는 함수컴포넌트 내에서 state를 업데이트할 때 사용
reducer 함수현재 stateaction을 받아 새로운 state를 반환하는 함수
컴포넌트 외부에서 상태 업데이트 로직을 정의
initialState초기 상태값
init초기 상태를 지연 생성할 때 사용하는 함수 (lazy initialization에 사용됨)

useReducer를 사용할 때 유용한 상황

  1. 복잡한 상태 로직

    • 여러 상태를 하나로 묶어서 관리해야 할 때 유용
    • 여러 액션을 통해 상태를 관리하고, 각 액션마다 다른 처리가 필요할 때

    ex) 대규모 폼에서 각 필드의 상태나 유효성 검사 처리

  2. 상태 변화가 여러 단계로 이루어지는 경우

    • 상태 변화가 여러 번의 변경을 거쳐야 할 때

    ex) 로딩 상태, 성공 상태, 실패 상태 등을 다룰 때

  3. 상태 변화를 추적하고 싶을 때:

    • 각 상태 변경에 대한 로직을 명확히 분리하여 관리할 수 있어 추적이 용이

    ex) 액션 타입에 따라 다르게 처리되는 복잡한 로직이 있을 때

코드 예시 (상태 로직이 복잡한 경우)

const initialState = { count: 0, isLoading: false, error: null };

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'decrement':
      return { ...state, count: state.count - 1 };
    case 'loading':
      return { ...state, isLoading: true };
    case 'error':
      return { ...state, isLoading: false, error: action.payload };
    default:
      return state;
  }
};

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

const handleIncrement = () => {
  dispatch({ type: 'increment' });
};

const handleDecrement = () => {
  dispatch({ type: 'decrement' });
};

const handleLoading = () => {
  dispatch({ type: 'loading' });
};

const handleError = (error) => {
  dispatch({ type: 'error', payload: error });
};

return (
  <div>
    <button onClick={handleIncrement}>Increment</button>
    <button onClick={handleDecrement}>Decrement</button>
    <button onClick={handleLoading}>Start Loading</button>
    <button onClick={() => handleError('Something went wrong!')}>Simulate Error</button>
    <p>Count: {state.count}</p>
    <p>{state.isLoading ? 'Loading...' : ''}</p>
    <p>{state.error ? `Error: ${state.error}` : ''}</p>
  </div>
);
  • 여러 상태(count, isLoading, error)를 하나의 useReducer에서 처리
  • 각 액션에 대해 별도의 로직을 관리할 수 있어 복잡한 상태를 효율적으로 관리할 수 있음
  • 액션을 명확히 구분해서 관리할 수 있기 때문에 가독성이 좋음

useState와 useReducer의 차이점

클로저를 활용하여 state를 관리하는 것은 같음

  • useState: 상태 업데이트가 직관적이고 간단하지만, 상태 관리가 복잡해지면 코드가 길어지거나 불편해질 수 있음
  • useReducer: 상태가 여러 개일 때나 복잡한 로직이 필요한 경우, 액션을 통해 상태를 변경할 수 있어 구조적이고 관리가 쉬워짐
항목useStateuseReducer
목적간단한 상태 관리복잡한 상태 관리 (여러 상태 변화를 다룰 때 유용)
상태 업데이트직접적인 값 변경 (setState)액션을 통해 상태를 업데이트 (dispatch)
초기화 방법초기값을 useState(initialValue)로 직접 설정useReducer(reducer, initialState, init)으로 설정
상태 관리 방식useState는 각 상태가 독립적이고 간단함useReducer는 복잡한 상태를 하나의 상태 객체로 관리
상태 업데이트 흐름상태 업데이트가 즉시 적용reducer 함수에서 상태를 반환하는 방식으로 관리
주요 사용 예시단순한 상태 (카운터, 텍스트 필드 등)여러 상태 변화가 있는 복잡한 애플리케이션 (폼 상태, 리스트 관리 등)

useState와 useReducer 비교

useState 예시

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

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

return <button onClick={increment}>Count: {count}</button>;

useReducer 예시

const initialState = { count: 0 };

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    default:
      return state;
  }
};

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

return <button onClick={() => dispatch({ type: 'increment' })}>Count: {state.count}</button>;
profile
세상은 호락호락하지 않다. 괜찮다. 나도 호락호락하지 않으니까.

0개의 댓글