TIL
클로저 (feat. useState)
closure
JS
2024.05.29.
목차보기

    클로저는 와닿지 않는 개념 중 하나다. 하지만 우리가 사용하고있는 React hook 중 useState도 클로저를 적용했단 사실을 아는가? 난 이 사실을 알고 클로저가 좀 더 와닿았다. 전반적인 클로저에 정리, useState를 예시로 작성하고자 한다.

    클로저에 대한 이해

    우선, MDN에 정의된 클로저의 정의는 이렇다. MDN 정의

    A closure is the combination of a function and the lexical environment within which that function was declared.
    클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.


    클로저를 이해하기 전, 렉시컬 스코프와 실행 컨텍스트를 이해하는 것이 도움이 될 것이다.
    짧게 설명하자면,
    • 렉시컬 스코프(정적 스코프): 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다.

    • 실행 컨텍스트: 자바스크립트에서 코드가 실행하는데 필요한 환경을 제공하고 이를 관리하는 영역이다.

      • 렉시컬 환경: 식별자와 식별자에 대한 바인딩된 값(환경 레코드), 그리고 상위 스코프에 대한 참조를 기록(외부 렉시컬 환경에 대한 참조)하는 자료구조로 실행 컨텍스트를 구성하는 컴포넌트다. (이전 정리했던 실행컨텍스트 글)
    • 함수 객체의 내부 슬롯 [[Environment]] : 렉시컬 스코프가 가능하려면 함수 정의가 위치하는 스코프를 기억해야한다. 이를 위해 함수는 자신의 내부 슬롯 [[Environment]]에 상위 스코프의 참조를 저장한다.

      • 함수 정의가 평가되어 함수 객체를 생성할 때 상위 스코프의 참조를 함수 객체 자신의 내부 슬롯 [[Environment]]에 저장하는데, 저장된 참조는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장될 참조값이다.

    const x = 1;
    
    function outerFunc() {
      var x = 10; //외부함수 변수 x
    
      var innerFunc = function () {
        console.log(x); //상위 함수의 변수에 접근
      };
    
      return innerFunc;
    }
    
    var inner = outerFunc();
    inner(); // 10

    outerFunc()을 호출 후 지역변수 x와 변수값 10을 저장하고 있던 outerFunc 함수의 실행 컨텍스트는 실행컨텍스트에서 pop되면서, 생명주기는 마감한다. 생명주기가 마감했기 때문에 지역변수 x는 더이상 유효하지 않는 걸로 보이지만. inner()를 호출하면 10이 찍힌다.

    어째서?

    위 코드의 innerFunc()가 평가될 때를 설명하자면,
    innerFunc() 함수 표현식으로 정의했기 때문에 런타임에 평가된다. innerFunc()는 자신의 [[Environment]] 내부 슬롯에 outerFunc 함수의 렉시컬 환경을 상위 스코프로 저장한다. (현재 실행 중인 실행 컨텍스트의 렉시컬 환경 = outerFunc)
    outerFunc()innerFunc()를 반환하고 생명주기는 종료되지만, outerFunc의 렉시컬 환경까지 소멸하는 것은 아니다. 해당 렉시컬 환경은 innerFunc 함수의 [[Environment]] 내부 슬롯에 의해 참조되고 있기 때문에 가비지 컬렉션의 대상이 되지 않는다 ! (가비지 컬렉터는 누군가가 참조하고 있는 메모리를 함부로 해제하지 않는다) 뒷 이야기를 하자면, 전역 변수 inner를 호출하면 innerFunc 함수의 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 푸시되며, 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에는 innerFunc 함수 객체의 [[Environment]] 내부 슬롯에 저장되어 있는 참조값이 할당되겠쥬?

    이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 중첩 함수를 클로저라고 한다.

    클로저를 사용하는 이유는?

    • 전역 변수 사용 최소화
      전역변수가 많으면 의도치 않게 어디에서든 접근하는 상황이 발생할 수 있다. 클로저를 이용하여 전역변수를 최소한으로 사용함으로써 이러한 실수나 예외적인 상황을 방지할 수 있다.
    • 상태 유지 (아래 useState로 보자.)
    • 정보은닉
      변수 값을 은닉하여, private 키워드를 흉내낼 수 있다.
      (기본적으로 자바스크립트는 접근제한자를 제공하지 않아 모든 프로퍼티와 메서드는 기본적으로 외부에 공개되어 있다.)

    클로저를 적용한 useState 구현

    리액트에서 컴포넌트의 렌더링 및 상태를 관리할 수 있도록 useState를 자주 사용했을 것이다. 상태관리를 하려면, 이전 값을 기억해야한다. 이때 클로저를 적용하여 간단한 구현을 할 수 있다. (원래 리액트 코드는 이보다 더 복잡하다.)

    const React = (function () {
      let val;
    
      function useState(initVal) {
        const state = val || initVal
        const setState = (newVal) => {
          val = newVal
        }
    
        return [state, setState]
      }
    
      function render(Component) {
        const C = Component()
        C.render()
    
        return C
      }
    
      return { useState, render }
    })()
    

    모듈 패턴을 사용해서 React 모듈안에 있는 useState로 구현한 코드다. 화면에 렌더링 해주기 위해 render 메소드도 추가

    function Component() {
      const [count, setCount] = React.useState(1)
    
      return {
        render: () => console.log(count),
        add: (num) => setCount(num),
      }
    }
    
    const App = React.render(Component) // 1
    App.add(5)
    const App2 = React.render(Component) // 5

    state가 유지되며 업데이트가 된다.


    Front Developer 김정희 😊

    글 목록보기