Profile Picture

윤찬의 개발노트

2025. 2. 23.

React 만들기 - useState

React.jsJavaScript
"배우는 가장 좋은 방법은 직접 만들어 보는 것"

JS로 리액트를 만들어보자 - React Craft

React의 핵심 개념을 살리면서 JavaScript로만 구현하는 레포를 만들었다. 최신 기술을 공부하는 것도 중요하지만, 결국 모든 원리는 JavaScript와 React에서 비롯된다고 생각한다. 따라서 이번 경험이 한 번쯤은 꼭 필요하다고 판단했다. 제일 처음 만든 것은 React Hook이다. 그 중, useState를 먼저 만들어 보았다.


useState with js

useState의 주요 특징을 본다면, 다음과 같이 정리할 수 있었다.

  • getter, setter 함수 역할
  • 비동기적으로 상태 업데이트
  • 배치 처리
  • 상태 변경 이후 해당 컴포넌트 렌더링

더 많긴 하지만, 이정도 동작하도록 구현을 하면 이것을 기반으로 다른 훅을 구현하기에는 충분했다. 만약 그때 필요한게 있다면 그때 구현하면 된다.

// 먼저, useState에 들어온 값들을 저장하는 저장소를 만들었다.

const stateStore = {
  state: [],
  stateIndex: 0,
  resetStateIndex() {
    this.stateIndex = 0;
  },
};


또한 useState부분은 우선 값 저장과 렌더링을 할 수 있도록 우선 구현을 했다.

function useState(initialValue) {
  const currentIndex = stateStore.stateIndex;

  if (stateStore.state[currentIndex] === undefined) {
    stateStore.state[currentIndex] = initialValue;
  }

  function getState() {
    return stateStore.state[currentIndex];
  }

  function setState(newValue) {
    stateStore.state[currentIndex] = newValue;
    App();
  }

  stateStore.stateIndex++;

  return [getState, setState];
}


들어오는 인자값(initialValue)현재 상태 인덱스(stateStore.stateIndex)를 기반으로 상태를 저장하거나 반환하는 구조다.

  • 초기 상태 설정: stateStore.state에 해당 인덱스의 값이 없으면 initialValue를 저장한다.
  • 상태 조회 (getState): 현재 상태 값을 반환한다.
  • 상태 업데이트 (setState): 새로운 값을 저장하고 App()을 호출해 렌더링을 유도한다.
  • 인덱스 증가: 다음 useState 호출 시 올바른 상태를 참조하도록 stateIndex를 증가시킨다.
  • 최종 반환: [getState, setState] 배열을 반환해 상태를 조회하고 업데이트할 수 있도록 한다.

useState 점검1

초반에 목표로 설정했던, getter, setter 함수 역할, 상태 변경 이후 해당 컴포넌트 렌더링 은 구현이 되었지만, 비동기 처리, 배치 처리 에서는 정확한 동작을 하지 않는다.

  • 특히 배치 처리를 제대로 해줘야 할 것 같은데, 우선 지금 스냅샷이 안된다는 문제와, 비동기 처리 또한 고려를 하지 않은 상태이다.
function setState(newValue) {
  useStateStore[currentIndex] = newValue;
  App();
}

기존의 이 코드에서는, 그냥, 일괄처리없이 App.js에서 호출만 했다면, 값을 바꾸고 있다. 여기서 배치 업데이트를 위한 저장 공간과, 같은 setState가 여러 번 호출되더라도, 최종 값만 남김이라는 로직을 추가해보면 다음과 같다.

stateStore.updateQueue = stateStore.updateQueue.filter(
  (item) => item.index !== currentIndex
);

stateStore.updateQueue.push({ index: currentIndex, value: newValue });

useState 호출의 고유 index(currentIndex)를 key로 넣고, 들어온 값을 value로 삼아, filter로직을 추가했다. 그렇다면, 같은 한 주기에 들어오는 같은 setState에 대해서, 최근 값만 반영하게 되었고, 이제 한 렌더링 주기에 대해서 배치 처리만 적용하면 된다.

if (!stateStore.isUpdating) {
  stateStore.isUpdating = true;
    setTimeout(() => {
      stateStore.updateQueue.forEach(({ index, value }) => {
          stateStore.state[index] = value;
        });
        stateStore.updateQueue = [];
      stateStore.isUpdating = false;
    App();
  }, 0);
}

이렇게 된다면, isUpdating에 따라서, 해당 렌더링 주기의 첫번재 setState호출시, 더이상 지금의 분기문에 들어오지 않고, 이후 forEach를 통해 클로저로 stateStore.updateQueue 참조를 유지해서 최신값들만 유지할 수 있다.

만약 forEach없이 stateStore.state[currentIndex] = newValue; 직접 단일 값 업데이트를 진행했다면, setTimeout이 실행될 시점에서는 가장 마지막으로 할당된 값만 남게 된다.

최종결과는 스냅샷이 적용되었다.


useState 점검2

마지막으로 추가할 기능은 한 렌더링 주기에서 콜백을 던져줄 때이다. react가 상태 업데이트를 비동기적으로 처리할 때도 이전 상태(prev)를 올바르게 반영하도록 해야하는데, 현재의 경우는 그렇지 않다. 따라서 함수형 업데이트의 경우, 방금 구현한 배치 로직에 들어오지 않고, 따로 순차적으로 값을 적용하면 된다.

if (typeof newValue === "function") {
  const existingItem = stateStore.updateQueue.find(
    (item) => item.index === currentIndex
  );

  let prevValue;
  if (existingItem) {
    prevValue = existingItem.value; // 큐에 값이 있으면 기존 값 사용
  } 
  else {
    prevValue = stateStore.state[currentIndex]; // 없으면 현재 상태값 사용
  }
  newValue = newValue(prevValue);
}
 

함수형 업데이트가 전달된 경우, 먼저 해당 값이 stateStore.updateQueue에 존재하는지 확인한다. 존재하면 기존 값을 가져오고, 없으면 현재 상태 값을 사용한다. 그 후, 이전 값을 함수의 인자로 전달하여 newValue를 새로 계산하고 정의한다. 이렇게 되면, 즉시 값을 업데이트 할 수 있게 된다.


마지막 결과

전체 만든 결과는 다음과 같다.


리액트를 바닐라 JS로 구현하는 첫 번째 작업으로 useState를 만들었다. 이를 통해 상태를 변경하는 방식부터 살펴보고, 다음으로 useEffect를 구현할 예정이다.이렇게 하나씩 만들어가다 보면, 언젠가 나만의 리액트를 완성할 수 있지 않을까 싶다.

전체 코드 https://github.com/React-Craft/Hooks

Profile Picture

CHAN

과정은 복잡하되, 결과는 단순하게

Thank You for Visiting My Blog, Have a Good Day 😆
ⓒYoonchan Cho