vanila JS로 react 구현하기 (part 3)

개발 log·2022년 7월 21일
post-thumbnail

코드만 바로 확인하고 싶으시다면 Soact 라이브러리 Github <- 해당 링크로 바로 이동해주시면 됩니다.

이번 글에서는 react의 stateHook 중 useState를 어떻게 구현하는지 알아보려고 한다.

계기 및 호기심

내가 stateHook을 구현해보고 싶게된 이유는 어떻게 함수 내부에서 함수를 호출하는데 상태를 유지할 수 있는 것이지?라는 호기심 때문이었다.

때문에 여러 참고 자료와 내 생각이 섞인 stateHook이라 react의 stateHook과 똑같지는 않겠지만 어떻게 상태를 유지했는지에 대해서는 알아볼 수 있었다.

의심

리액트를 사용해봤다면 아마 대부분 아래 에러가 무엇인지 잘 알 것이다.

바로 조건문 안에서 useState를 호출하지 말라는 에러인데 이런 에러를 발생시키는 이유는 Hook 규칙을 읽어보면 알 수 있다.

간단하게 말하자면 react는 특정 state를 갖는 useState를 호출되는 순서로 관리한다.

이 때문에 Hook은 두 가지 규칙을 갖고 있다.

이 규칙만 잘 따라도 항상 동일한 순서로 Hook이 호출되는 것이 보장된다.

  1. 최상위에서만 Hook을 호출해야한다.
  2. 오직 React 함수 내에서 Hook을 호출한다.

나는 이렇게 순서를 기반으로 동작하는 stateHook을 직접 구현해보며 체득하고자 했다.

구현

// 이전 글에서 설명한 VDOM을 기반으로 DOM을 업데이트하는 메서드
import { updateDOM } from './manageDOM';
// 따로 설명하지 않아도 될만큼 간단한 메서드라 네이밍으로 이해해도 좋다.
// getValidState: store에서 현재 stateId를 확인하여 있으면 store state를 반환하고 없으면 store에 state를 등록하고 이를 반환한다.
// stateId: useState가 호출되는 순서를 관리하는 id
// getStoreState: stateId를 기반으로 store의 상태를 가져옴
// setStoreState: stateId를 기반으로 state를 set함
// increaseStateId: 순서를 증가시키는 메서드
import {
  getValidState,
  stateId,
  getStoreState,
  setStoreState,
  increaseStateId,
} from './store';

const useState = <T>(initialState: T): [T, Dispatcher<T>] => {
  const currentStateId = stateId;
  const state = getValidState(initialState);
  const setState = (nextState: T) => {
    if (Object.is(getStoreState(currentStateId), nextState)) {
      return;
    }
    setStoreState(currentStateId, nextState);
    updateDOM();
  };

  increaseStateId();
  return [state, setState];
};

로직 자체만 놓고보면 크게 어렵지 않다.

  1. currentStateId에 현재 stateId를 바인딩한다.(이렇게 해야 setter함수에서 해당 state에 맞는 stateId를 사용할 수 있다.)
  2. getValidState를 통해 store에 등록된 state를 불러오거나 initial state를 등록하고 이를 state에 바인딩한다.
  3. setStoreState를 통해 currentStateId를 기반으로 상태를 수정하고,
  4. updateDOM을 통해 비교 알고리즘을 수행하여 DOM을 업데이트한다.

그렇다. 만약 react에서도 이렇게 동작하는 것이라면 stateHook은 완전히 Observer 패턴으로 동작하는 것이다.

react를 개발한 facebook 팀에서는 flux 패턴을 소개했는데 이를 내 로직에 대입해보자면 아래와 같다.

참고자료: Flux로의 카툰 안내서

단방향 데이터 흐름

facebook 팀에서는 위와 같은 형식으로 단방향 데이터 흐름을 구현하고자 했는데 이를 배역으로 소개한 것이 꽤나 흥미롭다.

배역에는 총 4가지가 있다.

  1. 액션 생성자
  2. 디스패쳐
  3. 스토어
  4. 컨트롤러 뷰와 뷰

액션 생성자

액션 생성자가 하는 일은 전보기사에 빗대었다.
무슨 메시지를 보낼지 알려주면 액션 생성자는 나머지 시스템이 이해할 수 있는 포맷으로 바꿔준다고 한다.

처음에 이 액션 생성자라는 배역이 어색하게 느껴졌는데 지금 다시 생각해보면 간단하게 어떤 setter를 사용할지를 디스패쳐에게 알려주는 역할이라고 할 수 있을 것 같다.

stateHook이 아니라면 이는 대부분 key값으로 관리할 것이다.(stateHook은 호출 순서 === key)
실제로 redux나 react-query 등 많은 라이브러리가 key를 기반으로 store에 상태를 등록하고 key를 기반으로 상태의 위치를 알아내어 알맞은 작업을 수행한다.

디스패쳐

디스패쳐는 전화 교환대에서 교환원이 일하는 것과 같다고 소개한다.
그 이유는 전화 교환대에서는 등록된 모든 전화들과의 연결이 가능하기 때문이다.
디스패쳐는 액션을 보낼 필요가 있는 모든 스토어를 가지고 있고 액션 생성자로부터 액션이 넘어오면 여러 스토어에 액션을 보낸다.

사실 항상 이 디스패쳐가 나에게 어려움으로 다가왔다.
솔직히 정말 단순하게 생각하자면 setter함수와 다른게 없다고 생각했기 때문이다.

하지만 잘못 생각했던 것이 있었다.

이 디스패쳐는 facebook팀이 지적한 MVC 패턴에서(아마 잘못 설계된 MVC패턴을 의미하는 것이라 생각한다.) 핑퐁 현상이 발생하며 상태가 예측과 다르게 동작하는 경우를 처리하는데에 도움을 준다고 한다.

내가 구현한 stateHook은 정확히 말하자면 이 디스패쳐가 적용되어 있지 않다.

flux의 디스패쳐는 다른 아키텍쳐들과 다른 점이 모든 액션을 일단 받은 뒤 처리할지 말지를 결정한다는 것인데, 아마 이 부분은 debounce를 생각해도 좋을듯하다.

내 이론은 이렇다.

  1. 우선 디스패쳐가 액션을 수집한다.
  2. 내가 구현한 useState는 setter를 실행하고 바로 updateDOM을 실행하지만 여기서는 debounce처럼 waitFor을 사용해서 모든 액션을 수집한 뒤 최후에 한번 updateDOM을 수행하는 것이라고 생각한다.

결론적으로는 디스패쳐는 setter라기 보다는 액션을 수집하고 스토어에 전달하는 역할로 보는 것이 더 적절할듯하다.

스토어

스토어는 애플리케이션 내의 모든 상태와 그와 관련된 로직을 가지고 있다.

스토어는 디스패쳐를 통해 액션들을 전달받는데 여기서 수행하는 역할은 이 액션들을 보고 상태 변경을 수행할지 말지 판단하는 것이다.

일단 스토어에서 상태 변경을 완료하고 나면 변경 이벤트를 내보내며 이 이벤트는 컨트롤러 뷰에게 상태가 변경되었다는 것을 알려준다.

내 코드에 빗대어 보자면 아래와 같다.

  const setState = (nextState: T) => {
    // Object.is를 통해 상태가 변경되었는지 확인하고 변경되지 않았으면 이후의 작업을 실행하지 않는다.
    if (Object.is(getStoreState(currentStateId), nextState)) {
      return;
    }
    // 상태가 변경되었다면 실질적으로 상태를 변경시키고, DOM을 업데이트한다.
    setStoreState(currentStateId, nextState);
    updateDOM();
  };

여기서 상태 변경을 판단하는 것이 Object.is이고, 변경 이벤트가 updateDOM이라고 보면 될듯하다.

컨트롤러 뷰와 뷰

컨트롤러 뷰는 스토어와 뷰 사이의 중간관리자이다.
상태가 변경되었을 때 스토어가 컨트롤러 뷰에게 알리면 컨트롤러 뷰는 자신의 아래에 있는 모든 뷰에게 새로운 상태를 넘겨준다.

뷰는 발표자와 같다. 앱 내부에 대해 아는 것은 없지만 받은 데이터를 기반으로 사람들이 이해할 수 있는 포맷(HTML)로 어떻게 바구는지 알 수 있다.

이 배역과 매칭되는 함수는 내 이전 글인 vanila JS로 react 구현하기 (part 2)를 보면 된다.

이전 글의 updateDOM메서드가 컨트롤러 뷰, updateElement메서드가 라고 봐도 무방할듯하다.

결론

이렇게 hook 규칙과 flux 패턴을 공부해가며 stateHook을 구현해보았다.
이를 통해 왜 if문 내부에서 useState등 여러 Hook을 호출하면 안되는지 알게되었고,
실제 내가 구현한 react에 적용해보며 동작원리를 깊게 파악해볼 수 있었던 시간이었다.

profile
프론트엔드 개발자

0개의 댓글