Frontend/Javascript

[Javascript] 클로저를 이용해 React.useState 따라하기

뽀또들 2024. 9. 12. 17:34

목차

  1. 클로저 개념 알아보기
  2. 클로저를 이용해 useState 구현해보기

1.  클로저 개념 알아보기

클로저의 개념을 알기 위해선 먼저 렉시컬 환경에 대해 알아야 합니다.

 

자바스크립트의 모든 실행 컨텍스트(함수, 코드 블록, 전역 등)는 렉시컬 환경이라는 객체를 갖습니다.

 

렉시컬 환경은 크게 환경 레코드외부 렉시컬 환경에 대한 참조으로 구성됩니다.

  • 환경 레코드: 해당 실행 컨텍스트의 지역 변수를 프로퍼티로 저장하고 있는 객체. `this` 같은 기타 정보도 여기에 저장됨.
  • 외부 렉시컬 환경에 대한 참조: 말 그대로 외부 렉시컬 환경에 대한 참조. 이를 이용해 외부 실행 컨텍스트의 변수에 접근할 수 있음.
// global

let name;
name = "뽀또";

/**
* 전역_렉시컬_환경: {
*   환경_레코드: {
*     name: "뽀또"
*   },
*   외부_렉시컬_환경: null,
* }
*/

 

이와 같이 스크립트 전체와 관련된 렉시컬 환경을 전역 렉시컬 환경이라고 합니다. 전역 렉시컬 환경을 간단히 표현해보면, 환경 레코드 안에 변수 food 값이 저장되고 전역 실행 컨텍스트는 외부 스코프가 없기 때문에 외부 렉시컬 환경이 null이죠.

let name = "뽀또";

function doing(something) {
	console.log(`${name}, ${something}하다.`); 
    function todo() {
    	something = "운동";
        console.log(`${name}, ${something}해라.`)
    }
    todo();
}

doing("개발"); // 뽀또, 개발하다.\n뽀또, 운동해라.
console.log(something); // ReferenceError: something is not defined

/*
todo_렉시컬_환경: {
  환경_레코드: {},
  외부_렉시컬_환경: doing_렉시컬_환경
}
              ↓
doing_렉시컬_환경: {
  환경_레코드: {
    something: "개발"
  },
  외부_렉시컬_환경: 전역_렉시컬_환경
}
              ↓
전역_렉시컬_환경: {
  환경_레코드: {
    name: "뽀또"
  },
  외부_렉시컬_환경: null
}
*/

함수를 호출하면 호출 중인 함수를 위한 내부 렉시컬 환경과 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경이 존재합니다.

여기서 내부 렉시컬 환경은 `doing_렉시컬_환경`이고 외부 렉시컬 환경은 `전역_렉시컬_환경`입니다.

 

코드에서 변수에 접근할 때 먼저 본인의 렉시컬 환경의 환경 레코드에서 검색합니다. 만약, 발견을 하지 못 하면 외부 렉시컬 환경 참조를 따라 외부 렉시컬 변수의 환경 레코드에서 검색하게 됩니다.

 

이런 동작 원리 덕분에, `doing_렉시컬_환경`에는 `name`이라는 변수가 저장되어 있지 않지만, "뽀또"를 잘 찾아서 콘솔 출력이 정상적으로 이루어질 수 있습니다.

 

동일한 이유로 todo 함수에서 doing 함수의 매개변수인 something에 접근할 수 있습니다. 하지만, doing 스코프 외부에서는 something에 접근하면 Reference 에러가 발생하죠.

 

let name = "뽀또";

function doing(something) {
	console.log(`${name}, ${something}하다.`);
    return (other) => {
    	something = other;
        console.log(`${name}, ${something}해라.`);
    }
}

const todo = doing("개발");  // 뽀또, 개발하다.
todo("운동")                 // 뽀또, 운동해라.

/*
todo_렉시컬_환경: {
  환경_레코드: {},
  외부_렉시컬_환경: doing_렉시컬_환경
}
              ↓
doing_렉시컬_환경: {
  환경_레코드: {
    something: "개발"
  },
  외부_렉시컬_환경: 전역_렉시컬_환경
}
              ↓
전역_렉시컬_환경: {
  환경_레코드: {
    name: "뽀또"
  },
  외부_렉시컬_환경: null
}
*/

이제 doing 함수에서 내부 함수를 리턴하는 형태로 코드를 수정했습니다. 리턴된 함수는 내부 함수이기 때문에, doing 함수의 매개변수에 접근할 수 있는 건 놀랍지 않습니다. 하지만, todo 함수가 호출된 곳은 전역 스코프로, doing 함수 스코프 내부가 아닙니다.

 

어떻게 doing 스코프 바깥에서 doing의 매개변수에 접근할 수 있었을까요?

 

바로, 자바스크립트에서 모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억하기 때문입니다. 함수는 `[[Environment]]`라는 숨김 프로퍼티를 갖는데, 이곳에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장됩니다.

 

`[[Environment]]` 덕분에 todo 함수의 호출 장소가 doing 함수의 스코프 바깥이어도 `something` 변수를 찾아낼 수 있었습니다.

 

이처럼 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 클로저라고 합니다.


2. 클로저를 이용해 useState 구현해보기

시작하기에 앞서, 아래 코드는 실제 React의 useState와는 거리가 멉니다. 클로저의 개념을 이용해 useState와 유사하게 동작하는 것 같아보이는 함수를 구현한 것입니다!

 

Try 1

클로저의 개념을 알았으니 이를 이용해서 useState를 구현해봅시다.

const createReact = () => {
    const useState = (initialValue) => {
        let state = initialValue;

        const setState = (newValue) => {
            state = newValue;
            console.log("inner:", state) // inner: 1
        }
        return [state, setState];
    };

    return { useState }
};
    
const React = createReact();

const [num, setNum] = React.useState(0);

console.log(num); // 0 -> OK
setNum(1);
console.log(num); // 0 -> ?!

 

아주 간단한 리액트를 구현해봤습니다.

 

useState 내부 변수로 state를, 내부 함수로 setState를 선언했습니다. 이렇게 클로저를 사용하면 setState 함수는 useState 외부에서도 state 값을 변경할 수 있죠.

 

그런데, 출력 결과를 보면 `num`이 바뀌지 않고 그대로 `0`을 출력하고 있습니다! 왜 그런걸까요..?

이는 setNum이 state 값을 변경하더라도, num은 useState가 처음 반환한 state의 스냅샷이기 때문입니다.

 

 

Try 2

const createReact = () => {
    let state = {};

    const useState = (initialValue) => {
        // 사용자가 제공한 키값을 통해 상태를 관리합니다
        const key = Symbol();

        if (!(key in state)) {
            state[key] = initialValue;
        }

        const setState = (newValue) => {
            state[key] = newValue;
        };

        return [() => state[key], setState];
    };

    return { useState };
};

// 사용 예제
const React = createReact();

const App = () => {
    const [num, setNum] = React.useState(0);
    const [name, setName] = React.useState('뽀또');

    console.log(num()); // 0
    setNum(1);
    console.log(num()); // 1

    console.log(name()); // 뽀또
    setName("초코송이");
    console.log(name()); // 초코송이    
};

App();
 

useState (NodeJS) - myCompiler

웹사이트에 퍼가기

www.mycompiler.io

 

먼저, 여러 개의 state를 React가 관리할 수 있도록 state를 객체로 관리하고, Symbol을 key 값으로 사용하도록 수정했습니다.

 

그리고, state의 변경을 트래킹 할 수 있도록 useState의 첫 번째 리턴 값을 getter 함수로 구현했습니다. 이렇게 하면 setState 함수가 state를 수정하면 num와 name에도 적용이 되는 것을 확인할 수 있습니다.

 

getter 함수이기 때문에, 실제 React의 useState 처럼 사용할 수 없다는 아쉬움이 남습니다.. 추후에 더 정확한 구현 방식을 찾으면 업데이트 하도록 하겠습니다.

 


 

관련 자료

모던 자바스크립트 튜토리얼얼 - 변수의 유효 범위와 클로저

 

변수의 유효범위와 클로저

 

ko.javascript.info

 

[10분 테코톡] 💙 하루의 실행 컨텍스트

 

'Frontend > Javascript' 카테고리의 다른 글

[Javascript] 호이스팅에 대해 설명해주세요.  (9) 2024.08.28
[Javascript] 재귀와 스택  (0) 2024.08.12