[React] useState 이해하고 활용해보기

alswjd·2025년 3월 21일

React Hooks 탐험기

목록 보기
2/5
post-thumbnail

React를 이용해 프로젝트를 개발하다보면, 여러 데이터들을 많이 다루게 되고 useState 같은 상태 관리 Hook들을 많이 사용하고는 한다.
이 과정에서, '상태 관리'란 무엇인지,
그리고 대표적인 상태 관리 Hook인 useState의 동작 원리는 무엇이며 어떻게 활용하면 좋을지에 대한 글을 작성해보고자 한다.

🧾 참고 문서 :
https://ko.react.dev/learn/state-a-components-memory
https://ko.react.dev/reference/react/useState
https://ko.react.dev/learn/state-as-a-snapshot
https://ko.react.dev/learn/queueing-a-series-of-state-updates

🤔 '상태'란 무엇인가?

  • 애플리케이션에서의 상태 즉, state
    컴포넌트의 렌더링과 동작에 영향을 미치는 데이터를 뜻함
    - 쉽게 말해, 컴포넌트의 현재 상황을 나타내는 값임.
    - ex) 버튼의 클릭 여부, 사용자가 input에 입력한 텍스트, 서버에서 받은 데이터
  • 컴포넌트가 렌더링 간에 어떤 정보를 “기억”해야 할 때 state 변수 사용

💡 상태의 종류

1. 지역 상태 (Local State)

  • 컴포넌트 자체적으로 내부 상태 관리
  • 해당 state를 다른 컴포넌트에 전달하려면, props를 사용해야 함
  • useState, useReducer Hook을 사용하여 상태 관리

2. 전역 상태 (Global State)

  • 여러 컴포넌트, 더 넓게는 애플리케이션 전체에서 상태 관리
  • useContext Hook을 사용하여 상태 관리
  • Redux, Recoil 같은 상태 관리 라이브러리를 이용하기도 함
    => 추후 포스트에서 다룰 예정

💡 상태 관리란?

  • '상태 관리'는 리액트에서 중요한 개념으로, 변화하는 데이터를 효율적으로 관리하는 것.

상태 관리가 중요한 이유

  • 상태가 변경될 때마다 컴포넌트를 다시 렌더링하고 UI를 업데이트해야 하기 때문
    • ex) 버튼 클릭 -> 버튼 관련 state 변경 -> UI 업데이트

✅ useState

  • 컴포넌트에 state 변수를 추가할 수 있는 React Hook
  • 가장 기본적인 Hook으로, 함수형 컴포넌트에서 상태 관리할 때 사용

📌 사용 방법 요약

  1. 상태 선언하기
const [state, setState] = useState(initialState);
  1. 상태 업데이트
setState(변경할 값);
  1. UI에 상태 반영
<div>{state}</div>

💡 useState(initialState)

매개변수

initialState: state의 초기 설정값. 어떤 값이든 지정 가능

  • 만약 초기 설정값을 함수로 지정한 경우, 초기 렌더링 이후에는 무시됨.
    • 즉, 이후 렌더링에서는 함수를 다시 호출하지 않고, 이미 초기화된 상태를 재사용함.
    • 이 때, 이 함수는
      1. 인수를 반환하지 않고
      2. 순수함수(=외부 상태 의존하지 않고 예측 가능한 값 반환)여야 함.
    • 🔥 주의점 :
      아래와 같이 함수 자체(createInitialTodos)가 아닌, 함수 호출 결과(createInitialTodos())를 초기 설정값으로 넣은 경우, 렌더링 발생할 때마다 매번 함수를 호출하게 되므로 성능이 저하됨!
      function TodoList() {
      const [todos, setTodos] = useState(createInitialTodos());
      // ...

반환값

  1. current state: state. 첫 번째 렌더링 중에는 initialState가 들어감.
  2. set function: setState 함수. state를 다른 값으로 업데이트하고 리렌더링 발생시킴.

주의 사항

  • useState는 Hook이므로 컴포넌트의 최상위 레벨이나 직접 만든 Hook에서만 호출 가능.
    • 반복문이나 조건문 안에서는 호출 불가. 필요한 경우 새 컴포넌트를 만들어서 옮겨야 함

💡 set function

  • setSomething(nextState) 과 같이 사용
  • 사용 방법 2가지
  1. 업데이트 할 값을 직접적으로 전달
setName('Taylor');
  1. 이전 state를 바탕으로 계산하는 함수를 전달
setAge(a => a + 1);

매개변수

nextState: state가 될 값. 모든 데이터 타입이 허용 가능하나,

  • 함수를 nextState로 전달하면 업데이터 함수(=이전 state 바탕으로 계산하는 함수)로 취급함.
    • 이 함수는 순수해야 하고(=외부 상태 의존하지 않고 예측 가능한 값 반환)
    • 대기 중인 state를 유일한 인수로 사용해야 하며,
    • 다음 state를 반환해야 함.
  • React는 업데이터 함수를 대기열에 넣고 컴포넌트를 리렌더링 합니다. 다음 렌더링 중에 React는 대기열에 있는 모든 업데이터를 이전 state에 적용하여 다음 state를 계산합니다.

반환값

반환값 없음.

주의 사항

  • set function은 다음 렌더링을 위해서 state를 업데이트함.

    • 따라서 set function을 호출한 후 바로 state를 읽으면 호출 이전 값을 얻게 됨.
  • 새로 전달한 값이 현재 state와 동일할 경우 React는 최적화를 위해 컴포넌트 리렌더링을 하지 않음

  • React는 state 업데이트를 일괄적으로(batch) 처리함.

    • 따라서 이벤트 핸들러와 set function이 호출된 후에 화면이 업데이트됨. (하나의 이벤트 중에 여러 번 렌더링 되는 것 방지 위함)

💡 사용 예제

1. 컴포넌트에 state 추가하기

  • 아래 예제는 카운터(숫자)
    - 버튼 한 번 누른 결과

  • 이 외에 TextField(문자열), Checkbox(불리언), Form(2개의 변수)도 가능

import { useState } from 'react'; // useState 가져오기

export default function Counter() {
  const [count, setCount] = useState(0); // 상태 선언하기

  function handleClick() {
    setCount(count + 1); /// 상태 변경하기
  }

  return (
    <button onClick={handleClick}>
      You pressed me {count} times // UI에 상태 반영
    </button>
  );
}

2. 이전 state를 기반으로 state 업데이트하기

  • 아래 예제에서 +3 버튼을 누르면 Your age: 45로 변함
import { useState } from 'react';

export default function Counter() {
  const [age, setAge] = useState(42);

  function increment() {
    setAge(a => a + 1);
  }

  return (
    <>
      <h1>Your age: {age}</h1>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <button onClick={() => {
        increment();
      }}>+1</button>
    </>
  );
}

3. 객체 및 배열 state 업데이트하기

  • state는 읽기 전용으로 간주되므로, state를 아래와 같이 직접 변경하려고 하면 안됨.
    // 🚩 state 안에 있는 객체를 다음과 같이 변경하지 마세요.
    form.firstName = 'Taylor';
  • => state가 객체 및 배열인 경우,
  1. state를 복사하여 새로운 값을 만든 다음, (주로 spread 연산자 ... 사용)
  2. 새로운 값에서 필요한 부분만 변경하여
  3. set function에 추가함
// ✅ 새로운 객체로 state를 교체합니다.
setForm({
  ...form,
  firstName: 'Taylor'
});
  • 다만, 아래 예제와 같이
    Form의 상태가 중첩된 객체로 이루어져 있을 경우,

    import { useState } from 'react';
    
    export default function Form() {
      const [person, setPerson] = useState({
        name: 'Niki de Saint Phalle',
        artwork: {
          title: 'Blue Nana',
          city: 'Hamburg',
          image: 'https://i.imgur.com/Sd1AgUOm.jpg',
        }
      });
    
      return <></>;
    }

    • 바꾸고 싶은 값이 객체 안의 깊숙한 곳에 있으면,
      ex) person state

      person
      └── artwork
        └── title  ← 이걸 바꾸고 싶다!
      

      아래와 같이 ❗ 그 값을 포함하고 있는 모든 객체 ❗ 들을 차례로 복사해야 함.

      function handleTitleChange(e) {
        setPerson({
          ...person,
          artwork: {
            ...person.artwork,
            title: e.target.value
          }
        });
      }

      🤔 Why?

      자바스크립트는 객체를 복사할 때 얕은 복사를 하기 때문.

      즉, 최상위 한 단계까지만 복사하고,
      그 내부에 있는 객체들은 원본과 똑같은 참조를 가지기 때문에
      React는 객체가 변경되었다고 감지하지 않음.

      예시) 만약 최상위 person 객체만 복사할 경우
      const original = {
        name: 'Kim',
        artwork: {
          title: 'Untitled'
        }
      };
      
      const copy = { ...original };
      

      위 코드에서 copyoriginal과는 다른 객체지만, 그 안의 artwork는 같은 객체를 참조함.

      따라서

      copy.artwork.title = 'New Title';
      console.log(original.artwork.title); // 'New Title'
      

      즉, copy.artwork.title을 바꾸면 원래 original.artwork.title도 함께 바뀜
      => 객체 내부의 artwork 까지도 복사해서 사용해야 함!

      😇 대체 방안)

    1. 간결하게 객체 내부까지 복사해주는 Immer 라이브러리 사용
    2. 복잡한 로직을 외부로 분리할 수 있는 useReducer Hook 사용
      • 이후 포스트에서 언급 예정

🚨 TroubleShooting

1. state를 업데이트했지만 로그에는 계속 이전 값이 표시될 때

  • 예시
function handleClick() {
  console.log(count);  // 0

  setCount(count + 1); // 1로 리렌더링 요청합니다.
  console.log(count);  // 아직 0입니다!

  setTimeout(() => {
    console.log(count); // 여기도 0이고요!
  }, 5000);
}
  • 🤔 Why?
    state는 스냅샷 같이 동작하므로, 리렌더링 기준으로 업데이트되기 때문

=> 따라서 set function을 호출했더라도 아직 리렌더링 발생하기 전이라 업데이트된 값이 아닌, 이전 값이 출력됨

  • 😇 해결 방안)
    set function에 변경값을 전달하기 전에, 변경값을 먼저 console.log에 찍어보기

2. state를 업데이트해도 화면이 바뀌지 않을 때

  • 🤔 Why?
    React는 Object.is()로 비교한 뒤 다음 state가 이전 state와 같으면 업데이트를 무시하기 때문
    객체나 배열의 state를 직접 변경할 때 주로 발생
    obj.x = 10;  // 🚩 잘못된 방법: 기존 객체를 변경
    setObj(obj); // 🚩 아무것도 하지 않습니다.
  • 😇 해결 방안)
    객체나 배열 state를 변경하는 대신 항상 교체하기
    • state는 읽기 전용이므로, 직접 교체하면 안됨
      // ✅ 올바른 방법: 새로운 객체 생성
      setObj({
        ...obj, // 이전 state를 복사해서
        x: 10 // 원하는 부분만 수정 후 set function에 전달
      });

3. “Too many re-renders” 에러가 발생할 때

  • 🤔 Why?
    렌더링 중에 state를 변경하려고 한 경우에 발생
    • 렌더링 -> state 설정(렌더링 유발) -> 렌더링 -> state 설정(렌더링 유발) 무한 루프
    • 이벤트 핸들러를 지정하는 과정에서 주로 발생함
      // 🚩 잘못된 방법: 렌더링 동안 핸들러 요청
      return <button onClick={handleClick()}>Click me</button>
  • 😇 해결 방안)
    • 이벤트 핸들러에 함수 호출 결과가 아닌, 함수 자체를 전달
      // ✅ 올바른 방법: 이벤트 핸들러로 전달
      return <button onClick={handleClick}>Click me</button>
    • 인자가 있는 경우, 인라인 함수로 전달
      // ✅ 올바른 방법: **인라인 함수**로 전달
      return <button onClick={(e) => handleClick(e)}>Click me</button>

4. set function을 연속으로 여러 번 실행했는데, state 변경이 한 번 밖에 반영되지 않은 경우

  • 예시

    • set function을 3번 연속으로 실행했지만
      클릭해보면 age는 45가 아니라 43이 됨
    function handleClick() {
      setAge(age + 1); // setAge(42 + 1)
      setAge(age + 1); // setAge(42 + 1)
      setAge(age + 1); // setAge(42 + 1)
    }
  • 🤔 Why?
    한 번의 렌더링에서 발생한 모든 상태 변경을 큐(queue)에 저장한 후
    일괄적으로 처리하기 때문.

    식당에서 웨이터가 손님의 주문을 모두 받은 후 주방에 전달하는 것처럼,
    React는 state 업데이트를 하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다림

  • 😇 해결 방안)
    업데이터 함수를 사용하여
    대기 중인 이전 state를 바탕으로 다음 state를 계산하기

    function handleClick() {
      setAge(a => a + 1); // setAge(42 => 43)
      setAge(a => a + 1); // setAge(43 => 44)
      setAge(a => a + 1); // setAge(44 => 45)
    }

    React는 업데이터 함수를 큐에 넣으면,

    첫 번째 a => a + 1은 대기 중인 state로 42를 받고 다음 state로 43을 반환함.
    두 번째 a => a + 1은 대기 중인 state로 43을 받고 다음 state로 44를 반환함.
    세 번째 a => a + 1은 대기 중인 state로 44를 받고 다음 state로 45를 반환함.

    큐(queue) 목록a반환값
    a => a + 14242 + 1 = 43
    a => a + 14343 + 1 = 44
    a => a + 14444 + 1 = 45
profile
와우 프론트엔드

0개의 댓글