React는 왜 Hooks를 배열로 관리할까?

jinew·2025년 1월 31일
2

🥝 React

목록 보기
3/4
post-thumbnail

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);

1. useState.mjs

: _val를 정의하고, 만약 값이 없다면 초기값인 initialValue로 세팅해준다.
setValue 함수는 새로운 값을 기존 _val 값에 넣어준다. 최종적으로는 _val값과 setter 함수 setValue를 리턴한다.


2. index.mjs

: 위에서 만든 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' }

counttext 의 값이 모두 '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;
  1. useState가 호출될 때마다 hooks 배열의 각 인덱스에 값이 저장된다.
  2. currentHook 변수를 활용해 useState가 실행되는 순서를 유지한다.
  3. setState 함수는 각 state 값이 독립적으로 변경되도록 한다.

이를 통해 각 state 값이 개별적으로 관리되며, 하나의 state가 변경될 때 다른 state가 영향을 받지 않는다. 이렇게 React는 Hooks를 배열로 저장하여 순서를 보장하고 독립적인 상태를 유지하는 구조를 갖게 되었다.



1. Hooks의 역할과 특징


🔥 Hooks는 상태(state)와 사이드 이펙트(effect)를 관리한다

: React의 함수형 컴포넌트는 렌더링 될 때마다 함수를 다시 실행하기 때문에,
상태(state)를 유지하려면 별도의 저장 공간이 필요하다. 이 역할을 하는 것이 바로 Hooks! (useState, useEffect 등)

🌉 Hooks는 렌더링 될 때마다 다시 실행된다

: Hooks는 한 번만 실행되는 것이 아니라, 렌더링 될 때마다 실행된다.
그 말은 React가 이전 렌더링의 상태값을 어디에 저장하고 불러올 것인가에 대한 의문이 생긴다는 거다.

React는 이를 위해 Hook를 배열로 관리한다.



2. React가 Hooks를 배열로 관리하는 이유


(1) Hooks의 호출 순서를 유지하기 위해

: React는 Hooks를 배열에 저장하고, 컴포넌트가 렌더링될 때마다 항상 같은 순서로 Hooks를 실행한다.
이렇게 하면 이전 렌더링의 상태를 그대로 가져올 수 있기 때문이다!

📌 만약 배열 대신 객체나 Map을 사용하면?

  • 새로운 Hooks가 추가/삭제될 때 인덱스가 꼬일 가능성이 높다.
  • React는 훅을 찾기 위해 추가적인 연산이 필요해 성능이 저하될 수 있다.

📌 배열로 관리하면?

  • Hooks를 호출하는 순서만 유지하면 되니 빠르고 안정적이다.

(2) 최소한의 렌더링 비용을 위해

: React는 렌더링 최적화를 중요하게 생각한다.
배열을 사용하면 최소한의 연산으로 이전 렌더링과 현재 렌더링을 빠르게 비교할 수 있다.



(3) Hooks의 규칙을 강제하기 위해

: React의 공식 문서를 보면, Hooks는 항상 같은 순서로 호출해야 한다고 한다.
그런데, 이 규칙을 지키지 않으면 어떤 문제가 발생할까?

❌ 잘못된 예시: 조건문 안에서 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 값에 따라 호출되지 않을 수도 있다.
  • 다음 렌더링에서 Hooks의 순서가 달라져서 React가 올바르게 상태를 찾지 못하는 문제가 발생한다!

✅ 올바른 예시: 항상 같은 순서로 Hooks 사용

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를 배열로 저장하는 것이다!



3. 결론 : Hooks를 배열로 관리하는 이유는?


✅ 결국 React는 Hooks를 배열에 저장하고, 인덱스로 관리한다!

  • Hooks는 함수형 컴포넌트가 렌더링될 때마다 다시 실행되지만,
  • React는 Hooks를 배열로 관리하여 이전 렌더링에서 저장된 상태를 올바른 순서로 불러옴
  • 배열의 인덱스를 기반으로 상태를 유지하기 때문에, 상태가 초기화되지 않고 계속 유지됨

✅ Hooks는 배열로 저장해야만

렌더링될 때도 같은 순서로 실행 가능
이전 상태를 빠르게 찾을 수 있어 렌더링 성능 최적화
조건문/반복문에서 훅을 사용할 때 발생할 수 있는 오류 방지


🚀 즉, React가 Hooks를 배열로 저장하는 이유를 한 문장으로 말하자면 "렌더링이 반복되더라도 상태를 정확하게 유지하기 위해서"다.
💡 이 원리를 이해하면, React의 Hooks를 더욱 안정적으로 사용할 수 있으니 차근차근 여러 번 곱씹어 이해해야겠다!

profile
멈추지만 않으면 도착해 🛫

0개의 댓글

관련 채용 정보