만약 함수가 상태를 가질 수 있다면?

172025년 09월 01일4

월요일 아침, 댄은 평소보다 일찍 출근했다. 그의 머릿속은 주말 내내 구상했던 아이디어로 가득 차 있었다. 그는 팀원들이 오기 전, 조용한 사무실에서 자신의 가설을 코드로 증명해보고 싶었다.

그의 목표는 명확했다.
클래스가 아닌, 평범한 자바스크립트 함수로 만들어진 컴포넌트 안에서, ‘상태’를 만들어내고 업데이트하는 것.

그는 먼저 가장 단순한 형태의 함수형 컴포넌트를 작성했다.

function Counter() {
  // 이 곳에서 상태를 만들고 싶다.
  // let count = 0; // 이건 안 된다. 렌더링마다 0으로 초기화될 테니까.

  return (
    <div>
      <p>You clicked {count} times</p>
      <button>Click me</button>
    </div>
  );
}

문제는 명백했다. Counter 함수는 렌더링이 필요할 때마다 호출된다. 함수 내부에 선언된 let count = 0;는 호출될 때마다 실행되므로, count 값은 영원히 0에 머무를 터였다. 리액트는 이전 렌더링의 count 값을 ‘기억’할 방법이 없었다.

“리액트가 기억하게 만들면 돼.”

댄의 손가락이 움직이기 시작했다.
그는 리액트의 코어 로직 어딘가에, 컴포넌트의 상태를 저장할 비밀스러운 공간이 있다고 가정했다. 그리고 그 공간에 접근할 수 있는 특별한 함수를 상상했다. 그는 그 함수의 이름을 useState라고 지었다.

그가 상상한 useState는 초기값을 인자로 받고, 두 가지를 반환한다.

  1. 현재 상태 값 (count)
  2. 그 상태를 업데이트할 수 있는 함수 (setCount)

그는 이 상상의 함수를 Counter 컴포넌트 안에서 호출했다.

function Counter() {
  // useState가 마법처럼 상태 값과 업데이트 함수를 반환한다고 가정한다.
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // 버튼이 클릭되면, setCount 함수를 호출해 상태 변경을 요청한다.
    setCount(count + 1);
  };

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

코드가 놀랍도록 간결해졌다.
class 선언도, constructor도, this도 없었다. 모든 것이 평범한 함수와 변수, 그리고 useState라는 정체불명의 함수 호출로 이루어져 있었다.

이제 남은 것은 useState의 실체를 구현하는 일이었다.
댄은 리액트의 렌더링 로직을 흉내 내는 가짜 코드를 작성하기 시작했다. 그의 첫 번째 아이디어는 가장 단순하고 직설적인 방식이었다.

‘컴포넌트마다 하나의 상태만 가질 수 있다고 가정하고, 그 상태를 컴포넌트와 연결된 어딘가에 저장하자.’

그는 리액트 내부에서 각 컴포넌트를 식별하고, 그 컴포넌트에 해당하는 상태 값을 저장하는 전역 저장소를 떠올렸다.

// --- React Core (아주 단순화된 상상) ---
let globalState = undefined;

function useState(initialValue) {
  if (globalState === undefined) {
    globalState = initialValue;
  }

  const setState = (newValue) => {
    globalState = newValue;
    // 상태가 변경되었으니, 컴포넌트를 다시 렌더링하라고 알린다.
    React.render(Counter);
  };

  return [globalState, setState];
}
// ------------------------------------

허점투성이인 코드였다. 전역 변수를 사용하는 것은 위험했고, 세상에 Counter 컴포넌트 하나만 존재한다고 가정한 것이나 마찬가지였다.

하지만 댄은 개의치 않았다. 그는 지금 완벽한 제품이 아닌, 가능성을 타진하는 실험을 하고 있었다.
그는 자신이 만든 가짜 리액트와 Counter 컴포넌트를 연결하고 실행했다.

브라우저에 You clicked 0 times라는 문구와 버튼이 나타났다.
그는 떨리는 마음으로 버튼을 클릭했다.

화면이 깜빡이더니, 숫자가 ‘1’로 바뀌었다.
다시 한번 클릭하자, ‘2’가 되었다.

작동했다.
평범한 함수가, 자신의 이전 값을 기억하고, 그 값을 갱신하는 데 성공한 것이다.

댄은 의자에 등을 기댔다. 심장이 세차게 뛰었다.
모니터에 떠 있는 몇 줄 안 되는 코드는, 리액트의 미래를 바꿀 수 있는 거대한 잠재력을 품고 있었다. 그것은 클래스라는 견고한 성벽에 생긴, 작지만 치명적인 균열이었다.

물론, 이 아이디어는 아직 갓 태어난 아기와 같았다.
만약 하나의 컴포넌트 안에서 useState를 두 번 호출하면 어떻게 될까?
전역 변수 하나만으로는 여러 상태를 구분할 수 없었다.

그의 머릿속에 섬광처럼 스친 이 작은 성공은, 곧바로 더 풀기 어려운 질문으로 이어졌다.
그리고 그 질문에 대한 해답의 실마리는, 세바스티안이 오랫동안 품고 있던, 훨씬 더 심오하고 추상적인 개념 속에 숨어 있었다.