Hooks는 React 함수형 컴포넌트의 핵심이다. 그런데 왜 React는 Hooks를 배열에 저장할까?
이는 1) 렌더링 최적화, 2) 순서 보장, 3) 불필요한 연산 최소화 때문이라고 하는데,
이번 글에서는 React가 Hooks를 배열로 관리하는 이유와 그 원리를 쉽게 풀어보고자 한다.
우리가 사용하는 useState
의 로직을 단순하게 생각해보면 이렇다.
// useState.mjs
let _val;
export const useState = (initialValue) => {
if (!_val) {
_val = initialValue;
}
function setValue(newValue) {
_val = newValue;
}
return [_val, setValue];
};
// index.mjs
import MyReact from './MyReact.mjs';
import { useState } from './useState.mjs';
function FunctionalComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
return {
click: () => setCount(count + 1),
type: (text) => setText(text),
noop: () => setCount(count),
render: () => console.log('render', { count, text }),
};
}
let App = MyReact.render(FunctionalComponent);
: _val
를 정의하고, 만약 값이 없다면 초기값인 initialValue
로 세팅해준다.
setValue
함수는 새로운 값을 기존 _val
값에 넣어준다. 최종적으로는 _val
값과 setter 함수 setValue
를 리턴한다.
: 위에서 만든 useState
를 import해서 state를 선언한다.
그리고 MyReact.render(FunctionalComponent)
로 실행하면 ?
// console.log
render { count: 0, text: '' }
아래와 같이 초기값이 잘 세팅된 모습으로 출력된다.
여기서 다른 메소드를 활용해서 값을 바꿔보자.
import MyReact from './MyReact.mjs';
import { useState } from './useState.mjs';
function FunctionalComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
return {
click: () => setCount(count + 1),
type: (text) => setText(text),
noop: () => setCount(count),
render: () => console.log('render', { count, text }),
};
}
let App = MyReact.render(FunctionalComponent);
App.click();
App = MyReact.render(FunctionalComponent);
결과를 예상해본다. render { count: 1, text: '' }
로 출력될 것이라 예상했다면 오산이다.
// console.log
render { count: 0, text: '' } // 최초 렌더링
render { count: 1, text: 1 }
두 값이 동시에 바뀌었다. 이 상태에서 type
메소드를 사용하면?
let App = MyReact.render(FunctionalComponent);
App.click();
App = MyReact.render(FunctionalComponent);
App.type('bar');
App = MyReact.render(FunctionalComponent);
이번엔 어떤 결과가 나올까?
// console.log
render { count: 0, text: '' }
render { count: 1, text: 1 }
render { count: 'bar', text: 'bar' }
count
와 text
의 값이 모두 'bar'
로 변경되었다.
왜 그럴까? 이는 바로 useState.mjs
에서 하나의 값인 _val
을 두 state 모두가 참조하고 있기 때문이다.
두 state가 같은 값을 공유하고 있기 때문에 모든 값이 동일하게 변경되는 것이다!
여기서 리액트는 각각의 hook들이 자신만의 값과 setter 함수를 유지하게끔 배열로 저장하는 방법을 선택했다. 배열을 사용하면 각 useState 호출이 독립적인 인덱스를 가지게 되어 값이 섞이지 않기 때문이다.
위 로직을 배열로 관리할 수 있게 수정한다면 이렇게 될 것이다.
// MyReact.mjs
let hooks = [],
currentHook = 0;
const MyReact = {
render(Component) {
const Comp = Component();
Comp.render();
currentHook = 0;
return Comp;
},
};
export const useState = (initialValue) => {
hooks[currentHook] = hooks[currentHook] || initialValue;
const hookIndex = currentHook;
const setState = (newState) => {
hooks[hookIndex] = newState;
};
return [hooks[currentHook++], setState];
};
export default MyReact;
useState
가 호출될 때마다 hooks
배열의 각 인덱스에 값이 저장된다.currentHook
변수를 활용해 useState
가 실행되는 순서를 유지한다.setState
함수는 각 state 값이 독립적으로 변경되도록 한다.이를 통해 각 state 값이 개별적으로 관리되며, 하나의 state가 변경될 때 다른 state가 영향을 받지 않는다. 이렇게 React는 Hooks를 배열로 저장하여 순서를 보장하고 독립적인 상태를 유지하는 구조를 갖게 되었다.
: React의 함수형 컴포넌트는 렌더링 될 때마다 함수를 다시 실행하기 때문에,
상태(state)를 유지하려면 별도의 저장 공간이 필요하다. 이 역할을 하는 것이 바로 Hooks! (useState
, useEffect
등)
: Hooks는 한 번만 실행되는 것이 아니라, 렌더링 될 때마다 실행된다.
그 말은 React가 이전 렌더링의 상태값을 어디에 저장하고 불러올 것인가에 대한 의문이 생긴다는 거다.
✅ React는 이를 위해 Hook를 배열로 관리한다.
: React는 Hooks를 배열에 저장하고, 컴포넌트가 렌더링될 때마다 항상 같은 순서로 Hooks를 실행한다.
이렇게 하면 이전 렌더링의 상태를 그대로 가져올 수 있기 때문이다!
📌 만약 배열 대신 객체나 Map을 사용하면?
📌 배열로 관리하면?
: React는 렌더링 최적화를 중요하게 생각한다.
배열을 사용하면 최소한의 연산으로 이전 렌더링과 현재 렌더링을 빠르게 비교할 수 있다.
: React의 공식 문서를 보면, Hooks는 항상 같은 순서로 호출해야 한다고 한다.
그런데, 이 규칙을 지키지 않으면 어떤 문제가 발생할까?
function BadComponent() {
const [count, setCount] = useState(0);
if (count > 5) {
// 🚨 useEffect가 조건문 안에서 실행됨 → React가 올바르게 관리할 수 없음!
useEffect(() => {
console.log("Effect 실행!");
}, []);
}
return <button onClick={() => setCount(count + 1)}>클릭</button>;
}
🚨 문제점
useEffect
가 조건문 안에 들어가면, count
값에 따라 호출되지 않을 수도 있다.function GoodComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count > 5) {
console.log("Effect 실행!");
}
}, [count]);
return <button onClick={() => setCount(count + 1)}>클릭</button>;
}
✔ Hooks를 항상 같은 순서로 실행하면 문제 없이 동작한다.
✔ 이런 규칙을 강제하려고 React는 Hooks를 배열로 저장하는 것이다!
① 렌더링될 때도 같은 순서로 실행 가능
② 이전 상태를 빠르게 찾을 수 있어 렌더링 성능 최적화
③ 조건문/반복문에서 훅을 사용할 때 발생할 수 있는 오류 방지
🚀 즉, React가 Hooks를 배열로 저장하는 이유를 한 문장으로 말하자면 "렌더링이 반복되더라도 상태를 정확하게 유지하기 위해서"다.
💡 이 원리를 이해하면, React의 Hooks를 더욱 안정적으로 사용할 수 있으니 차근차근 여러 번 곱씹어 이해해야겠다!