사전스터디 Q&A Week 3 세션에서 멘토 경현님이 언급하신 Scope Chain
, Lexical scope
, Closure
에 대해서 간단히 알아보려고 한다. 참고한 자료는 <코어 자바스크립트>, <모던 자바스크립트 Deep Dive>이다.
Scope Chain
scpoe
스코프(scope)
: 식별자에 대한 유효범위
var
로 선언한 변수에 대해서는 작용하지 않고, let
과 const
, class
, strict mode
에서의 함수 선언 등에 대해서만 범위로서의 역할을 수행한다. 스코프 체인(scope chain)
이라고 한다. scope chain
스코프 체인(scope chain)
: 스코프가 계층적으로 연결된 것.(출처: https://dasha.ai/en-us/blog/javascript-scope-and-scope-chain)
위 gif를 보면 최상위 스고프인 전역 스코프(Global scope
), 전역에서 선언된 calcAge
함수의 지역 스코프, calcAge
함수 내부에서 선언된 yesrsToRetire
함수의 지역 스코프와 if block
지역 스코프로 이루어져있다. 이렇게 스코프는 계층적 구조로 연결되어 있으며 이를 스코프 체인
이라고 한다.
변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색(identifier resolutio)한다. 절대 하위 스코프로 내려가면서 식별자를 검색하는 일은 없다. 이는 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조할 수 있지만, 하위 스코프에서 유효한 변수를 상위 스코프에서 참조할 수 없다는 것을 의미한다.
스코프 체인으로 연결된 스코프의 계층적 구조는 부자 관계로 이뤄진 상속과 유사하다. 상속을 통해 부모의 자산을 자신이 자유롭게 사용할 수 있지만 자식의 자산을 부모가 사용할 수는 없다. 스코프 체인도 마찬가지 개념이다.
Lexical scope
함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정한다.
자바스크립트는 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정(동적 스코프)하는 것이 아니라 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정(렉시컬 스코프, 정적 스코프)한다.
자바스크립트는 렉시컬 스코프를 따르며, 함수가 호출된 위치는 상위 스코프 결정에 어떠한 영향도 주지 않는다. 즉, 함수의 상위 스코프는 언제나 자신이 정의된 스코프이다.
함수의 상위 스코프는 함수 정의가 실행될 때 정적으로 결정된다. 함수 정의(함수 선언문 또는 함수 표현식)가 실행되어 생성된 함수 객체는 이렇게 결정된 상위 스코프를 기억한다. 함수가 호출될 때마다 함수의 상위 스코프를 참조할 필요가 있기 때문이다.
// example
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo();
bar();
위 예제의 foo
함수와 bar
함수는 모두 전역에서 정의된 전역 함수다. 함수의 상위 스코프는 함수를 어디서 정의했느냐에 따라 결정되므로 foo
함수와 bar
함수의 상위 스코프는 전역이다. 함수를 어디서 호출하는지는 함수의 상위 스코프 결정에 어떠한 영향도 주지 못한다. 즉, 함수의 상위 스코프는 함수를 정의한 위치에 의해 정적으로 결정되고 변하지 않는다.
bar
함수가 호출되면 호출된 곳이 어디인지 관계없이 언제나 자신이 기억하고 있는 전역 스코프를 상위 스코프로 사용하여 위 예제를 실행할 경우 전역변수 x
의 값 1을 두 번 출력한다.
렉시컬 스코프는 클로저와 깊은 관계가 있으며, 바로 클로저에 대해 알아보도록 하자.
Closure
다양한 서적에서 클로저를 한 문장으로 요약해서 설명하는 부분들을 소개하면 다음과 같다.
- 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수 - 자바스크립트 닌자 비급
- 이미 생명 주기상 끝난 외부 함수의 변수를 참조하는 함수 - 인사이드 자바스크립트
- 자신이 생성될 떄의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수 - 함수형 자바스크립트 프로그래밍
// 클로저 사용 X
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
//이전 상태를 유지하지 못한다.
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
// 클로저 사용 O
// 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
위 코드에서 클로저가 사용되지 않은 예제는 increase
함수가 호출될 때마다 지역변수 num
은 다시 선언되고 0으로 초기화되기 때문에 출력 결과는 언제나 1이다. 다시 말해, 상태가 변경되기 이전 상태를 유지하지 못한다.
반면, 클로저가 사용된 예제를 보면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환한 함수가 increase
변수에 할당된다. increase
변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저다.
즉시 실행 함수는 호출된 이후 소멸되지만 즉시 실행 함수가 반환한 클로저는 increase
변수에 할당되어 호출된다. 이때 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하고 있다. 따라서 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 자유 변수 num
을 언제 어디서 호출하든지 참조하고 변경할 수 있다.
즉시 실행 함수는 한 번만 실행되므로 increase
가 호출될 때마다 num
변수가 재차 초기화될 일은 없을 것이다. 또한 num
변수는 외부에서 직접 접근할 수 없는 은닉된 private
변수이므로 전역 변수를 사용했을 떄와 같이 의도되지 않은 변경을 걱정할 필요도 없기 때문에 더 안정적인 프로그래밍이 가능하다.
정리하자면, 클로저
란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 말한다.
클로저를 활용하는 방안은 다음 기회에 정리하도록 하겠다!
참고 자료