React.js

함수형 업데이트와 클로저 이해하기 (심화)

2025. 5. 12. 23:59
setCount((prev) => prev + 1);

리액트를 하다 보면 이러한 형태의 setter 함수를 쓰게 된다. 

이는 `useState`의 setter 함수에서 콜백 형태로 상태를 업데이트 하는 것인데, 이를 '함수형 업데이트' 방식이라고 한다.

 

이번에는 값 기반 업데이트와 함수형 업데이트의 차이, 그리고 함수형 업데이트에 담긴 '클로저'에 대해 이해해보려고 한다. (사실 후자가 목적)

 

 

1. 값 기반 업데이트 (단순히 '새 값'으로 상태 변경)

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

  const action = async () => {
    const n = await new Promise((resolve) => setTimeout(resolve, 1000));
    setCount(count + n);
    setCount(count + n);
    // setCount((prev) => prev + 1);
    // setCount((prev) => prev + 1);
  };

  return (
    <main>
      <p>{count}</p>
      <button onClick={action}>action</button>
    </main>
  );
};

 

이 코드를 실행하면 버튼을 한 번 클릭했을 때, `count`가 1만 올라간다.

이는 리액트의 'Automatic Batching', 줄여서 '배치'라는 특성 때문인데, 리액트는 성능 최적화를 위해 같은 상태가 여러번 업데이트 된다면 이를 모두 하나로 묶어 업데이트한다. 

그래서 여기서는 2번의 `setCount`가 1번의 `count` 업데이트로 배치(batch)된 것이다. 

결과적으로 React는 동일한 상태값으로 두 번 설정하는 것은 무시하고, 결국 `count`는 1만 증가한 것처럼 보이는 것이다.

 

 

✅ React의 Auto Batching이란?

자동 배치 처리는 React 18부터 도입된 기능으로, 여러 개의 setState 호출을 하나의 렌더링으로 묶어 처리해주는 최적화 기법이다.

예전에는 setState가 이벤트 핸들러 안에서만 자동으로 배치 처리되었지만,
React 18부터는 `setTimeout`, `fetch`, `async/await` 등 비동기 코드 안에서도 자동으로 배치 처리된다.

 

=> `setCount(count + 1)`처럼 이전 상태를 기반으로 하지 않는 방식은 동일한 값으로 두 번 업데이트되면 덮어씌워짐.

 

 

 

2. '함수형' 업데이트

다음은 setCount가 이전 상태를 기억하도록 콜백을 사용한 방식이다.

// 위아래 코드는 생략...
const action = async () => {
    const n = await new Promise((resolve) => setTimeout(resolve, 1000));
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
};

이 코드를 실행하면 버튼을 한 번 클릭했을 때 `count`가 2로 누적된다.

여기서 '클로저'의 개념이 적용되는데, `setCount` 함수가 자신이 호출된 당시의 렉시컬 환경을 기억하고 있는 것이다.

당시 렉시컬 환경에 있던 `prev`의 값을 첫번째 `setCount` 내부의 클로저가 기억하고,

그 호출문에서 업데이트된 `prev` 값은 두번째 `setCount`가 호출될 당시의 렉시컬 환경에 담겨있어서 두번째 `setCount`의 내부 클로저가 해당 `prev`를 정상적으로 이어받아 값을 누적시키는 방식이다.

 

 

+ 그럼 prev가 렉시컬 환경에 저장되는 원리는..?

🔍 prev => prev + 1의 작동 원리와 렉시컬 환경

setCount(prev => prev + 1);

이 구문에서 `prev => prev + 1`은 익명 함수(arrow function)이고, `prev`는 이 함수의 매개변수다.

 

📌 여기서의 렉시컬 환경:

  1. 이 함수가 정의되는 순간, 자바스크립트는 이를 하나의 클로저로 간주.
  2. `prev`는 이 함수의 매개변수이자 지역 변수로 자신만의 렉시컬 환경에 저장.
  3. 이후 React가 `setCount`를 실행하면서 이 클로저를 호출.
  4. 그때 React는 내부적으로 현재 상태값을 `prev`에 인자로 전달하여 이 함수를 실행.
  5. 클로저는 자신이 기억하고 있는 환경에서 `prev`를 참조해 계산을 수행.

 

React는 내부적으로 어떻게 현재 상태값을 prev로 전달할까?

function setCount(update) {
  const currentState = ... // 내부적으로 저장된 count의 현재 값

  let nextState;
  if (typeof update === 'function') {
    nextState = update(currentState); // 여기서 currentState가 prev로 전달됨!
  } else {
    nextState = update;
  }

  // 상태 갱신 및 리렌더링
  ...
}

여기서 React는 함수형 업데이트(`setCount(prev => prev + 1);`)가 전달되면 내부적으로 `update(currentState)`를 호출하고, 이때 `currentState`가 `prev` 인자로 전달된다.

그래서 `prev => prev + 1`의 prev는 React가 넘겨준 현재 상태값이 되는 것이다.

 

 

다시 본론으로 돌아와서,

setCount((prev) => prev + 1); // prev: 0
setCount((prev) => prev + 1); // prev: 1

여기서 `setCount((prev) => prev + 1)` 내부의 `(prev) => prev + 1`이 클로저이고,

클로저는 `prev`라는 변수로 최신 상태값에 접근할 수 있게 해준다.

따라서 이렇게 두 번 호출해도 상태가 누적되어 정확하게 증가하는 이유는, 각각의 클로저가 최신 상태값을 기억하고 참조하고 있기 때문이다.

 

 

✅ 정리

  • `setCount(count + 1)` 방식은 리액트의 auto batching으로 인해 두 번 호출해도, 최종적으로 한 번만 실행된 것처럼 보인다.
    (상태 업데이트 함수는 여러 번 실행됐지만, React는 마지막 상태만 반영함)
  • `setCount(prev => prev + 1)`는 각 호출이 최신 상태(prev)를 기반으로 동작하기 때문에 누적으로 업데이트가 된다.
  • 따라서 함수형 업데이트의 prev는 현재 상태값이라고 요약할 수 있다.

 

 

 

⭐️ setState와 클로저의 관계는?

핵심 요점: setState는 상태를 보존하는 클로저다. (setState === 클로저)

React는 컴포넌트마다 자체적인 상태 저장소를 만들고, 그 안에 상태를 저장한다. setState는 그 저장소와 연결된 "클로저"다.

 

React 내부를 간단히 모델링하면 다음과 같다. (실제 코드 X)

function useState(initialValue) {
  let state = initialValue;

  function setState(newState) {
    if (typeof newState === 'function') {
      state = newState(state); // 함수형 업데이트
    } else {
      state = newState;        // 값으로 직접 업데이트
    }

    render(); // 상태 바뀌었으니 리렌더링!
  }

  return [state, setState]; // 이때 setState는 state에 접근 가능한 클로저
}

여기서 주목할 부분은,

  • setState는 state를 자신의 상위 렉시컬 환경에서 참조한다.
  • 그래서 state가 함수 바깥으로 나가도 사라지지 않고, 계속 유지된다.
  • 이렇게 구현된 setState가 바로 클로저다.

 

 

✅ 실제 React에서는 이렇게 관리된다

  • React는 각 컴포넌트 인스턴스마다 "상태 슬롯" 같은 구조를 갖고 있고,
  • setState는 이 슬롯을 가리키는 포인터 역할을 하는 클로저이다.

즉, 컴포넌트가 여러번 리렌더링되어도 setState는 항상 같은 상태 슬롯(컴포넌트)을 참조하기 때문에 상태 업데이트가 안전하게 이뤄질 수 있다.

 

🎯 useState가 클로저로 만들어진 이유

상태 유지(캡슐화) 상태가 컴포넌트 바깥으로 나가지 않고 안전하게 캡슐화됨
함수형 업데이트 지원 `prev => prev + 1` 같은 클로저 기반 상태 계산 가능
컴포넌트 독립성 각 컴포넌트마다 상태가 분리되어 격리 가능

 

 

⚖️ 언제 함수형 업데이트를 해야 할까?

상황 함수형 업데이트 필요 여부
단순히 특정 값으로 상태를 바꿈 ❌ 필요 없음 → `setCount(3)` 같이 직접 대입해도 OK
이전 상태를 기반으로 새 상태를 계산 ✅ 꼭 사용해야 함 → `setCount(prev => prev + 1)`
여러 개의 상태 업데이트가 연달아 일어날 수 있는 경우 (ex. 이벤트 핸들러 내부) ✅ 권장
비동기 로직 안에서 상태를 업데이트할 때 ✅ 강력히 권장

 

 

 

+ ”prev는 이 함수의 매개변수이자 지역 변수로 자신만의 렉시컬 환경에 저장된다”의 뜻

이는 자바스크립트 엔진이 함수 실행 시 어떻게 변수들을 다루는지에 대한 내용이다.

예를 들어,

const fn = (prev) => {
  return prev + 1;
};

 

이 함수가 실행되면, 자바스크립트는 다음과 같은 단계를 거친다.

  1. 함수 실행 컨텍스트 생성
    • 실행에 필요한 정보를 담은 공간
  2. 렉시컬 환경 생성
    • 이 함수에서 사용할 지역 변수, 매개변수(prev), 상위 스코프를 참조할 수 있게 구성
  3. prev에 인자 값 저장
    • 예: fn(2)라고 하면, 렉시컬 환경 내 prev = 2로 저장

👉 즉, prev는 이 함수의 매개변수이자, 함수 실행 시 생성되는 렉시컬 환경(지역 스코프)에 저장되는 변수다.

 

💬 비유적으로 말하면...

  • React는 `setCount(prev => prev + 1)`을 보면 “오, 상태 업데이트 함수를 넘겼네. 그럼 현재 상태를 prev에 넣고 실행해볼게.”
  • 자바스크립트는 `prev => prev + 1`을 실행하면서 “음, 이 함수엔 매개변수 하나 있고, 그 값을 prev로 기억해 둘 렉시컬 환경을 만들자.”

 

'React.js' 카테고리의 다른 글

Navigate vs useNavigate  (0) 2025.05.15
투두리스트로 간단하게 알아보는 useReducer  (0) 2025.05.13
브라우저가 리액트를 실행하는 과정 (간단 요약)  (0) 2025.05.08
배열 state 변경할 때, Spread 문법을 써야 하는 이유 (중요)  (0) 2025.05.05
리액트를 쓰는 이유 (a.k.a. 개념 정리)  (0) 2025.05.01
'React.js' 카테고리의 다른 글
  • Navigate vs useNavigate
  • 투두리스트로 간단하게 알아보는 useReducer
  • 브라우저가 리액트를 실행하는 과정 (간단 요약)
  • 배열 state 변경할 때, Spread 문법을 써야 하는 이유 (중요)
쥬피썬더의노예
쥬피썬더의노예
오히려 좋아
  • 쥬피썬더의노예
    d.log
    쥬피썬더의노예
    글쓰기 관리
    • 분류 전체보기 (112)
      • JS (37)
      • TS (3)
      • WEB (10)
      • React.js (20)
      • Next.js (4)
      • tanstack query (2)
      • Node.js (2)
      • HTML (5)
      • CSS (13)
      • CS (1)
      • 에이전트 (1)
      • Git (4)
      • JAVA (0)
      • SQL (0)
      • db (0)
      • GSAP (0)
      • 자료구조 (1)
      • 알고리즘 (0)
      • ✨회고 (5)
      • 포꾸 (0)
      • 인터뷰 (0)
      • 개발일지 (0)
      • 일기 (1)
      • etc (3)
      • 정처기 실기 (0)
        • C (0)
        • Java (0)
        • Python (0)
      • fonts (0)
      • articles (0)
      • 도서 (0)
  • 인기 글

  • 태그

    CSR
    상태 관리
    TypeScript
    SSG
    Next.js
    useEffect
    state
    SSR
    아키텍처
    HTML
    GIT
    zustand
    Til
    조합 패턴
    useState
    유효성 검사
    자바스크립트
    프론트엔드
    안티그래비티
    React.JS
    React Query
    css
    리팩토링
    리액트
    WEB
    React
    폼
    javascript
    클로저
    슬라이딩 윈도우
  • 최근 글

  • 전체
    오늘
    어제
  • hELLO· Designed By정상우.v4.10.3
쥬피썬더의노예
함수형 업데이트와 클로저 이해하기 (심화)
상단으로

티스토리툴바