1. useState 구현체
React의 useState는 상태를 선언하고 관리하는 가장 기본적인 훅이다. 이를 바닐라 JavaScript로 단순화하면 아래와 같은 형태로 생각할 수 있다.
let currentComponent = null;
function useState(initialValue) {
if (!currentComponent) {
throw new Error('useState must be called within a component');
}
const stateIndex = currentComponent.stateIndex;
if (!currentComponent.state[stateIndex]) {
currentComponent.state[stateIndex] = [
initialValue,
(value) => {
currentComponent.state[stateIndex][0] = value;
currentComponent.render();
},
];
}
const stateTuple = currentComponent.state[stateIndex];
currentComponent.stateIndex++;
return [stateTuple[0], stateTuple[1]];
}
위 코드는 state를 배열로 관리하지만, React의 실제 구현에서는 **링크드 리스트(linked list)**로 상태를 관리한다. 배열 대신 링크드 리스트를 사용함으로써, 상태 노드를 동적으로 추가하거나 삭제할 때 더 효율적으로 동작할 수 있다.
링크드 리스트는 다음과 같은 구조로 상태를 연결한다.
function StateNode(value, next = null) {
this.value = value;
this.next = next;
}
React는 이러한 상태 노드를 컴포넌트별로 연결하여 상태 관리의 효율성을 높이고, 상태 접근과 업데이트를 동적으로 처리한다.
2. 컴포넌트에서 상태를 관리하는 방식
React에서 컴포넌트는 상태와 렌더링 로직을 캡슐화하는 기본 단위이다. 바닐라 JavaScript로 간단히 표현하면 아래와 같은 구조가 된다.
function createComponent(renderFn) {
return function Component() {
currentComponent = {
state: [], // 상태를 배열로 관리
stateIndex: 0, // 상태 인덱스 추적
renderFn: renderFn, // 렌더링 함수
render: function () {
this.stateIndex = 0; // 렌더링 시 인덱스 초기화
const newVNode = this.renderFn(); // 새로운 Virtual DOM 생성
const rootElement = document.getElementById('root') || document.body;
if (!this.vnode) {
// 초기 렌더링
this.vnode = newVNode;
rootElement.appendChild(createElement(newVNode));
} else {
const patches = diff(this.vnode, newVNode); // Virtual DOM 비교
patch(rootElement, patches);
this.vnode = newVNode;
}
},
};
currentComponent.render();
return currentComponent;
};
}
위 코드는 React의 기본 동작을 모방한 것이다. 여기서 중요한 점은 컴포넌트가 자신의 상태를 관리하며, 상태는 useState와 같은 훅을 통해 관리된다는 점이다.
React에서 말하는 로컬리티(Locality)
React는 상태를 컴포넌트 단위로 관리한다. 즉, 각 컴포넌트는 자신의 상태와 렌더링 주기를 독립적으로 유지한다. 이를 통해 Locality를 실현하며, 다음과 같은 이점을 제공한다.
1. 독립성
• 부모와 자식 컴포넌트의 상태는 분리되어 관리된다.
• 부모의 상태 변경이 자식의 상태에 직접적인 영향을 미치지 않는다.
2. 예측 가능성
• 상태와 렌더링 로직이 컴포넌트 내에 캡슐화되어 있어, 컴포넌트의 동작을 쉽게 이해할 수 있다.
3. 성능 최적화
• 특정 컴포넌트의 상태 변경은 해당 컴포넌트에 국한되며, 전체 애플리케이션의 렌더링 비용을 줄일 수 있다.
3. 초기 렌더링과 재렌더링 시 상태 관리 방식
React에서 상태 관리는 두 가지 주요 단계로 나뉜다.
1. 초기 렌더링
• useState가 최초로 호출될 때, 초기 상태 값을 계산하거나 제공받는다.
• React는 이 값을 상태 링크드 리스트에 저장하고, 이후 렌더링 주기 동안 이를 참조한다.
2. 재렌더링
• 컴포넌트가 재렌더링되면 React는 상태 링크드 리스트에서 기존 값을 가져와 useState에 반환한다.
• 이 과정에서 useState의 초기 상태 값은 무시된다.
재렌더링 시 상태 링크드 리스트의 특정 노드 값만 변경되더라도, 컴포넌트 함수 본문은 처음부터 끝까지 다시 실행된다. React는 이를 통해 상태 관리와 컴포넌트 동작의 일관성을 유지한다.
4. Lazy Initialization으로 비용 줄이기
useState의 초기 상태 값은 컴포넌트의 최초 렌더링에서만 사용된다. 그러나 컴포넌트 함수가 재실행될 때 초기 상태 값을 계산하는 코드가 불필요하게 실행될 수 있다. 예를 들어 다음 코드를 보자
const [count, setCount] = React.useState(Number(window.localStorage.getItem('count')));
이 코드는 컴포넌트가 재렌더링될 때마다 Number(window.localStorage.getItem('count'))를 호출하지만, React는 초기 상태 값을 사용하지 않는다. 이를 최적화하기 위해 Lazy Initialization을 사용할 수 있다.
const [count, setCount] = React.useState(() => {
return Number(window.localStorage.getItem('count'));
});
Lazy Initialization은 초기 상태 계산을 함수로 감싸서, 컴포넌트의 최초 렌더링 시에만 실행되도록 한다. 이후 재렌더링에서는 상태 링크드 리스트에서 기존 값을 가져오므로 불필요한 연산이 제거된다. Lazy Initialization을 도입할 때 중요한 점은 함수 선언 자체는 빠르다는 것이다. 따라서 초기 상태를 설정할 때 계산 비용이 큰 연산이 있다면, 이를 함수로 감싸 Lazy Initialization을 적용하는 것이 좋다. 이렇게 하면 불필요한 작업을 줄이고 컴포넌트의 성능을 최적화할 수 있다.
5. 결론
React의 useState는 단순히 상태를 선언하는 훅처럼 보이지만, 내부적으로는 효율적인 상태 관리 메커니즘과 링크드 리스트 기반의 데이터 구조를 사용하여 동작한다.
6. 참고자료
1. Kent C. Dodds, “Use State Lazy Initialization and Function Updates”, https://kentcdodds.com/blog/use-state-lazy-initialization-and-function-updates
2. Faustino Aguilar, “React useState Implementation Example", https://gist.github.com/faustinoaq/b19da758fc45155a0b3b10d9f578c5ce
'React' 카테고리의 다른 글
React Hook이란 무엇인가 (0) | 2024.12.02 |
---|---|
React.memo에 대하여 (1) | 2024.11.16 |
React 18에서 변경된 Suspense (1) | 2024.11.04 |
React에서 hook을 통해 결합도 줄이기 (0) | 2024.11.04 |
React Custom Hook에 대하여 (1) | 2024.10.28 |