[Hook] useState

OlMinJe·2025년 9월 2일

React

목록 보기
16/19

리액트 공식 문서를 참고한 정리 내용 (25.08 기준)

컴포넌트에 state 변수를 추가할 수 있는 Hook이다. 쉽게 말하자면 컴포넌트 안에서 값을 기억하고 싶을 때 사용하는 도구이다.

const [state, setState] = useState(initialState)

컴포넌트의 최상위 레벨에서 useState를 호출하여 state 변수를 선언한다.

useState를 호출하면 두 가지를 돌려준다.

  • state 변수: 현재 값
  • set 함수: 값을 바꾸는 함수
import { useState } from 'react';

function MyComponent() {
  const [age, setAge] = useState(28);
  const [name, setName] = useState('Taylor');
  const [todos, setTodos] = useState(() => createTodos());
  // ...

여기에서 제일 중요한 건, 항상 배열 구조 분해를 사용하여 [변수, set변수]과 같은 형태로 사용하는 게 규칙이다.

매개변수

initialState

  • state의 초기값으로, 처음 렌더링될 때만 사용된다.
  • 초기값 대신 초기화 함수로 넣을 수 있다.
    이러한 경우는 성능이 많이 드는 계산을 매번 하지 않으려고 사용하는 방식이다.

반환값

set 함수는 반환값이 없다.


주의사항

set 함수 주의사항

업데이트는 바로 반영되지 않음

setState를 호출하면 그 즉시 state 값이 바뀌는게 아니라, 다음 렌더링 때 새로운 값이 들어간다.
그래서 setState 직후에 state를 콘솔 찍으면 이전 값이 보일 수 있다.


같은 값이면 리렌더링 안 함

새 값이 기존 값과 똑같다고 React가 판단하면(내부적으로 Object.js로 비교), 화면을 다시 그리지 않는다. ⇒ 최적화

업데이트는 모아서 처리 (Batching)

이벤트 안에서 setState를 여러 번 호출해도 React는 모아서 한 번만 렌더링하고, 이렇게 해서 성능을 올린다.

항상 같은 함수

setState 함수는 React가 항상 똑같은 식별자를 주기 때문에, useEffect의 의존성 배열에는 보통 넣지 않는다. (넣어도 상관없지만 의미가 없다.)

렌더링 도중 호출 가능 (특수 케이스)

보통은 이벤트 핸들러나 Effect 안헤서만 setState를 쓰지만, 아주 드물게 렌더링 중에도 호출할 수 있다.
이 경우 React가 현재 출력은 버리고, 새로운 state로 다시 렌더링한다.

개발자 모드에서는 2번 호출된다. (놀라지말고 펜잘큐)

👌🏻 핵심 요약

  • setState즉시 반영되지 않고 → 다음 렌더링 때 적용된다.
  • 같은 값이면 렌더링을 건너뛴다.
  • React는 여러 업데이트를 모아 한 번만 렌더링한다.
  • setState항상 같은 함수라 의존성 배열에서 생략해도 된다.
  • 개발 모드 Strict Mode에서는 2번 실행될 수 있다.

사용법

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

useState로 관리하는 값은 비동기적으로 업데이트된다.
그래서 단순히 setState(state + 1) 같은 식으로 여러 번 호출하면 기대한 값이 나오지 않을 수 있다.

예를 들어 age가 42라고 할 때, 버튼 클릭 시 3번 증가시키고 싶다고 해보자.

function App() {
  const [age, setAge] = useState(42);

  function handleClick() {
    setAge(age + 1);
    setAge(age + 1);
    setAge(age + 1);
    console.log(age); // 여전히 42
  }

  return (
    <>
      <p>나이: {age}</p>
      <button onClick={handleClick}>+3</button>
    </>
  );
}

기대한 값은 42 -> 45이지만, 실제 값은 42 -> 43이다.
이유는 클릭하는 순간 실행 중인 코드에서는 age 값이 여전이 42라서 setAge(42 + 1)이 세 번 호출되어도 결국 43이 된다.

이 문제를 해결하려면 업데이트 함수를 사용해서, 이전 값을 인자로 받아 새로운 값을 계산해서 반환해야 한다.

function App() {
  const [age, setAge] = useState(42);

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

  return (
    <>
      <p>나이: {age}</p>
      <button onClick={handleClick}>+3</button>
    </>
  );
}
  • setState(state + 1) : 실행 시점의 현재 값만 보고 계산 → 여러 번 호출해도 같은 값 적용됨
  • setState(prev => prev + 1) : 이전 값(prev) 을 안전하게 받아서 순차적으로 계산 → 원하는 만큼 증가 가능

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

React의 state는 불변(immutable)이므로, 기존 객체·배열을 직접 수정하지 말고 새 객체·배열을 만들어 교체해야 한다.

setForm({
  ...form,
  firstName: 'Taylor'
});

객체 State 업데이트하기배열 State 업데이트하기 문서 참고하기!


초기 state 다시 생성하지 않기

React에서 useState는 초기값을 한 번만 사용하고, 이후 렌더링에서는 무시한다.
하지만 잘못 작성하면 렌더링할 때마다 불필요하게 초기화 함수가 실행될 수 있어 성능 문제가 생긴다.

// 잘못된 예시 ❌
function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos()); 
  // 렌더링될 때마다 createInitialTodos() 실행됨
}

createInitialTodos()는 첫 렌더링에만 필요하지만, 모든 렌더링마다 불필요하게 실행된다. (큰 배열 생성이나 무거운 계산이 들어있다면 낭비!)

// 올바른 예시 ✅
function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos); 
  // 함수 자체를 전달 → 최초 렌더링 때만 실행
}

함수는 호출 결과가 아니라, 함수 자체를 넘기면 React가 초기 렌더링 때만 실행한다.
그 후에는 다시 실행하지 않고 저장된 state만 사용한다.

👉 useState에 초기값으로 함수 호출 결과가 아니라 함수 자체를 넘기면, 초기 렌더링에만 실행되어 성능 낭비를 막을 수 있다.


key로 state 초기화하기

React에서 key는 단순히 리스트 렌더링할 때만 사용하는게 아니라, 컴포넌트를 아예 새로 만들도록 강제하는 역할도 한다.

어떻게 동작하나? 🤔

  • 컴포넌트에 key를 주면 React는 그 key를 기준으로 해당 컴포넌트를 "같은 것인지, 다른 것인지" 구분한다.
  • key 값이 바뀌면, React는 기존 컴포넌트를 재사용하지 않고 처음부터 새로 만든다.
  • 그래서 컴포넌트 안의 state도 초기화된다.
export default function App() {
  const [version, setVersion] = useState(0);

  function handleReset() {
    setVersion(version + 1); // key를 바꿔줌
  }

  return (
    <>
      <button onClick={handleReset}>Reset</button>
      <Form key={version} /> {/* key가 바뀌면 Form이 새로 생성됨 */}
    </>
  );
}

function Form() {
  const [name, setName] = useState('Taylor');

  return (
    <>
      <input value={name} onChange={e => setName(e.target.value)} />
      <p>Hello, {name}.</p>
    </>
  );
}
  • 버튼을 누르면 version이 바뀌고, Formkey도 달라진다.
  • React는 Form을 새로 만들고, 내부 state(name)도 다시 'Taylor'로 초기화된다.

👉 key를 바꾸면 React가 컴포넌트를 새로 만들기 때문에, 내부 state도 자동으로 초기화된다.


이전 렌더링에서 얻은 정보 저장하기

보통은 이벤트(버튼 클릭 등)에서 state를 업데이트를 하지만, 가끔은 렌더링이 끝난 후에 이전 값과 비교해서 새로운 정보를 저장해야 하는 경우가 있다.(ㅜㅜ)

예를 들어, count라는 값이 바뀔 때 “이전보다 증가했는지 감소했는지”를 알고 싶다면 단순히 count만으로는 알 수 없어서 이전 값(prevCount)을 따로 저장해야 한다.

이럴 때는 prevCount라는 별도의 state를 두고, 렌더링 중에 업데이트해줄 수 있다.

function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null); // 'up' or 'down'

  if (count !== prevCount) {
    setTrend(count > prevCount ? 'up' : 'down');
    setPrevCount(count);
  }

  return (
    <h1>
      {count} ({trend})
    </h1>
  );
}

prevCounttrend를 같이 관리해서, count가 이전보다 커졌는지(up) 줄었는지(down)를 표시할 수 있다.

👉 이전 값과 비교해야 할 때는 prevSomething 같은 state를 따로 두고 관리하면 된다.

중요한 주의 사항 ⚠️
렌더링 중에 set 함수를 쓸 때는 몇 가지 조건이 필요하다.

  1. 조건문 안에서만 호출하기
    if (count !== prevCount) 같은 조건이 있어야 한다. 만약 조건 없이 setPrevCount(count)를 호출하면 무한 렌더링이 발생한다.

  2. 현재 컴포넌트의 state만 업데이트 가능하며, 다른 컴포넌트의 state를 건드리면 에러가 난다.

  3. 변경이 없는 state 업데이트는 허용되지 않으며, 값이 실제로 바뀌지 않으면 React가 업데이트를 무시한다.

  4. 이 규칙은 "렌더링 중에 set 호출이 허용된다"는 게 아니라, 특정 패턴에서만 안전하다는 의미로 예외가 아니다.

--

React state 문제 해결 정리 ✨

State는 바로 업데이트되지 않는다.

그 이유는 state가 스냅샷처럼 동작하기 때문이다.

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

  setCount(count + 1);
  console.log(count); // 여전히 0
}

setCount를 해도 실행 중인 코드 안에서는 state 값이 바로 안 바뀌며, 다음 렌더링 때 새로운 값을 반영한다.

다음 state를 사용하고 싶으면 변수에 따로 저장하거나 함수형 업데이트(setCount(c ⇒ c + 1)를 사용하면 된다.


state를 바꿨는데 화면이 안 바뀐다.

obj.x = 10;  // ❌ 기존 객체를 직접 수정
setObj(obj); // 반영 안 됨

이전 값과 비교(Object.is)해서 같으면 업데이트를 안 하기 때문에, 항상 새로운 객체/배열을 만들어 교체해야 한다.


“렌더링 횟수가 너무 많습니다” 에러

return <button onClick={handleClick()}>Click</button>;

렌더링 도중에 setState가 무한히 호출되면 발생하는 에러로, 핸들러를 실행하지 말고 참조만 전달해야 한다.

return <button onClick={handleClick}>Click</button>;

초기화 함수나 업데이트 함수가 두 번 실행됨

개발 모드의 Strict Mode에서만 나타나는 현상으로, 실제 배포 환경에서는 한 번만 실행되니 걱정은 노논~

개발 환경 전용 동작은 컴포넌트를 순수하게 유지하는 데 도움이 된다!


state에 함수를 저장할 때

const [fn, setFn] = useState(someFunction);

someFunction을 함수 자체가 아니라 “초기화 함수”라고 생각해서 실행하기 때문에, 정말 함수를 저장하려면 () => someFunction 처럼 함수 안에 감싸서 전달해야 한다.

const [fn, setFn] = useState(() => someFunction);
profile
큐트걸

0개의 댓글