본문 바로가기
FrontEnd/JavaScript

클로저에 대해 알아보기 with useState

by 위그든씨 2024. 10. 9.

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

아래 함수를 실행해보면 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