ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 클로저에 대해 알아보기 with useState
    FrontEnd/JavaScript 2024. 10. 9. 16:19

    클로저란 실행 컨텍스트와 렉시컬 스코프의 조합으로 함수가 선언 되었을 당시에 환경을 기억하여 상위 스코프내의 변수에 접근 할 수 있게 해주는 문법이다.

    아래 함수를 실행해보면 10이 출력되는 것을 통해 inner 함수가 상위 스코프인 outer의 _var를 참조하는 것을 알 수 있다.

    이는 클로저에 의해 접근 가능한 것.

    const outer = () => {
        let _var = 10;
        const inner = () => console.log(_var);
    
        inner();
    };
    
    outer(); // 10

    아래 함수를 예측해보면 어떻게 출력될까

    const outer = () => {
        let _var = 10;
        const setVar = (newVal) => {
            _var = newVal;
        };
    
        return [_var, setVar];
    };
    
    const [state, setState] = outer();
    
    console.log(state);
    setState(30);
    console.log(state);

    setState에 의해 값이 변경 되었으니 10, 30이 출력될 것으로 예상되지만 결과값은 10 10으로 값에 변화가 없다.

    setVar 함수 내부에서 콘솔을 통해 찍어보면 10, 새로운 변수 할당: 30  ,10 이  출력된다.

    const outer = () => {
        let _var = 10;
        const setVar = (newVal) => {
            _var = newVal;
            console.log('새로운 변수 할당:', _var);
        };
    
        return [_var, setVar];
    };
    
    const [state, setState] = outer();
    
    console.log(state);
    setState(30);
    console.log(state);

    setState는 제대로 동작했는데 state는 왜 계쏙 10을 출력하는 이유는 outer가 실행되면서 복사 된 _var는 10으로 고정 되어 있기 때문이다.

    이것을 원하던 로직으로 해결하기 위해선 값을 복사하는 것이 아닌 호출할 때마다 값을 불러오는 get 함수를 짜면 된다.

    const outer = () => {
        let _var = 10;
        const getVar = () => _var;
        const setVar = (newVal) => {
            _var = newVal;
            console.log('새로운 변수 할당', _var);
        };
    
        return [getVar, setVar];
    };
    
    const [state, setState] = outer();
    
    console.log(state());
    setState(30);
    console.log(state());

    이렇게 작성하면 state를 실행할 때마다 외부 함수에서 기억하고 있는 var의 메모리에 접근한 뒤 그 안에 담겨진 값을 그때그때마다 가져와준다.

    그렇다면 아래 코드의 결과는 어떻게 될까

    const ZeroJin = (function () {
        let state;
    
        function useState(initialValue) {
            if (state === undefined) {
                state = initialValue;
            }
    
            const setState = (newValue) => {
                state = newValue;
                render(); // 상태 변경 시 리렌더링
            };
    
            return [state, setState];
        }
    
        function render() {
            Counter();
        }
    
        return { useState, render };
    })();
    
    // 사용 예시
    function Counter() {
        const [count, setCount] = ZeroJin.useState(0);
        console.log(`${stage} 단계의 현재 count 값:`, count);
        return {
            increment: () => setCount(count + 1),
            decrement: () => setCount(count - 1),
            getCount: () => count,
        };
    }
    
    let stage = 0;
    // 초기 렌더링
    ZeroJin.render();
    
    // 카운터 조작
    const counter = Counter();
    stage++;
    counter.increment();
    stage++;
    counter.increment();
    stage++;
    counter.decrement();
    
    //
    console.log('Final count:', counter.getCount());

    결과는 0,1,1,-1이다.

    usetState의 반환 값을 const getState = () => state 로 반환하면 state원하는 대로 적용되어 0,1,2,1로 원하는 로직대로 출력 됨.

    이 때 아래와 같이 각각의 count와 text를 선언했다면 어떻게 될까

    function Counter() {
        const [count, setCount] = ZeroJin.useState(0);
        const [text, setText] = ZeroJin.useState('initial Text');
        console.group(stage);
        console.log(`${stage} 단계의 현재 count 값:`, count());
        console.log(`${stage} 단계의 현재 text 값:`, text());
        
        return {
            increment: () => setCount(count() + 1),
            decrement: () => setCount(count() - 1),
            getCount: () => count(),
        };
    }
    
    // 0
    //   0 단계의 현재 count 값: 0
    //   0 단계의 현재 text 값: 0
    //   0
    //     0 단계의 현재 count 값: 0
    //     0 단계의 현재 text 값: 0
    //     1
    //       1 단계의 현재 count 값: 1
    //       1 단계의 현재 text 값: 1
    //       2
    //         2 단계의 현재 count 값: 2
    //         2 단계의 현재 text 값: 2
    //         3
    //           3 단계의 현재 count 값: 1
    //           3 단계의 현재 text 값: 1
    //           Final count: 1

    text는 count와 같은 같은 값이 출력된다.

    이러한 이유는 Zerojin이 즉시 실행 함수로 실행되면서 private한 공간을 만들면서 동일한 클로저를 공유하기 때문이다.

    이 같은 점을 피하기 위해 state를 객체 또는 배열로 설정하는 방법이 있다. 실제로 리액트에서 훅 타입을 살펴보면 아래와 같다.

    export type Hook = {
      memoizedState: any, //훅의 현재 상태를 저장합니다.
      baseState: any, //업데이트 큐를 처리하기 전의 초기 상태입니다.
      baseQueue: Update<any, any> | null, //처리되지 않은 업데이트들의 큐입니다.
      queue: any, //향후 처리될 업데이트들의 큐입니다.
      next: Hook | null, //다음 훅을 가리키는 포인터입니다.
    };

    다음 훅을 가리키는 포인터를 통해 훅들은 연결 리스트로 이루어진 것을 유추할 수 있고 아래처럼 선언된 훅 state는 

    Hook1 (count) -> Hook2 (name) -> Hook3 (isActive) -> null 로 연결 된 것을 알 수 있다.

    function MyComponent() {
      const [count, setCount] = useState(0);
      const [name, setName] = useState('John');
      const [isActive, setIsActive] = useState(true);
    }

    이를 통해 각각의 state들은 독립적으로 작동되어 dispatcher에 의해 값 변경은 공유되지 않는다.

    (내가 작성한 코드에서 연결 리스트 형식으로 hook을 작성하고 싶었지만 논리적 부족함으로 배열로 작성해봄)

    const ZeroJin = (function () {
        let hooks = null;
        let currentHook = 0;
    
        function useState(initialValue) {
            if (hooks === null) {
                hooks = [];
            }
    
            const hookIndex = currentHook;
            currentHook++;
    
            if (hooks[hookIndex] === undefined) {
                hooks[hookIndex] = initialValue;
            }
    
            const setState = (newValue) => {
                hooks[hookIndex] = newValue;
                render();
            };
    
            return [() => hooks[hookIndex], setState];
        }
    
        function render() {
            currentHook = 0;
            const result = Counter();
            return result;
        }
    
        return { useState, render };
    })();
    
    // 사용 예시
    function Counter() {
        const [count, setCount] = ZeroJin.useState(0);
        const [text, setText] = ZeroJin.useState('initial Text');
    
        return {
            increment: () => setCount(count() + 1),
            decrement: () => setCount(count() - 1),
            updateText: (newText) => setText(newText),
            getCount: () => count(),
            getText: () => text(),
        };
    }
    
    let stage = 0;
    // 초기 렌더링
    let counter = ZeroJin.render();
    stage++;
    counter.increment();
    counter.updateText('first');
    
    console.group(`Stage ${stage}`);
    console.log(counter.getCount(), counter.getText());
    // 1 first
    console.groupEnd();
    
    stage++;
    counter.increment();
    counter.updateText('second');
    
    console.group(`Stage ${stage}`);
    console.log(counter.getCount(), counter.getText());
    // 2 second
    console.groupEnd();
    
    stage++;
    counter.decrement();
    counter.updateText('third');
    
    console.group(`Stage ${stage}`);
    console.log(counter.getCount(), counter.getText());
    // 1 third
    console.groupEnd();

     

    참고: https://github.com/ZacharyL2/mini-react/blob/master/src/mini-react.ts

     

    mini-react/src/mini-react.ts at master · ZacharyL2/mini-react

    Implement Mini-React in 400 lines of code, a minimal model with asynchronous interruptible updates. - ZacharyL2/mini-react

    github.com

     

    https://ricki-lee.medium.com/400%EC%A4%84%EC%9D%98-%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%82%98%EB%A7%8C%EC%9D%98-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%B6%80%EC%A0%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EC%9B%90%EB%A6%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%8B%AC%EC%B8%B5-%EC%97%B0%EA%B5%AC-f4c51b96001d

Designed by Tistory.