본문 바로가기

공부/react

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

반응형

 

React는 Hooks를 배열(`Linked List`)로 관리하고 있습니다. 이는 Hooks를 관리하고 있는 배열에 index로 접근할 수 있다는 뜻이며, 호출 순서에 의존하고 있다는 뜻입니다.

 

Hooks의 내부 관리 방식 📖

Hooks는 `컴포넌트의 최상위 레벨` 또는 커스텀 훅에서만 호출할 수 있습니다. 조건문, 반복문 또는 기타 중첩된 함수 내부에서는 훅을 호출할 수 없습니다.

 

각각 `useState`의 인자로 초기값을 받아 배열 구조분해 할당으로 state 변수와 state 설정자(setter) 함수를 받고 있습니다.

function App() {
  const [value1, setValue1] = useState('');
  const [value2, setValue2] = useState(0);

  return <button onClick={() => setValue1('react')}>Click Me</button>;
}

 

애플리케이션이 실행되면 React는 내부 공간에 state와 설정자 함수를 묶어 index를 가진 배열에 넣고 아래는 임의로 만든 상태 값 관리 배열입니다.

// 임의 상태 값 관리 배열
[
  [value1, setValue1],
  [value2, setValue2],
];

 

버튼을 클릭하면 `() => setValue1('react')`코드가 실행됩니다. 그러면 배열 묶음의 setValue1 설정자 함수가 실행된 것인데, setValue1은 임의로 만든 상태 값 관리 배열에서 index가 `0`입니다. 그렇다면 setValue1의 인자로 넘어온 값은, 다시 상태 값 관리 배열에서 index가 0인 state를 업데이트합니다.

 

예시 코드

React에서 Hooks를 조건문에 넣지 말라고 했지만 넣어서 상태 값 관리 배열을 만든다면

let firstRender = true;

function App() {
  let value3;
  if (firstRender) {
    [value3] = useState('dont');
    firstRender = false;
  }

  const [value1, setValue1] = useState('');
  const [value2, setValue2] = useState(0);

  return <button onClick={() => setValue1('react')}>Click Me</button>;
}
// 임의 상태 값 관리 배열
[
  [value3, undefined],
  [value1, setValue1],
  [value2, setValue2],
];

 

처음 if 문이 true였기 때문에 예상한 대로 잘 들어왔습니다.

사용자는 버튼을 클릭해 `setValue1`을 실행시키고 상태 값이 변경 돼 리렌더링이 발생으로 `App()`함수가 새로 호출됩니다. `firstRender1`의 값은 `false`가 되었고 `[value3]`을 반환하는 useState가 호출되지 않았습니다.

 

이 부분을 상태 값 관리 배열로 표현하면

// 임의 상태 값 관리 배열
[
  [value3, setValue1],
  [value1, setValue2],
  [value2, undefined],
];

 

React에서 state는 컴포넌트가 리렌더링 한 후에도 변수를 기억합니다. 따라서 리렌더링 된 상태 값인 `value3`은 기억 되기 때문에 index 0번 자리로 들어가게 됩니다. 그러면 `setValue1` 설정자 함수를 다시 호출했을 때 `value1` 값을 변경시키게 되는 것입니다. 

 

그렇다면 왜 React는 Hooks를 배열로 다루고 있을까요?

 

배열로 다루고 있지 않다면 생기는 문제점

이름 충돌

const [value1, setValue1] = useState(true);
const [value2, setValue2] = useState(true);

 

2개의 상태 값에 `true` 값이 세팅되어 있고 개발자는 그 상태 값이 `value1`과 `value2`라는 것을 알고 있지만 React 입장에서는 이를 알 수 없기 때문에 배열로 저장되지 않고 이름으로 접근하고 싶다면 useState에 상태 값에 대한 이름을 명확하게 작성해줘야 합니다.

 

useState의 첫 번째 인자로 상태 값을 가리키는 고유 식별자 이름을 넣어주고, 뒤에 초기 값을 넣도록 하는 겁니다.

const [value1, setValue1] = useState('value1', true);
const [value2, setValue2] = useState('value2', true);

 

그러면 호출 순서에 상관없이 상태 값에 대한 이름이 붙어있기 때문에 배열로 관리되지 않아도 됩니다.

 

그러나 두 상태 값 모두 `value1`이라고 이름을 지으면 오류가 발생할 소지가 있습니다.

 

동일한 Hooks를 두 번 호출할 수 없다

식별자 자체를 `Symbol`로 만들면 어떨까요? 첫 번째 인자로 이름을 줄 필요가 없으니 useState마다 충돌할 수 없습니다.

const symbol1 = Symbol();
const symbol2 = Symbol();

function App() {
  const [value1, setValue1] = useState(symbol1);
  const [value2, setValue2] = useState(symbol2);
  // ...

 

하지만 커스텀 Hooks이 존재한다면?

const symbol1 = Symbol();

function useCustomHook() {
  const [value, setValue] = useState(symbol1);
  return [value, setValue];
}

function App() {
  const [value1, setValue1] = useCustomHook();
  const [value2, setValue2] = useCustomHook();
  // ...

 

Symbol은 고유한 값이지만 `useCustomHook`을 여러 번 사용하기 시작하면 문제가 생깁니다. `symbol1`이라는 값은 함수 외부 공간에 생성되었고, useCustomHook을 호출하고 상태 값을 변경하는 순간 `symbol1`의 값이 동기화되어 있는 모든 커스텀 훅의 상태 값이 symbol1을 바라보고 있기 때문에 동일 값을 참조하고 있어 충돌이 발생합니다.

 

복사 붙여 넣기 어려움

위에서 문제는 Symbol값이 격리되지 않았기 때문입니다. Wrapper 함수를 만들어 값을 격리시키고 Symbol을 사용해 네임스페이스를 만들어보겠습니다.

function createUseCustomHook() {
  const symbol = Symbol(); // 클로저가 생성되고 격리됨. 즉, 호출 할 때마다 새로운 값이 생성.

  return function useCustomHook() {
    const [value, setValue] = useState(symbol);
    return [value, setValue];
  };
}

 

`createUseCustomHook`함수를 호출해 커스텀 훅을 다시 생성 후 사용할 수 있습니다.

const useNameFormInput = createUseCustomHook();
const useSurnameFormInput = createUseCustomHook();

// Component
export default function Component() {
  const name = useNameFormInput();
  const surname = useNameFormInput();
  // ...
}

 

이렇게 사용하면 문제는 해결되지만 매번 이름을 `useNameFormInput``useSurnameFormInput`으로 정확하게 만들어야 합니다.

 

근데 위 코드를 보면 `const surname = useNameFormInput()`은 개발자가 실수로 `useNameFormInput`을 두 번 선언한 것입니다. 

 

이렇게 Wrapper 함수를 두어 이름 변수를 격리시킨다면 이름 자체가 생겨 구분하기는 쉬워지지만, 연속해서 편리하게 호출할 수 없는 불편함이 생깁니다.

 

조건부로 선언하면 코드 파악이 어려움

props로 내려오는 `isActive`값에 의해 상태 값이 생성되고, 생성되지 않는데 상태 값이 언제 초기화되는지 알 수 없습니다.

function Counter(props) {
  if (props.isActive) {
    const [count, setCount] = useState('count');
    return (
      <p onClick={() => setCount(count + 1)}>
        {count}
      </p>;
    );
  }
  return null;
}

 

 

useEffect도 실행될지 안될지 알 수 없습니다.

function Counter(props) {
  if (props.isActive) {
    const [count, setCount] = useState('count');
    useEffect(() => {
      const id = setInterval(() => setCount(c => c + 1), 1000);
      return () => clearInterval(id);
    }, []);
    return (
      <p onClick={() => setCount(count + 1)}>
        {count}
      </p>;
    );
  }
  return null;
}

 

이 코드가 훨씬 깔끔한 코드입니다.

function Counter(props) {
  if (props.isActive) {
    return <TickingCounter />;
  }
  return null;
}

 

 

Hooks 간에 값을 전달할 수 없음

Hooks 호출 순서에 따라 동작하도록 만들면 연쇄적인 반응이 가능합니다.

const [value, setValue] = useState(1);
const state = useCustomHook(value); // state값 전달

 


출처 🏷️

https://talent500.co/blog/anti-patterns-in-react-that-you-should-avoid/

https://pozafly.github.io/react/react-is-managing-hooks-as-an-array/ 

반응형