React 원리 파악 2일차 입니다. 어제는 React가 화면을 그리고 업데이트하는 큰 그림, 즉 렌더링 파이프라인과 Fiber 아키텍처에 대해 알아보았죠. 오늘은 그 과정에서 아주 중요한 역할을 하는 '상태(State)'를 관리하는 방법, 그중에서도 가장 기본적이면서 핵심적인 useState 훅에 대해 깊이 탐구해볼 시간입니다.
컴포넌트가 기억해야 하는 값, 그리고 그 값이 변했을 때 화면을 다시 그리게 만드는 힘. 이것이 바로 상태의 역할인데요, useState는 함수형 컴포넌트에서 이런 상태를 아주 간편하게 사용할 수 있도록 도와줍니다. 하지만 단순히 사용하는 것을 넘어,
어떻게
동작하는지 그 원리를 알면 더욱 강력하고 효율적인 코드를 작성할 수 있겠죠?!
오늘은 useState가 내부적으로 상태를 어떻게 기억하고, setState 함수가 어떻게 상태 업데이트를 처리하는지, 그 비밀스러운 과정들을 하나씩 파헤쳐 보겠습니다. 중간중간 면접 단골 질문들도 포함되어 있으니, React 면접 준비하시는 분들께도 많은 도움이 될 거예요.
위 코드처럼 useState(0)를 호출하면, count라는 상태 변수와 setCount라는 상태 업데이트 함수를 얻게 됩니다. 그런데 React는 여러 컴포넌트, 또는 한 컴포넌트 내의 여러 useState 호출을 어떻게 구분하고 각자의 상태를 정확히 기억할 수 있을까요?
데이터(값)와 다음 데이터 조각을 가리키는 참조(포인터)를 함께 묶은 노드들이 기차처럼 줄줄이 연결된 자료구조입니다. 배열처럼 순서가 있지만, 메모리상에 연속적으로 위치하지 않아도 되고 중간에 삽입/삭제가 용이하다는 특징이 있습니다.
컴포넌트가 처음 렌더링될 때 useState가 호출되면, React는 해당 훅에 대한 정보(Hook 객체)를 생성하고 이 연결 리스트에 추가합니다. 이 Hook 객체에는 현재 상태 값(memoizedState), 업데이트 대기열(queue) 등의 정보가 들어있습니다.
Hook 객체 연결 리스트
컴포넌트가 다시 렌더링될 때는, React는 이 저장된 연결 리스트를 순서대로 탐색하면서 각 useState 호출에 맞는 Hook 객체를 찾아 현재 상태 값을 반환합니다. 만약 훅 호출 순서가 달라진다면, React는 잘못된 Hook 객체를 참조하게 되어 예상치 못한 버그가 발생하게 되는 것이죠.
setState 함수가 호출되면, React는 즉시 상태를 변경하지 않습니다. 대신, '상태를 이렇게 바꿔주세요'라는
업데이트 요청(Update)
객체를 만들어서 해당 Hook 객체의
queue
속성에 추가합니다. 이 queue는 업데이트 요청들을 순서대로 처리하기 위한 대기열이며, 이 역시 단일 연결 리스트(Singly Linked List) 구조로 구현되어 있습니다.
Update 객체: 업데이트할 값(또는 함수), 우선순위 등의 정보를 담고 있습니다.
Update Queue: setState가 호출될 때마다 생성된 Update 객체들이 순서대로 쌓이는 곳입니다.
여러 개의 Update 객체를 연결 리스트 형태로 가지는 Queue
React는 다음 렌더링 주기(Render Phase)에 이 Update Queue를 처리하여 최종 상태 값을 계산합니다. 이렇게 업데이트를 바로 적용하지 않고 모아서 처리하는 방식 덕분에 여러 상태 업데이트를 효율적으로 관리하고, 이전 포스팅에서 배웠던 동시성(Concurrency) 기능을 구현할 수 있습니다.
// 값으로 업데이트setCount(count + 1); // 이전 count 값에 의존// 함수형 업데이트setCount(prevCount => prevCount + 1); // 이전 상태를 인자로 받아 새 상태 반환
함수형 업데이트는 언제 유용할까요? 바로
이전 상태 값에 기반하여 다음 상태를 결정
해야 할 때입니다. 특히 짧은 시간 안에 여러 번의 상태 업데이트가 예약되는 경우(배치 처리될 때), 함수형 업데이트를 사용하면 각 업데이트가 실행되는 시점의
가장 최신 상태
를 안전하게 참조하여 상태를 계산할 수 있습니다.
만약 setCount(count + 1)을 여러 번 호출하면, 각 호출 시점의 count 값은 동일할 수 있기 때문에 예상대로 동작하지 않을 수 있습니다.
하지만 setCount(prevCount => prevCount + 1)을 사용하면, React는 Update Queue를 처리할 때 각 업데이트 함수를 순서대로 실행하면서 이전 업데이트 결과가 반영된 최신 상태(prevCount)를 다음 함수에 넘겨주므로 항상 정확한 결과를 보장합니다.
기능을 가지고 있습니다. 예를 들어, 하나의 이벤트 핸들러 내에서 setState를 여러 번 호출하더라도, React는 이를 모아서 단 한 번의 리렌더링만 수행합니다.
function handleClick() { setCount(count + 1); // 업데이트 요청 1 setSomethingElse(false); // 업데이트 요청 2 // React는 이 두 업데이트를 모아서 처리하고, 리렌더링은 한 번만 발생}
배치 과정
React 17 이전
: 이벤트 핸들러 내에서의 업데이트만 자동으로 배치되었습니다. setTimeout, Promise 콜백 등 비동기 코드 내에서의 업데이트는 각각 별도의 리렌더링을 유발했습니다.
React 18 이후
: 자동 배치(Automatic Batching)가 도입되어, setTimeout, Promise 콜백 등 어디서 setState를 호출하든 기본적으로 모든 업데이트를 자동으로 배치 처리합니다. 이는 불필요한 리렌더링을 줄여 성능을 향상시킵니다. 만약 배치를 원하지 않고 즉시 렌더링해야 하는 특별한 경우에는 flushSync API를 사용할 수 있습니다.
import { flushSync } from 'react-dom';function handleClick() { flushSync(() => { setCount(count + 1); // 즉시 렌더링 }); setSomethingElse(false); // 자동 배치로 묶임}
(Scheduler)가 이 업데이트의 우선순위를 판단하고 렌더링 작업을 예약합니다. 어제 배운 것처럼, Fiber는 작업을 작은 단위로 나누고 우선순위에 따라 처리할 수 있습니다.
사용자 입력에 대한 반응과 같이 높은 우선순위의 업데이트는 즉시 처리될 가능성이 높고, 덜 긴급한 업데이트는 다른 작업 중간이나 브라우저가 유휴 상태일 때 처리될 수 있습니다.
React 18의 동시성 기능(startTransition 등)은 개발자가 직접 업데이트의 우선순위를 낮출 수 있게 하여, 중요한 상호작용이 부드럽게 유지되도록 돕습니다.
오늘은 함수형 컴포넌트의 심장과도 같은 useState 훅과 setState 함수의 내부 동작 원리를 자세히 살펴보았습니다.
React는 훅 호출 순서에 의존하여 각 useState의 상태를 구분하고 관리합니다. 이는 훅의 규칙이 중요한 이유입니다.
각 훅의 정보는 Fiber 노드 내의 Hook 객체에 저장되며, 이 객체들은 연결 리스트 형태로 관리됩니다. 현재 상태 값은 memoizedState에 저장됩니다.
setState 호출은 즉시 상태를 바꾸는 것이 아니라, Update 객체를 생성하여 Update Queue(연결 리스트)에 추가하는 요청입니다.
함수형 업데이트 setState(prev => ...)는 이전 상태를 안전하게 참조하여 업데이트할 때 유용합니다.
React는 성능을 위해 여러 setState 호출을 배치 처리하여 한 번의 리렌더링만 수행합니다 (React 18에서는 자동 배치).
클래스의 setState는 상태를 병합하지만, 훅의 setState는 상태를 교체합니다.
초기 상태 지연 계산(Lazy Initial State)은 초기값 계산 비용이 비쌀 때 유용한 최적화 기법입니다.
setState는 React의 렌더링 스케줄러와 상호작용하여 업데이트 우선순위를 관리하고 렌더링을 예약합니다.
useState의 동작 원리를 이해하는 것은 React 컴포넌트의 동작 방식을 더 깊이 이해하고, 발생할 수 있는 문제를 예측하며, 성능을 최적화하는 데 큰 도움이 됩니다. 특히 훅의 규칙, 연결 리스트 구조, Update Queue, 배치, Lazy Initial State 등은 잘 기억해두시면 좋겠습니다.
다음 시간에는 React 18의 핵심 변화 중 하나인
자동 배칭
(Automatic Batching)과
동시성
(Concurrency)에 대해 더 자세히 알아보겠습니다. 상태 업데이트가 실제로 어떻게 묶이고 처리되는지, 그리고 동시성이 어떻게 사용자 경험을 향상시키는지 함께 알아볼 예정입니다.