
코드만 바로 확인하고 싶으시다면 Soact 라이브러리 Github <- 해당 링크로 바로 이동해주시면 됩니다.
이번 글에서는 react의 stateHook 중 useState를 어떻게 구현하는지 알아보려고 한다.
내가 stateHook을 구현해보고 싶게된 이유는 어떻게 함수 내부에서 함수를 호출하는데 상태를 유지할 수 있는 것이지?라는 호기심 때문이었다.
때문에 여러 참고 자료와 내 생각이 섞인 stateHook이라 react의 stateHook과 똑같지는 않겠지만 어떻게 상태를 유지했는지에 대해서는 알아볼 수 있었다.
리액트를 사용해봤다면 아마 대부분 아래 에러가 무엇인지 잘 알 것이다.

바로 조건문 안에서 useState를 호출하지 말라는 에러인데 이런 에러를 발생시키는 이유는 Hook 규칙을 읽어보면 알 수 있다.
간단하게 말하자면 react는 특정 state를 갖는 useState를 호출되는 순서로 관리한다.
이 때문에 Hook은 두 가지 규칙을 갖고 있다.
이 규칙만 잘 따라도 항상 동일한 순서로 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];
};
로직 자체만 놓고보면 크게 어렵지 않다.
그렇다. 만약 react에서도 이렇게 동작하는 것이라면 stateHook은 완전히 Observer 패턴으로 동작하는 것이다.
react를 개발한 facebook 팀에서는 flux 패턴을 소개했는데 이를 내 로직에 대입해보자면 아래와 같다.
참고자료: Flux로의 카툰 안내서

facebook 팀에서는 위와 같은 형식으로 단방향 데이터 흐름을 구현하고자 했는데 이를 배역으로 소개한 것이 꽤나 흥미롭다.
배역에는 총 4가지가 있다.
액션 생성자가 하는 일은 전보기사에 빗대었다.
무슨 메시지를 보낼지 알려주면 액션 생성자는 나머지 시스템이 이해할 수 있는 포맷으로 바꿔준다고 한다.
처음에 이 액션 생성자라는 배역이 어색하게 느껴졌는데 지금 다시 생각해보면 간단하게 어떤 setter를 사용할지를 디스패쳐에게 알려주는 역할이라고 할 수 있을 것 같다.
stateHook이 아니라면 이는 대부분 key값으로 관리할 것이다.(stateHook은 호출 순서 === key)
실제로 redux나 react-query 등 많은 라이브러리가 key를 기반으로 store에 상태를 등록하고 key를 기반으로 상태의 위치를 알아내어 알맞은 작업을 수행한다.
디스패쳐는 전화 교환대에서 교환원이 일하는 것과 같다고 소개한다.
그 이유는 전화 교환대에서는 등록된 모든 전화들과의 연결이 가능하기 때문이다.
디스패쳐는 액션을 보낼 필요가 있는 모든 스토어를 가지고 있고 액션 생성자로부터 액션이 넘어오면 여러 스토어에 액션을 보낸다.
사실 항상 이 디스패쳐가 나에게 어려움으로 다가왔다.
솔직히 정말 단순하게 생각하자면 setter함수와 다른게 없다고 생각했기 때문이다.
하지만 잘못 생각했던 것이 있었다.
이 디스패쳐는 facebook팀이 지적한 MVC 패턴에서(아마 잘못 설계된 MVC패턴을 의미하는 것이라 생각한다.) 핑퐁 현상이 발생하며 상태가 예측과 다르게 동작하는 경우를 처리하는데에 도움을 준다고 한다.
내가 구현한 stateHook은 정확히 말하자면 이 디스패쳐가 적용되어 있지 않다.
flux의 디스패쳐는 다른 아키텍쳐들과 다른 점이 모든 액션을 일단 받은 뒤 처리할지 말지를 결정한다는 것인데, 아마 이 부분은 debounce를 생각해도 좋을듯하다.
내 이론은 이렇다.
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에 적용해보며 동작원리를 깊게 파악해볼 수 있었던 시간이었다.