[JS] 다시 정리해보는 클로저

요들레이후·2023년 10월 22일
0

Javascript

목록 보기
11/11
post-thumbnail

Summary 🧚‍♀️

💡 자바스크립트 클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합으로, 외부 함수의 변수에 접근할 수 있는 내부 함수를 의미합니다.

Concept 🧐

스코프

변수 이름, 함수 이름, 클래스 이름과 같은 식별자가 본인이 선언된 위치에 따라 다른 코드에서 자신이 참조될 수 있을지 없을지 결정되는 것을 의미합니다.

function add(x, y) {
	console.log(x, y); // 3, 5
	return x + y;
}

add(3, 6);

console.log(x, y);
// ReferenceError: x is not defined~

변수 x, y는 자신이 선언된 위치에 따라 본인의 유효범위가 결정되는 것을 확인할 수 있습니다.

  • 스코프 체인
var x = '전역 x';

function outer() {
	var y = 'outer함수의 지역 y';
	console.log(x); // 전역 x
	console.log(y); // outer함수의 지역 y

	function inner() {
		var x = 'inner함수의 지역 x';
		console.log(x); // inner함수의 지역 x
		console.log(y); // outer함수의 지역 y
	}
	inner();
}

outer();
console.log(x); // 전역 x
console.log(y); // ReferenceError 어쩌고

스코프는 계층적으로 연결되어있고, 단방향성입니다.(아래에서 위로)
스코프 체인은 물리적으로 존재합니다.
자바스크립트 엔진은 스코프 체인을 통해 변수를 참조합니다.
함수가 어떤 변수를 참조하려고 하는데, 자신의 스코프 안에 원하는 변수가 없으면 하나 위로 올라갑니다.
전역 스코프에도 원하는 변수가 없으면 Reference Error를 내뿜습니다.
하위 스코프에서 상위 스코프에 있는 변수를 참조할 수 있습니다.

  • 상위스코프 결정

함수가 호출되는 시점에 동적으로 상위 스코프가 결정되는 것을 동적 스코프라고 합니다. 프로그램 런타임 도중에 실행 컨텍스트나 호출 컨텍스트에 의해서 스코프가 결정되는 것을 의미합니다.

함수가 정의 되는 시점에 상위 스코프가 결정되는 것을 정적 스코프(===렉시컬 스코프) 라고 합니다.
자바스크립트는 렉시컬 스코프를 따르기 때문에 함수는 태어나자마자 상위 스코프가 결정되고 이후에 해당 함수에 의해 함수 객체가 생성되면 해당 함수 객체는 본인의 상위 스코프를 항상 알 수 있게 됩니다.
자바스크립트에서 함수는 태어나면 본인의 내부 슬롯([[…]])에 상위 스코프에 대한 참조를 저장합니다.

  1. 호출된 함수의 실행 컨텍스트를 생성하고 스택에 push 합니다.
  2. 함수는 본인의 렉시컬 환경을 생성합니다. 렉시컬 환경은 어떠한 코드가 어디서 실행되고 본인 주변에 어떤 코드들이 있는지 구체적인 정보를 담고 있는 환경(자료구조)이라 할 수 있습니다.
  3. 코드의 실행이 끝나면 스택에서 pop하여 제거합니다.

클로저

중첩함수 inner가 이미 생명주기를 마감한 outer함수의 지역변수 x를 참조할 수 있다면 이때 inner를 클로저라고 합니다. 즉 상위 함수의 변수를 inner에서 참조할 수 있다면 inner함수가 클로저입니다.

const x = 1;

function outer() {
	const x = 10;
	const inner = function() {
		console.log(x);
	};

	return inner;
}

const ella = outer();
ella(); // 문제) ella는 어떤 값을 콘솔에 찍을까요? 10

outer함수의 생명주기가 끝나면 실행컨텍스트 스택에서 제거가 됩니다. outer함수는 ella한테 inner함수를 반환하면서 사라지기 때문에 ella는 inner함수 객체를 참조합니다.

이게 outer함수가 생명주기를 마감하면서 종료되었을 때의 모델입니다.

outer함수는 실행컨텍스트에서 제거가 되었지만, outer함수의 렉시컬 환경까지는 소멸되지 않았습니다.

ella는 inner함수 객체를 참조하고 있고, inner함수 객체는 본인의 내부 슬롯에 저장된 outer함수의 렉시컬 환경을 참조하기 때문에 가비지 컬렉션의 대상이 되지 않습니다.

ella에 의해 inner함수를 호출하면 outer함수 안에 있는 10을 값으로 가지고 있는 변수 x를 다시 참조할 수 있게 됩니다.
비록 inner함수 객체 기준, 내부 함수는 실행컨텍스트에서 사라지고 생명주기가 마감되었지만, 내가 기억하고 있는 내부 슬롯에 저장된 상위 스코프에 의존하여, 상위 스코프 내 식별자를 참조할 수 있게 되는 것입니다.

이렇게 상위 스코프의 식별자를 참조하고 있고, 본인의 외부 함수보다 더 오래 살아있다면 클로저라고 합니다.

이러한 클로저는 본인의 상위 스코프에서 현재 참조하는 식별자만을 기억 하는데, 이렇게 클로저에 의해 참조된 변수를 '자유변수' 라고 합니다. inner함수도 outer함수 내부에 있는 x를 참조하고 그 x가 자유 변수입니다.

닫혀있다는 느낌이 드는 ‘클로저’라는 이름은 함수 본인이 기억하고 있는 자유 변수에 의해 닫혀있다(closed되어있다)고 생각할 수 있습니다.

이러한 클로저는 하나의 state가 의도치 않게 변경되지 않도록 state를 은닉하고, 또는 특정 함수에게만 state변경을 허용하기 위해 사용한다고 합니다.

자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로 모든 함수는 클로저입니다.

Advantages 🤩

  • 상태 유지(데이터 보존): 클로저는 외부 함수의 지역 변수를 내부 함수에서 참조하여, 그 값을 유지할 수 있어 상태 관리에 유용합니다.

  • 데이터 은닉(캡슐화): 클로저를 사용하면 특정 변수나 함수를 공개하지 않고, 외부로부터 보호하면서 코드를 작성할 수 있습니다. 즉, 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게하는 폐쇄성을 갖습니다.

모듈화에 유리: 클로저 함수를 각각의 변수에 할당하면 각자 독립적으로 값을 사용하고 보존할 수 있습니다. 이와 같이 함수의 재사용성을 극대화하고 함수 하나를 독립적인 부품의 형태로 분리하는 것을 모듈화라고 합니다. 클로저를 통해 데이터와 메소드를 묶어다닐 수 있기에 모듈화에 유리합니다.

Disadvantages 😡

  • 메모리 사용 : 클로저는 메모리를 차지하기 때문에 과도하게 사용하면 웹 페이지의 성능에 영향을 줄 수 있습니다.
    • 가비지 컬렉션 코드 실행시 객체가 생성되면 자동으로 힙 메모리를 할당하게 됩니다. 이것이 쓸모 없어졌을 때 자동으로 해제 시켜주는 기능을 가비지컬렉션이라고 합니다. 자바스크립트 엔진은 직간접적으로 참조되지 않은 객체들을 메모리에서 해제하여 메모리 공간을 확보하는 방식으로 동작합니다. 하지만 클로저는 본인의 내부 슬롯에 저장된 외부 함수의 렉시컬 환경을 참조하기 때문에 가비지 컬렉션의 대상이 되지 않습니다. 그래서 참조되고 있다면 메모리에 계속 남게 되므로, 성능에 영향을 주게 됩니다.
    • 해결방법 : 사용하지 않을 때 참조를 끊어준다면 메모리 해제가 가능합니다. 가장 쉬운 방법은 쓰이지 않는 시점에 외부에서 null이나 undefined를 할당하는 방법입니다. 클로저가 더 이상 필요없어졌을 때 아래와 같이 메모리를 쓰지 않도록 변경합니다.
      function getClosure() {
        let count = 0;
        return function () {
          count++;
          console.log(count);
        };
      }
      let closure = getClosure();
      closure(); // 1
      closure(); // 2
      closure = null;
      내부에서 메모리를 해제하는 방법은 setInterval이나 이벤트 리스너로 달아줄 수 있습니다.
      // 1. setInterval
      function getClosure() {
        let count = 0;
        let interval;
      
        let inner = function () {
          count++; 
          console.log(count);
          if (count >= 5) {
            clearInterval(interval);
            inner = null;  // count가 5가 되었을 때 메모리를 해제
          }
        };
      
        interval = setInterval(inner, 1000); // count라는 외부 변수를 참조
      }
      getClosure();
      
      // 2. 이벤트 리스너
      function getClosure() {
        const btnElem = document.querySelector('.my-button');
        let count = 0;
      
        let onClick = function () {
          count++;
          console.log(count);
          if (count >= 5) {  // count가 5가 되었을 때, 버튼에서 핸들러를 제거하고 함수를 null로 바꾸면서 메모리를 해제
            btnElem.removeEventListener('click', onClick);
            onClick = null;
          }
        };
      
        btnElem.addEventListener('click', onClick);
      }
      getClosure();

Example Case 👏

간단 예시

function getAdd() {
  let foo = 1

  return function () {
    foo += 1

    return foo
  }
}

const add = getAdd()

console.log(add()) // 2
console.log(add()) // 3
foo = 9999 // 'foo' is not defined.
console.log(add()) // 4

외부에서 접근할 수 없지만 내부에서는 add 함수 실행을 통해 접근할 수 있습니다.

React useState에서 클로저

함수형 컴포넌트에서 이전 상태와 현 상태의 변경이 있는지를 감지하기 위해서는 함수가 실행되었을 때 이전 상태에 대한 정보를 가지고 있어야 합니다. React는 이 과정에서 클로저를 사용합니다.

//node_modules/react/cjs/react.development.js
function useState(initState) {
	var dispatcher = resolveDispatcher();
	return dispatcher.useState(initialState);
}

useState는 initState를 인자로 받는 함수로 선언되어 있습니다. 함수를 보면 resolveDispatcher라는 또다른 함수를 통해 리턴된 dispatcher의 useState 메소드에 initialState를 전달한 결과를 리턴하고 있습니다.

function resolveDispatcher() {
	var dispatcher = ReactCurrentDispatcher.current;

	if (!(dispatcher !== null)) {
		{
			throw Error('Invalid hook call. Hooks can only be called inside of~ ~~')
		}
	}
}

resolveDispatcher함수에서는 다시 ReactCurrentDispatcher안의 current값을 리턴하고 있습니다.

var ReactCurrentDispatcher = {
	current: null
};

ReactCurrentDispatcher는 current라는 값을 담은 변수입니다.

즉, useState는 외부에 선언된 상태값에 접근해서 이전 상태를 가져오고, 변경된 상태값을 관리하고 있습니다. 함수형 컴포넌트도 결국 함수이기 때문에, 클로저를 통해 선언되는 시점에 접근 가능했던 외부 상태값에 계속 접근할 수 있는 것입니다. 함수형 컴포넌트에서 상태값을 변경하면 외부의 값이 변경되고, 리렌더링(=함수 재호출)을 통해 새로운 값을 받아오게 됩니다.

Wrap-up 🍪

클로저는 함수의 렉시컬 환경을 기억하여 다양한 패턴과 기능을 구현할 수 있게 해줍니다. 그러나 메모리 사용 증가에 주의해야 합니다.

클로저에 대한 고찰 (소개 / 활용 / 단점 / 메모리)

React Hook과 Closure의 관계 (feat. useState)

useState Hook과 클로저

profile
성공 = 무한도전 + 무한실패

0개의 댓글