Javascript - Scope, Closure
클로저를 배우기 전에 스코프에 대한 개념부터 알아야 한다.
Scope
스코프는 사전적 의미로 ‘범위’를 뜻한다.
JavaScript로 함수 작성 시, 중괄호 {}를 이용하여 함수의 범위를 작성한다.
즉 여기서 말하는 스코프는 ‘코드가 영향을 미치는 범위’, ‘변수의 유효 범위’ 등으로 정의할 수 있다.
1. 스코프의 개념
- 선언된 변수에 대해서 접근할 수 있는 유효한 범위를 의미한다. 만약, 변수가 해당 스코프에 존재하지 않다면 사용할 수 없다.
- 계층적인 구조를 가지기 때문에 하위 스코프는 상위 스코프에 접근할 수 있지만, 상위 스코프는 하위 스코프에 접근할 수 없다.
2. 스코프의 동작에 따른 구분
스코프의 동작 별 구분은 스코프를 결정하는 방식에 따라서, 정적 스코프(혹은 렉시컬 스코프), 동적 스코프로 나뉜다.
JavaScript의 경우 정적 스코프(렉시컬 스코프) 방식을 채택하고 있다.
2-1. 정적 스코프(Static Scope) 혹은 렉시컬 스코프(Lexical Scope)
정적 스코프라 불리는 이유는 스코프가 컴파일 타임에 결정되고 변하지 않기 때문이다.
(반면 동적 스코프의 경우 런타임에 스코핑을 지원하는 방식이다)
정적 스코프 외에도 렉시컬 스코프라고 부르기도 하는데 Lexical은 사전적 의미로 ‘(한 언어의) 어휘의, 어휘적인’이라는 형용사이다. 이와 관련하여 Lexicon은 명사로 ‘(특정 언어·분야·개인·집단에서 사용하는 모든) 어휘, 어휘 목록’ 등을 의미 한다. 따라서 Lexical Scope는 ‘특정 구역 내에서 사용하는 스코프’로 이해하면 되겠다.
함수를 ‘선언한 시점’에 스코프를 결정하는 방식을 의미한다.
따라서 함수의 유효 범위는 어디서 실행되었느냐가 아닌, ‘어디서 선언’되었느냐에 따라 달라진다.
함수가 중첩되어 있을 때, 내부 함수(자식 함수)에서 해당 변수가 존재하지 않을 경우 상위 스코프(부모 함수)에 접근하여 해당 변수를 찾는 방식을 의미한다.
▼ 정적 스코프 예시 코드
- 정적 스코프는 함수 선언 시점에 스코프가 결정된다고 했다. 따라서 아래와 같은 경우 name 변수에 접근할 수 없으므로 undefined가 출력된다.
function name() {
var name = 'Janet';
}
function getName() {
console.log(name);
}
getName(); // undefined
2-2. 동적 스코프(Dynamic Scope)
함수의 스코프를 결정하는 방식 중 동적 방식은 함수를 ‘호출한 시점’에 스코프를 결정하는 방식을 의미한다.
따라서 함수를 ‘어디에서 호출’했느냐에 따라 접근할 수 있는 유효 범위가 달라진다.
동적 스코프의 선언은 런타임 도중에 실행 컨텍스트나 호출 컨텍스트에 의해 결정된다.
▼ 동적 스코프 예시 코드
var name = 'Tom'
function myName() {
var name = 'Janet';
getName();
}
function getName() {
console.log(name);
}
myName(); // Janet
▼ 위와 동일 코드로 정적 스코프인 경우
var name = 'Tom'
function myName() {
var name = 'Janet';
getName();
}
function getName() {
console.log(name);
}
myName(); // Tom
3. 스코프의 레벨에 따른 구분
3-1. 전역 스코프(Global Scope)
전역 스코프는 전역으로 선언된 변수에 대해 접근과 조작이 가능한 유효한 범위를 의미한다.
자바스크립트에서 만일 변수가 어떠한 함수에도 속하지 않은 상태이고, 어떠한 중괄호 안에 들어있지 않은 상태라면 그 변수를 전역 변수(Global variable)라 부른다.
let global = 'Global!'; // 전역 변수
function scope() {
let local = 'Local!'; // 지역 변수
console.log(global);
};
scope(); // Global!
console.log(local); // local is not defined
3-2. 지역 스코프(Local Scope)
지역 스코프는 변수가 함수 혹은 블록 내에서 접근(호출)이 가능한 유효한 범위를 의미한다.
함수 내에서 유효한 경우의 함수 레벨 스코프와 블록 내에서 유효한 경우의 블록 레벨 스코프를 의미한다.
3-2-1. 함수 레벨 스코프(Function Level Scope)
함수 스코프는 선언한 변수가 함수 내에서 유효한 범위를 의미한다.
- var로 선언된 변수, 함수들은 함수 레벨 스코프가 된다.
function hi(name) {
if (name) {
var greeting = 'Hello ' + name;
}
console.log(greeting);
}
hi('Janet'); // Hello Janet
3-2-2. 블록 레벨 스코프(Block Level Scope)
블록 스코프는 선언한 변수가 블록(중괄호 내)에서 유효한 범위를 의미한다.
- ES6부터 지원하기 시작한 let 과 const 로 선언된 변수, 함수들은 블록 레벨 스코프가 된다.
참고로, 함수 레벨 스코프는 블록 레벨 스코프보다 더 넓은 범위를 가지므로 편리하기도 하지만 반대로 코드에 대한 복잡성을 증가시키는 요인이 된다. - 아래 예시 코드는 블록 내부에서 생성된 지역 변수(greeting)를 외부에서 참조 할 수 없어 에러가 발생한 경우이다.
function hi(name) {
if (name) {
let greeting = 'Hello ' + name;
}
console.log(greeting);
}
hi('Janet'); // greeting is not defined
4. 스코프 체인(Scope Chain)
스코프 체인은 현재 스코프 레벨에서 참조값이 없는 경우 상위 레벨의 스코프에서 참조값을 찾아 나가는 현상을 의미한다.
이 찾는 과정은 스코프를 안에서 바깥쪽으로 단계적으로 탐색하는 과정이다. (내부 -> 외부 -> 전체)
만약 전역 스코프에도 참조값이 없다면 null을 반환하게 된다.
5. Scope 요약 정리
- 동작별 스코프: 정적 스코프, 동적 스코프 (자바스크립트의 경우 정적 스코프의 특징을 가짐)
- 레벨별 스코프: 전역 스코프, 지역 스코프(함수 레벨 스코프: var, 블록 레벨 스코프: let, const)
- 스코프 체인: 현재 스코프 레벨에서 참조값이 없는 경우 상위 레벨의 스코프에서 참조값을 찾아 나가는 현상을 의미
Closure
- 클로저는 ‘함수’를 지칭하고 또 ‘그 함수가 선언된 환경과의 관계’의 개념이다.
- 클로저는 ‘자신이 선언될 당시의 환경을 기억하는 함수’이다.
- 클로저는 ‘생명 주기가 끝난 외부 함수의 변수에 접근할 수 있는 내부 함수’를 말한다.
클로저란 함수와 함수가 선언된 어휘적 환경(lexical environment)의 조합이다.
클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
Closure의 특징
Closure의 사전적 의미는 폐쇄이다. 클로저의 핵심은 스코프를 이용하여 변수의 접근 범위를 ‘폐쇄’하는 것에 있다.
- 외부함수 스코프에서 내부함수 스코프로 접근 불가능하다.
- 내부함수에서는 외부함수 스코프에서 선언된 변수에 접근 기능하다. 따라서 내부 함수는 외부함수에 선언된 변수에 접근 가능하다.
외부함수의 실행이 종료된 후에도, 클로저 함수는 외부함수의 스코프, 즉, 함수가 선언된 어휘적 환경에 접근할 수 있다.
- 외부 함수 스코프가 내부함수에 의해 언제든지 참조될 수 있다. 따라서 클로저를 남발할 경우 퍼포먼스 저하가 발생할 수도 있다.
상위 스코프의 식별자를 포함하여 쓰여있는 내부 함수 코드 자체를 어휘적 환경(lexical environment)라고 부를 수 있다.
- 함수가 호출되는 환경과 별개로, 기존에 선언되어 있던 환경(어휘적 환경)을 기준으로 변수를 조회한다.
Closure의 장점
- 전역변수 사용의 최소화: 전역변수가 많으면 의도치 않게 어디에서든 접근하는 상황이 발생할 수 있다. 클로저를 이용하여 전역변수를 최소한으로 사용함으로써 이러한 실수나 예외적인 상황을 방지할 수 있다.
- 데이터 보존 가능: 클로저 함수는 외부 함수의 실행이 끝나더라도 외부 함수 내 변수를 사용할 수 있다. 따라서 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게하는 폐쇄성을 갖는다.
- 모듈화를 통한 코드 재사용에 편리: 클로저 함수를 각각의 변수에 할당하면 각자 독립적으로 값을 사용하고 보존 가능하다. 이와 같이 함수의 재사용성을 극대화하고 함수 하나를 독립적인 부품의 형태로 분리하는 것을 모듈화라고 한다. 클로저를 통해 데이터와 메소드를 묶어다닐 수 있기에 클로저는 모듈화에 유리하다.
- 정보의 접근 제한 (캡슐화): 클로저 모듈 패턴을 사용해 객체에 담아 여러 개의 함수를 리턴하도록 만들어 정보의 접근을 제한할 수 있는데, 이를 캡슐화라고 한다.
Closure의 단점
메모리 사용량 증가
- 외부 함수의 변수를 계속 참조하는데, 이러한 변수들은 가비지 컬렉션되지 않음
가비지 컬렉션이란? - 동적 할당된 메모리 영역 가운데 더 이상 사용할 수 없게 된 영역을 탐지하여 자동으로 해제하는 기법
Closure 예시코드
중첩함수의 경우의 클로저
function outer() {
let message = 'Hello! ';
return function inner(name) { // inner함수는 클로저
return message = message + name;
}
}
let greeting = outer(); // 외부함수 호출. 변수 greeting은 inner 함수를 참조
console.log(greeting('Janet')); // Hello! Janet
- outer 함수는 종료됐지만, outer 함수 내부 변수인 message는 inner함수를 통해 접근 가능하다. 여기서의 inner함수가 바로 클로저이다.
- 이는 클로저의 특성으로 inner함수가 선언될 때 그 주변의 lexical environment(어휘적 환경) 즉, outer 함수의 lexical environment와 함께 묶였기 때문이다. 따라서 message라는 변수를 사용할 수 있게 된다.