React

리액트(React) - useState 내부 구현과 동작 원리(feat : 클로저)

Chrysans 2023. 9. 25. 17:46
728x90
반응형

 


 

 

리액트 useState

 

 

리액트에서 함수형 컴포넌트 이전 클래스형 컴포넌트 사용시에는 상태를 지역변수 state에 정의하고 상태를 변경할 메소드 안에 setState 메소드를 넣어서 상태를 변경 했다.

그럼 왜 state변수를 직접 변경하지 않고 setState를 통해서 했는지는 해당 메소드를 사용해서 리액트 컴포넌트에 상태변경에 대해서 알려주고 리렌더링을 요청 하기 위해서다.

 


 

함수형 컴포넌트

 

리액트 함수형 컴포넌트에서는 useState 라는 React Hooks를 사용해서 상태를 관리한다.

해당 훅은 초기값을 받아서 [state, setState] 의 배열을 반환하며 [상태, 상태 변경 핸들러] 로 보면 된다.

 

함수형 컴포넌트는 렌더가 필요 할 때마다 함수를 재호출 하는데 렌더링은 곧 함수 호출이다.

함수가 재호출 되었을때 이전 상태를 기억하고 있어야 하고 리액트에서는 클로저 방식을 사용해서 해결한거 같다.

 

 

useState의 기본 구조

// dispatcher를 가져오는 함수
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  // dispatcher가 없으면 (컴포넌트 외부에서 호출되면) 에러 발생
  if (dispatcher === null) {
    throw new Error('Hooks can only be called inside a component');
  }
  return dispatcher;
}

// useState 실제 구현
function useState(initialState) {
  // 현재 사용 가능한 dispatcher를 가져옴
  const dispatcher = resolveDispatcher();
  // dispatcher의 useState 실행
  return dispatcher.useState(initialState);
}

 

  • useState는 훅의 가장 기본적인 형태
  • 초기값(initialState)을 받아서 [상태, 상태변경함수]를 반환

 

dispatcher의 종류

 

const ReactCurrentDispatcher = {
  current: null,
}

export default ReactCurrentDispatcher
  • ReactCurrentDispatcher는 싱글톤 객체로 관리됨
    • 싱글톤 객체 : 객체의 인스턴스가 오직 하나만 생성되는 패턴을 의미합니다.
  • 렌더링 단계에 따라 다른 dispatcher가 할당됨
  • 컴포넌트 렌더링 전에 할당되고 렌더링 후에 초기화됨
  • 잘못된 훅 호출을 방지하기 위한 안전장치로도 활용됨
더보기

과정이 복잡하고 이해가 안되는 부분은 생략 했습니다 ㅎㅎ..

 

 


 

Hook의 호출 순서와 규칙

 

상태 저장 위치

// 잘못된 예시 (상태가 함수 내부에 있는 경우)
function useState(initialValue) {
    let state = initialValue; // 매번 새로 할당됨
    return [state, setState];
}

// 올바른 구현 (외부에 상태 저장)
let states = []; // 컴포넌트의 모든 상태를 저장하는 배열
let currentIndex = 0;

function useState(initialValue) {
    if (states[currentIndex] === undefined) {
        states[currentIndex] = initialValue;
    }
    // ...
}
  • 상태가 담기는 곳이 useState 내부에 있다면 useState를 호출할 때마다 함수의 메모리 공간으로 새로 할당하기 때문에 초기값만 나오게 된다. 따라서 state는 함수의 외부에서 할당되어 여러번 호출되는 useState 함수가 이 지역 변수를 공유할 수 있도록 되어있다.

 

순서의 중요성

// 잘못된 예시 (조건부 훅 사용)
function Component() {
    const [name, setName] = useState("John");
    
    if (name === "John") {
        const [age, setAge] = useState(25); // 조건에 따라 건너뛸 수 있음
    }
    
    const [email, setEmail] = useState("john@example.com");
}

// 올바른 예시
function Component() {
    const [name, setName] = useState("John");
    const [age, setAge] = useState(25);     // 항상 같은 순서 보장
    const [email, setEmail] = useState("john@example.com");
}
  • 리액트에서는 useState 외부에 선언된 state를 배열 형식으로 관리하며 선언된 state 들은 배열에 순서대로 저장된다.

 

컴포넌트별 상태 관리

// 컴포넌트 ID와 상태 매핑
const componentStates = new Map();

function Component1() {
    // componentStates.get('Component1') -> [state1, state2, ...]
    const [count, setCount] = useState(0);
}

function Component2() {
    // componentStates.get('Component2') -> [state1, state2, ...]
    const [count, setCount] = useState(0);
}
  • 상태 배열은 컴포넌트를 유일하게 구분 짖는 키를 통해 접근 할수 있다.

 

훅 규칙 위반 시 문제

// 문제가 될 수 있는 코드
function Component() {
    for (let i = 0; i < 3; i++) {
        const [count, setCount] = useState(0); // 🚫 반복문에서 훅 사용
    }
    
    function handleClick() {
        const [value, setValue] = useState(0); // 🚫 일반 함수 내에서 훅 사용
    }
}

// 올바른 코드
function Component() {
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);
    const [count3, setCount3] = useState(0);
}
  • 반복문, 조건문 또는 중첩 함수에서 호출하면 안되는 이유도 위와 같은 이유 때문인데 해당 조건을 지키지 않으면 순서가 있는 배열이 실행 순서가 달라 질수 있기 때문에 원하는 상태를 가져올수 없는 일이 생길수 있다.

커스텀 훅에서의 사용

// 올바른 커스텀 훅 사용
function useCounter(initialValue = 0) {
    const [count, setCount] = useState(initialValue);
    
    const increment = () => setCount(count + 1);
    const decrement = () => setCount(count - 1);
    
    return [count, increment, decrement];
}

// 컴포넌트에서 사용
function Counter() {
    const [count, increment, decrement] = useCounter(0);
    return (
        <div>
            <button onClick={decrement}>-</button>
            {count}
            <button onClick={increment}>+</button>
        </div>
    );
}
  • 훅은 함수 컴포넌트, 커스텀 훅 내에서만 호출 가능하다

이러한 구조 덕분에:

  1. 상태가 컴포넌트 간에 독립적으로 유지됨
  2. 동일한 순서로 훅이 호출됨을 보장
  3. 상태 값이 렌더링 간에 보존됨
  4. 각 컴포넌트의 상태가 올바르게 추적됨

이 모든 규칙들은 React의 상태 관리 시스템이 안정적으로 작동하기 위한 중요한 기반이 됩니다.

해당 조건들이 지켜지면 렌더링시 동일한 순서로 훅이 호출되는 것을 보장받을수 있다.

 

 


 

useState와 클로저의 깊은 관계 이해하기

클로저(Closure)란?

클로저는 함수가 자신이 선언된 렉시컬 환경을 기억하고 접근할 수 있는 특성을 말합니다. 간단히 말해, 함수가 자신이 생성될 당시의 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 메커니즘입니다.

 

function createCounter() {
    let count = 0;  // 외부 변수
    
    return function() {  // 내부 함수
        return count++;  // 외부 변수 접근
    }
}

const counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1

 

 

useState와 클로저의 관계

useState가 클로저를 활용하는 방식을 더 자세히 살펴보겠습니다:

const React = (function() {
    let _state = []; // 상태를 저장할 배열
    let _index = 0;  // 현재 상태의 인덱스

    function useState(initialValue) {
        const currentIndex = _index; // 클로저: currentIndex 값을 캡처
        
        // 첫 렌더링시에만 초기값 설정
        if (_state[currentIndex] === undefined) {
            _state[currentIndex] = initialValue;
        }

        // setState 함수도 클로저: currentIndex를 기억
        const setState = (newValue) => {
            _state[currentIndex] = newValue;
            rerender();
        };

        _index++;
        return [_state[currentIndex], setState];
    }

    function rerender() {
        _index = 0;
        renderApp(); // 가정: 전체 앱을 다시 렌더링
    }

    return { useState };
})();

// 사용 예시
function Counter() {
    const [count, setCount] = React.useState(0);
    const [text, setText] = React.useState("hello");

    console.log("현재 카운트:", count);
    console.log("현재 텍스트:", text);

    // 각 setState는 자신의 currentIndex를 기억하고 있음
    return {
        increment: () => setCount(count + 1),
        updateText: () => setText("world")
    };
}

// 테스트
const counter = Counter();
counter.increment();  // count만 변경됨
counter.updateText(); // text만 변경됨

 

클로저가 캡처하는 값:

const currentIndex = _index; // 이 값이 클로저에 의해 보존됨

const setState = (newValue) => {
    _state[currentIndex] // setState는 자신의 currentIndex를 기억
};

 

여러 개의 독립적인 상태 관리:

const [count, setCount] = React.useState(0);    // currentIndex: 0
const [text, setText] = React.useState("hello"); // currentIndex: 1

// setCount는 항상 _state[0]을 변경
// setText는 항상 _state[1]을 변경

 

 

  • 각 useState 호출이 자신만의 고유한 인덱스를 기억
  • setState 함수가 생성될 때의 인덱스를 계속 기억
  • 다른 상태들과 독립적으로 자신의 상태만 업데이트

 


 

https://covelope.tistory.com/manage/newpost/74?type=post&returnURL=ENTRY

 

티스토리

좀 아는 블로거들의 유용한 이야기, 티스토리. 블로그, 포트폴리오, 웹사이트까지 티스토리에서 나를 표현해 보세요.

www.tistory.com

https://covelope.tistory.com/entry/guide-to-react-javascript-memory-leak-analysis-and-optimization

 

React & JavaScript 메모리 누수 분석과 해결 방법 - 최적화 노하우

안녕하세요! 오늘은 React와 JavaScript 애플리케이션에서 발생하는 메모리 누수 문제를 깊이 있게 다뤄보겠습니다. 실제 프로젝트에서 마주칠 수 있는 다양한 시나리오와 해결 방법을 함께 알아보

covelope.tistory.com

https://covelope.tistory.com/

 

Ch.Covelope

Chrys_Code_Develope. https://velog.io/@goggg8822

covelope.tistory.com

 


 

728x90
반응형