[ 자바스크립트 ] 클로저 (Closure)

김수연·2022년 9월 28일
0
post-thumbnail

실행 컨텍스트
Lexical scope
closure
private
비동기

📌 실행 컨텍스트

자바스크립트엔진은 스크립트를 실행하다가 함수를 만나면 실행컨텍스트를 생성한다.
이 실행컨텍스트에 함수가 실행될 때의 환경을 저장하며 이렇게 만들어진 실행컨텍스트는 자신만의 스코프(유효범위)를 갖게된다.

var x = 10
var y = 20

function print(){
  var x = 5
  console.log(x, y)
}

print() // 5,20

실행 컨텍스트 입장에서 살펴보자면

  1. GO ( x= 10, y= 20, print= function )
  2. print 함수 컨텍스트 ( x= 5 )
  3. 참조한 변수 : x= 5, y= 20

🚥 우선 자바스크립트가 실행되면 글로벌 오브젝트(GO)를 생성한다.
GO에는 전역 변수들과 함수들이 프로퍼티로 저장된다 ( x = 10, y = 20, print = function )

🚥 자바스크립트엔진이 print() 함수를 실행하게되면 새로운 실행컨텍스트가 생성된다. ( print 컨텍스트는 x = 5를 속성으로 저장한다. )

🚥 print 컨텍스트에 y 변수가 없으니 바로 상위 컨텍스트에서 y를 찾게된다.

🚥 만약에 GO에도 y가 선언되어있지 않다면 'y is not defined' 라는 컨퍼런스 에러가 발생한다.

즉, 변수를 참조할 땐 함수가 실행된 컨텍스트 내부를 먼저 살펴보고 없으면 상위 컨텍스트를 계속해서 찾는데 이것이 '스코프 체인'이다.

📌 Lexical Scope

Lexical Scope는 함수와 변수의 Scope를 선언된 위치를 기준으로,
Dynamic Scope는 함수나 변수의 Scope를 호출된 시점을 기준으로 사용한다.

자바스크립트는 Lexical Scope를 사용하는 점을 유념해서 아래 코드를 살펴보자

var x = 1;

function print() {
    console.log(x); // 1
}

function dummy() {
    var x = 999;
    print();
}

dummy();
  • dummy 함수를 호출하면 변수 x를 새롭게 선언, 할당하고 print 함수를 호출한다.
  • print 함수가 호출되면 선언된 위치를 파악하고 x가 전역 변수로 선언, 1을 할당 받은 뒤니까 전역 변수 x의 값을 출력한다.

만약 Dynamic Scope로 스코프를 정한다면 실행 중 동적으로 변하니까 x값을 999로 출력한다.

var x = 1;

function outer() {
    var x = 2;
    function inner() {
        console.log(x)
    }

    inner();
}

outer();
  • inner 함수가 선언된 위치를 중심으로 x변수를 찾는다면
    inner 함수 (없음) -> 외부함수 outer 접근 (발견) -> x = 2 출력
function init() {
	var name = "Mozilla"; 
   
    function displayName() { 
    	alert(name); 
    }
   
    displayName();
}

init();

위에서본 설명들을 조합해서 코드를 살펴보자면

  • init 함수가 실행되면 name 변수와 displayName 함수가 선언되고 실행된다.
  • displayName 함수가 선언된 위치를 기준으로 현재 실행 컨텍스트에는 name 변수가 없으니 외부 함수(init)를 살핀 후 name을 찾아 그 값인 Mozilla를 출력한다.

그럼 다음 예제도 같이 살펴보자

function makeFunc() {
	var name = "Mozilla";
    function displayName() {
    	console.log(name); // Mozilla
    }
    return displayName;
}

var myFunc = makeFunc();

myFunc();
  • 위의 예제 코드와 같은 결과를 출력하지만 displayName을 바로 출력하지 않는다.
  • 대신 displayName 함수를 리턴값으로 myFunc에 저장 후 myFunc를 실행한다.
  • 마찬가지로 displayName 함수 컨텍스트에는 name 변수가 없기 때문에 현재 함수 위치에서 외부 함수에 접근하고 name 값을 찾아 출력한다.

🚨 잠깐! 어떻게 '이미 리턴된' 함수와 그 지역변수에 접근 할 수 있는거지?

몇몇 프로그래밍 언어에서, 함수 안의 지역 변수 들은 그 함수가 처리되는 동안에만 존재한다.
따라서 makeFunc() 실행이 끝나면(displayName함수가 리턴되고 나면) name 변수에 더 이상 접근할 수 없다고 예상하는 것이 일반적이다.

그러나 위의 코드에선 이미 호출이 끝난 함수의 실행 컨텍스트에 접근해서
name 변수값을 받아와 출력할 수 있었다. 이게 어떻게 가능한 것일까?


📌 closure

자바스크립트는 함수를 리턴하고, 리턴하는 함수가 클로저를 형성한다.
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 이 환경은 클로저가 생성된 시점유효 범위 내에 있는 모든 지역 변수로 구성된다.

조금 더 쉽게 설명해보자면

Closure란 자신이 생성된 시점의 환경을 기억하는 함수 즉,
Closure는 자신의 외부 함수 호출이 종료되었음에도 그 외부함수의 변수 or 인자에 접근 할 수 있는 함수이다.

다시 예제를 살펴보자

function makeFunc() {
	var name = "Mozilla";
    function displayName() {
    	console.log(name); // Mozilla
    }
    return displayName;
}

var myFunc = makeFunc();

myFunc();
  • makeFunc가 리턴되면서 클로저가 생성되고 displayName이 생성될때의 위치를 기준으로 함수와 그 지역변수를 기억하게 된다.
  • 이 클로저를 myFunc가 참조할 수 있게 되고
  • myFunc 함수가 호출되는 순간 클로저에서 name 변수를 찾을 때까지 scope chain으로 상위 실행 컨텍스트를 살핀다.
  • makeFunc 함수의 지역변수인 name을 발견해 Mozilla를 출력한다.

📌 private 흉내내기

자바스크립트는 클로저를 이용하여 프라이빗 메소드를 흉내내는 것이 가능하다. 프라이빗 메소드는 코드에 제한적인 접근만을 허용한다는 점 뿐만 아니라 전역 네임 스페이스를 관리하는 강력한 방법을 제공하여 불필요한 메소드가 공용 인터페이스를 혼란스럽게 만들지 않도록 한다.

var counter = (function() {
	var privateCounter = 0;
    function changeBy(val) {
    	privateCounter += val;
    }
	return {
		increment: function() {
			changeBy(1);
		},
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
   };
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1
  • counter 변수에는 익명 함수가 즉시 실행되어 리턴값이 저장된다.
  • 같은 어휘적(lexical) 환경을 공유하는 세개의 클로저가 생성된다.
  • 이 어휘적 환경은 익명함수 안에서 만들어진 privateCounter 변수, changeBy 함수를 포함한다.
  • 세개의 메서드에 의해서만 두 값이 조작될 수 있다.

✨ 클로저 역할?
세개의 메서드는 클로저 환경의 값만 조작하기 때문에 공용 공간을 더럽히지 않고 private 변수나 함수에 외부에서 직접적으로 접근하여 조작할 수 없어 값을 보존할 수도 있다.

값이 보존되는 또다른 예제를 살펴보자

var makeCounter = function() {
	var privateCounter = 0;
    function changeBy(val) {
    	privateCounter += val;
    }
    return {
    	increment: function() {
        	changeBy(1);
     	},
        decrement: function() {
        	changeBy(-1);
        },
        value: function() {
        	return privateCounter;
        }
	}
};

var counter1 = makeCounter();
var counter2 = makeCounter();

counter1.increment();
counter1.increment();

console.log(counter1.value()); /* 2 */
console.log(counter2.value()); /* 0 */
  • 아까전의 코드와 다르게 익명함수를 즉시호출하지 않고 변수에 저장했다.
  • 변수를 호출해서 counter1, counter2 등 여러 버전을 만들 수 있다.

✨ 독립성 유지 방법?
각 클로저는 그들 고유의 클로저에서 privateCounter 변수의 다른 버전을 참조한다.
따라서 각 카운터가 호출될 때마다 하나의 클로저에서 변수 값을 변경해도 다른 클로저의 값에는 영향을 주지 않는다.

이런 방식으로 클로저를 사용하여 객체지향 프로그래밍의 정보 은닉과 캡슐화 같은 이점들을 얻을 수 있다.


📌 실용적 클로저

function makeSizer(size) {
	return function() {
   		document.body.style.fontSize = size + 'px';
    };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
  • 세개의 사이즈 변수에는 각자 다른 사이즈 값을 기억하는 클로저가 할당된다.
  • 이 함수는 실행되지 않고 있다가 dom에서 onclick 이벤트를 감지하면 실행된다.

✨ 비동기 작동에도 사용해
이처럼 자신만의 고유한 환경을 참조하면 다른 환경에서 값이 변해도 영향을 받지 않고 인자로 받은 값을 기억하고 있기 때문에 나중에 호출 되더라도
그 값을 유지한 채 작동된다.


출처:

https://medium.com/sjk5766/lexical-scope-closure-%EC%A0%95%EB%A6%AC-41f5d1c928e4 - 클로저

https://imki123.github.io/posts/33/ - 실행 컨텍스트

https://elvanov.com/2597 - 비동기

https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures - 클로저

profile
길을 찾고 싶은 코린이 of 코린이

0개의 댓글