JavaScript는 단순한 언어로 여겨져 왔습니다. 그래서 여러 개발자분들이 JavaScript를 배우기도 쉽고 간단히 쓸 수 있다는 편견을 가지고있습니다. 하지만, 최근 JavaScript의 관심이 늘어나면서 JavaScript는 더이상 '쉬운 언어'가 아닌 깊은 이해를 필요로 하는 언어라는 인식이 생기고있습니다. 저는 JavaScript에 대한 깊은 이해를 하기 위해서는 클로저(Closure)에 대해 알아야 되며 이를 알기 위해서는 Scope 개념의이해가 필요하다고 생각됩니다.
JavaScript 프로그래밍에서 유효범위를 잘 알아야 하는 이유가 무엇일까요? 제 생각은 다음과 같습니다.
유효범위란 JavaScript에서뿐만 아니라 모든 프로그래밍 언어 코드의 가장 기본적인 개념의 하나로 반드시 알아야 합니다. 유효범위의 개념을 모르면 관련된 다른 개념 역시 혼란스러울 수 있습니다.
JavaScript의 유효범위에는 다른 언어의 유효범위와는 다릅니다. 다른 프로그래밍 언어에 익숙한 개발자들은 JavaScript만의 유효범위를 이해해야 합니다.
JavaScript의 유효범위 개념은 간단하게 생각한다면 너무나 쉬운 내용이면서도 쉽게 이해하지 못할 함정에 자주 빠지게 합니다. 돌다리도 두들겨보고 건너라는 말이 있듯이 기본 개념부터 튼튼히 하고 넘어가야 합니다.
1. 유효범위(Scope)
Scope를 직역하면 영역, 범위라는 뜻입니다. 하지만 프로그램 언어에서의 유효범위는 어느 범위까지 참조하는지. 즉, 변수와 매개변수(parameter)의 접근성과 생존기간을 뜻합니다. 따라서 유효범위 개념을 잘 알고 있다면 변수와 매개변수의 접근성과 생존기간을 제어할 수 있습니다. 유효범위의 종류는 크게 두 가지가 있습니다. 하나는 전역 유효범위(Global Scope), 또 하나는 지역 유효범위(Local Scope)입니다. 전역 유효범위는 스크립트 전체에서 참조되는 것을 의미하는데, 말 그대로 스크립트 내 어느 곳에서든 참조됩니다. 지역 유효범위는 정의된 함 수 안에서만 참조되는 것을 의미하며, 함수 밖에서는 참조하지 못합니다.
위 그림은 유효범위의 종류에 대해 좀 더 설명하기 위해 첨부했습니다. 위 그림에서 전역변수(전역 유효범위를 가진 변수)는 global_scope이고 지역변수(지역 유효범위를 가진 변수)는 a_scope_param, local_scope_a, local_scope_b, local_scope_c입니다. 각각의 지역 변수의 유효범위는 a_cope_param, local_scope_a가 함수(Function) A의 중괄호 안의 영역, local_scope_b가 함수 B의 중괄호 안의 영역, local_scope_c가 함수 C의 중괄호 안의 영역입니다.
1.1 JavaScript 유효범위의 특징
앞서 JavaScript의 유효범위를 알아야 하는 이유에서 언급했듯 JavaScript의 유효범위는 다른 프로그래밍언어와 다른 개념을 갖습니다. JavaScript 유효범위만의 특징을 크게 분류하여 나열하면 다음과 같습니다.
함수 단위의 유효범위
변수명 중복 허용
var 키워드의 생략
렉시컬 특성
위와 같은 특징들을 지금부터 하나씩 살펴보겠습니다.
function scopeTest() {
var a = 0;
if (true) {
var b = 0;
for (var c = 0; c < 5; c++) {
console.log("c=" + c);
}
console.log("c=" + c);
}
console.log("b=" + b);
}
scopeTest();
//실행결과
/*
c = 0
c = 1
c = 2
c = 3
c = 4
c = 5
b = 0
*/
[예제 1] 유효범위 설정단위
위의 코드는 JavaScript의 유효범위 단위가 블록 단위가 아닌 함수 단위로 정의된다는 것을 설명하기 위한 예제 코드입니다. 다른 프로그래밍 언어들은 유효범위의 단위가 블록 단위이기 때문에 위의 코드와 같은 if문, for문 등 구문들이 사용되었을 때 중괄호 밖의 범위에서는 그 안의 변수를 사용할 수 없습니다. 하지만 JavaScript의 유효범위는 함수 단위이기 때문에 예제코드의 변수 a,b,c모두 같은 유효범위를 갖습니다. 그 결과, 실행화면을 보면 알 수 있듯이 구문 밖에서 그 변수를 참조합니다.
var scope = 10;
function scopeExam(){
var scope = 20;
console.log("scope = " +scope);
}
scopeExam();
//실행결과
/*
scope =20
*/
[예제 2] 변수 명 중복
JavaScript는 다른 프로그래밍 언어와는 달리 변수명이 중복되어도 에러가 나지 않습니다. 단, 같은 변수명이 여러 개 있는 변수를 참조할 때 가장 가까운 범위의 변수를 참조합니다. 위의 코드 실행화면을 보면 함수 내에서 scope를 호출했을 때 전역 변수 scope를 참조하는 것이 아니라 같은 함수 내에 있는 지역변수 scope를 참조합니다.
다른 프로그래밍 언어의 경우 변수를 선언할 때 int나 char와 같은 변수 형을 썼지만, JavaScript는 var 키워드를 사용합니다. 또, 다른 프로그래밍 언어의 경우 변수를 선언할 때 변수형을 쓰지 않을 경우 에러가 나지만 JavaScript는 var 키워드가 생략이 가능합니다. 단, var 키워드를 빼먹고 변수를 선언할 경우 전역 변수로 선언됩니다. 위 코드의 실행 결과를 보면 scope라는 변수가 함수 scopeExam 안에서 변수 선언이 이루어졌지만, var 키워드가 생략된 상태로 선언되어 함수 scopeExam2에서 호출을 했을 때도 참조합니다.
```
function f1(){
var a= 10;
f2();
}
function f2(){
return console.log("호출 실행");
}
f1();
function f1(){
var a= 10;
f2();
}
function f2(){
return a;
}
f1();
//실행결과
/*
Uncaught Reference Error
: a is not defined
*/
</td>
</tr>
</tbody>
</table>
*[예제 4] 렉시컬 특성*
렉시컬 특성이란 함수 실행 시 유효범위를 함수 실행 환경이 아닌 함수 정의 환경으로 참조하는 특성입니다. 위의 좌측코드를 봤을 때 함수 f1에서 함수 f2를 호출하면 실행이 됩니다. 함수 f1,f2 모두 전역에서 생성된 함수여서 서로를 참조할 수 있죠. 하지만 우측코드처럼 함수 f1안에서 함수 f2를 호출했다고 해서 f2가 f1안에 들어온 것처럼 f1의 내부 변수 a를 참조할 수 없습니다. 렉시컬 특성으로 인해서 함수 f2가 실행될 때가 아닌 정의 될 때의 환경을 보기 때문에 참조하는 a라는 변수를 찾을 수 없습니다. 그래서 실행결과는 위와 같이 나옵니다. 또 다른 JavaScript의 특징 중에 하나로 호이스팅이라는 개념이 있습니다. 호이스팅에 대해 살펴 보겠습니다.
###1.2 호이스팅(Hoisting)
호이스팅이란 무엇일까요? Hoisting이라는 단어를 직역하면 끌어올리기, 들어 올려 나르기라는 뜻입니다. JavaScript에서 호이스팅도 비슷한 의미를 갖고 있습니다. 간단하게 말해서 JavaScript에서의 호이스팅의 의미는 변수 선언문을 끌어올린다는 뜻으로 이해하면 됩니다. 좀 더 이해를 돕기위해 아래의 코드를 준비했습니다.
<table border="0">
<col width="50*" />
<col width="50%" />
<tbody>
<tr>
<td style="background-color:#fff;border:none;">
function hoistingExam(){
console.log("value="+value);
var value =10;
console.log("value="+value);
}
hoistingExam();
function hoistingExam(){
var value;
console.log("value="+value);
value =10;
console.log("value="+value);
}
hoistingExam();
//실행결과
/*
value= undefined
value= 10
*/
</td>
</tr>
</tbody>
</table>
*[예제 5] 호이스팅*
위의 코드는 호이스팅을 설명하기 위한 간단한 예제입니다. 좌측 코드를 보시게 되면 함수 hoistingExam안에서 변수 value의 호출이 두 번 일어납니다. 한 번은 변수 선언문 전에 또 한 번은 변수 선언 후에 호출이 되는데, 다른 프로그래밍 언어의 경우에는 선언문 전에 호출됐을 때 에러가 납니다. 하지만 JavaScript의 경우 호이스팅이 됨으로써 오른쪽 코드와 같은 구동이 이루어집니다. 즉, 변수 선언문이 유효범위 안의 제일 상단부로 끌어올려 지게 되고, 선언문이 있던 자리에서 초기화가 이루어지는 결과를 갖는 것입니다. 그 실행결과 첫 번째 호출에서는 초기화가 되지 않은 undefined가, 두 번째 호출에서는 초기화된 값이 나옵니다.
var value=30;
function hoistingExam(){
console.log("value="+value);
var value =10;
console.log("value="+value);
}
hoistingExam();
//실행결과
/*
value= undefined
value= 10
*/
*[예제 6] 호이스팅 2*
그렇다면 위와 같은 코드에서는 어떤 결과가 나올까요? 다른 프로그래밍 언어에 익숙한 개발자 분들은 변수 value의 첫 호출에서 전역변수가 참조된다고 생각하실 수 있습니다. 하지만 JavaScript의 호이스팅으로 인해서 선언 부가 함수 hoistingExam의 최 상단에서 끌어올려 짐으로써 전역변수가 아닌 지역변수를 참조합니다.
함수의 호이스팅을 이해할 때는 한 가지만 기억하시면 될 것 같습니다. 바로, 여러 가지의 함수 정의 방법 중 ‘함수 선언문 방식만 호이스팅이 가능하다.’라는 점입니다.
// 함수 선언문
hoistingExam();
function hoistingExam(){
var hoisting_val =10;
console.log("hoisting_val ="+hoisting_val);
}
//실행결과
/*
hoisting_val =10
*/
//함수 표현식
hoistingExam2();
var hoistingExam2 = function(){
var hoisting_val =10;
console.log("hoisting_val ="+hoisting_val);
}
//실행결과
/*
hoistingExam2 of object is not a function
*/
//Function 생성자
hoistingExam3();
var hoistingExam3 = new Function("","return console.log('Ya-ho!!');");
//실행결과
/*
hoistingExam3 of object is not a function
*/
*[예제 7] 함수 호이스팅*
앞서 말하였듯 위의 코드와 실행결과를 보시면 함수 선언문 방식만 호이스팅이 제대로 이루어집니다. 이 결과를 보고 왜 함수 선언문 방식만 호이스팅이 되고 함수 표현 식과 Function생성자를 통해 함수를 정의하는 방법은 호이스팅이 되지 않는지 궁금해하시는 분들도 계실 것 같은데요. 그 이유는 함수 표현 식과 Function생성자를 통한 함수 정의 방법은 변수에 함수를 초기화시키기 때문에(이를 함수변수라고도 합니다) 선언문이 호이스팅이 되어 상단으로 올려진다 하더라도 함수가 아닌 변수로써 인지되기 때문입니다. 위의 코드에서 함수실행 구문이 아닌 변수를 호출하게 되면 변수의 호이스팅과 같은 undefined란 결과가 나옵니다.
###1.3 실행 문맥(Execution context)
실행 문맥은 간단하게 말해서 실행 정보입니다. 실행에 필요한 여러 가지 정보들을 담고 있는데 정보란 대부분 함수를 뜻합니다. JavaScript는 일종의 콜 스택(Call Stack)을 갖고 있는데, 이 곳에 실행 문맥이 쌓입니다. 콜 스택의 제일 위에 위치하는 실행 문맥이 현재 실행되고 있는 실행 문맥이 되는 것이죠.
console.log("전역 컨텍스트 입니다");
function Func1(){
console.log("첫 번째 함수입니다.");
};
function Func2(){
Func1();
console.log("두 번째 함수입니다.");
};
Func2();
//실행결과
/*
전역 컨텍스트 입니다
첫 번째 함수 입니다.
두 번째 함수 입니다
*/
<figure>
<img src="/content/images/2021/01/jsseo-140320-Scope-03-1.png" alt="">
<figcaption>[그림 2] 코드실행에 따른 실행문맥 스택</figcaption>
</figure>
스크립트가 실행이 되면 콜 스택에 전역 컨텍스트가 쌓입니다. 위의 코드에서 함수 Func2의 실행 문구가 나와 함수가 실행이 되면 그 위에 Func2의 실행 컨텍스트가 쌓입니다. Func2가 실행되는 도중 함수 Func1이 실행이 되면서 콜 스택에는 Func2 실행 컨텍스트위에 Func1의 실행컨텍스트가 쌓이죠. 그렇게 Func1이 종료가되고 Func2가 종료가 되면서 차례로 컨텍스트들이 스택에서 빠져나오게됩니다. 마지막으로 스크립트가 종료가 되면 전역 컨텍스트가 빠져나오게 되는 구조입니다.
그렇다면 실행 문맥은 어떤 구조로 이루어져있고 어떤 과정을 통해 생성이 될까요? 지금부터 알아보겠습니다.
###1.4 실행 문맥 생성
실행 문맥은 크게 3가지로 이루어져 있습니다.
- 활성화 객체: 실행에 필요한 여러 가지 정보들을 담을 객체입니다. 여러 가지 정보란 arguments객체와 변수등을 말합니다.
- 유효범위 정보: 현재 실행 문맥의 유효 범위를 나타냅니다.
- this 객체: 현재 실행 문맥을 포함하는 객체 입니다.
<figure>
<img src="/content/images/2021/01/jsseo-140320-Scope-04-1024x378.png" alt="">
<figcaption>[그림 3] 실행문맥 생성</figcaption>
</figure>
위의 코드를 실행하게 되면 함수abcFunction의 실행 문구에서 위와 같은 실행 문맥이 생깁니다. 실행문맥 생성 순서는 다음과 같습니다.
1. 활성화 객체 생성
2. arguments객체 생성
3. 유효범위 정보 생성
4. 변수 생성
5. this객체 바인딩
6. 실행
arguments객체는 함수가 실행될 때 들어오는 매개변수들을 모아놓은 유사 배열 객체입니다. 위의 그림에서 Scope Chain이 유효범위 정보를 담는 일종의 리스트이며 0번지는 전역 변수 객체를 참조합니다. Scope Chain에 대해서는 뒤에 다시 한 번 설명하겠습니다. 변수들은 위의 코드의 지역변수와 매개변수 a,b,c 입니다. 매개변수 a와 b는 실행 문맥 생성단계에서 초기화 값이 들어가지만, c의 경우 생성 후 실행 단계에서 초기화가 되기 때문에 undefined란 값을 가지고 생성됩니다.
#2. 유효범위 체인(Scope Chain)
유효범위 체인을 간단하게 설명하면 함수가 중첩함수일 때 상위함수의 유효범위까지 흡수하는 것을 말합니다. 즉, 하위함수가 실행되는 동안 참조하는 상위 함수의 변수 또는 함수의 메모리를 참조하는 것입니다. 앞서 실행 문맥 생성에 대해 설명했듯이 함수가 실행될 때 유효범위를 생성하고 해당 함수를 호출한 부모 함수가 가진 활성화 객체가 리스트에 추가됩니다.
<figure>
<img src="/content/images/2021/01/jsseo-140320-Scope-09-1024x480.png" alt="">
<figcaption>[그림 4] 유효범위 체인 관계형성</figcaption>
</figure>
쉽게 말해서 위와 같은 코드를 실행할 때 전역 변수 객체, 상 하위 객체 간에 부모/자식 관계가 형성된다고 생각하시면 쉽게 이해 할 수 있습니다. 위의 코드를 실행 문맥 개념으로 좀 더 자세히 보면 다음과 같은 구조를 가집니다.
<figure>
<img src="/content/images/2021/01/jsseo-140320-Scope-08-1.png" alt="">
<figcaption>[그림 5] 유효범위 체인</figcaption>
</figure>
(앞으로 활성화 객체는 변수 객체와 같기 때문에 변수 객체라고 부르겠습니다) 함수 outerFunction이 실행 되면 outerFunction의 실행 문맥이 생성이 되고 그 과정은 앞선 실행 문맥 생성과정과 동일합니다. outerFunction이 실행이 되면서 내부 함수 innerFunction이 실행되면 innerFunction실행 문맥이 생성이 되는데 유효범위 정보가 생성이 되면서 outerFuction과는 조금 차이가있는 유효범위 체인 리스트가 생깁니다. innerFunction 실행문맥의 유효범위 체인 리스트는 1번지에 상위 함수인 outerFunction의 변수 객체를 참조합니다. 만약 innerFunction내부에 새로운 내부 함수가 생기게 되면 그 내부함수의 유효범위 체인의 1번지는 outerFunction의 변수 객체를, 2번지는 innerFunction의 변수 객체를 참조합니다.
이어서 이 유효범위 체인을 이용한 클로저에 대해 알아 봅니다.
##3. 클로저(Closure)
클로저는 JavaScript의 유효범위 체인을 이용하여 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 방법입니다. 외부 함수가 종료되더라도 내부함수가 실행되는 상태면 내부함수에서 참조하는 외부함수는 닫히지 못하고 내부함수에 의해서 닫히게 되어 클로저라 불리 웁니다. 따라서 클로저란 외부에서 내부 변수에 접근할 수 있도록 하는 함수입니다.
내부 변수는 하나의 클로저에만 종속될 필요는 없으며 외부 함수가 실행 될 때마다 새로운 유효범위 체인과 새로운 내부 변수를 생성합니다. 또, 클로저가 참조하는 내부 변수는 실제 내부 변수의 복사본이 아닌 그 내부 변수를 직접 참조합니다.
<table border="0">
<col width="50%" />
<col width="50%" />
<tbody>
<tr>
<td style="background-color:#fff;border:none;">
function outerFunc(){
var a= 0;
return {
innerFunc1 : function(){
a+=1;
console.log("a :"+a);
},
innerFunc2 : function(){
a+=2;
console.log("a :"+a);
}
};
}
var out = outerFunc();
out.innerFunc1();
out.innerFunc2();
out.innerFunc2();
out.innerFunc1();
function outerFunc(){
var a= 0;
return {
innerFunc1 : function(){
a+=1;
console.log("a :"+a);
},
innerFunc2 : function(){
a+=2;
console.log("a :"+a);
}
};
}
var out = outerFunc();
var out2 = outerFunc();
out.innerFunc1();
out.innerFunc2();
out2.innerFunc1();
out2.innerFunc2();
//실행결과
/*
a = 1
a = 3
a = 1
a = 3
*/
</td>
</tr>
</tbody>
</table>
*[예제 8] 클로저의 상호작용, 서로 다른 객체*
위의 코드는 클로저의 예제 코드이며 그 중 좌측 코드는 서로 다른 클로저가 같은 내부 변수를 참조하고 있다는 것을 보여주고 있습니다. 서로 다른 클로저 innerFunc1과 innerFunc2가 내부 변수 a를 참조하고 a의 값을 바꿔주고 있습니다. 실행 결과를 보면 내부 변수 a의 메모리를 같이 공유한다는 것을 알 수 있습니다.
우측 코드는 같은 함수를 쓰지만 서로 다른 객체로 내부 변수를 참조하는 모습입니다. 외부 함수가 여러 번 실행되면서 서로 다른 객체가 생성되고 객체가 생성될 때 마다 서로 다른 내부 변수가 생성되어 보기엔 같은 내부 변수 a로 보이지만 서로 다른 내부 변수를 참조합니다.
###3.1 클로저의 사용이유
클로저를 사용하게 되면 전역변수의 오,남용이 없는 깔끔한 스크립트를 작성 할 수 있습니다. 같은 변수를 사용하고자 할 때 전역 변수가 아닌 클로저를 통해 같은 내부 변수를 참조하게 되면 전역변수의 오남용을 줄일 수 있습니다. 또한, 클로저는 JavaScript에 적합한 방식의 스크립트를 구성하고 다양한 JavaScript의 디자인 패턴을 적용할 수 있습니다. 그의 대표적인 예로 모듈 패턴을 말 할 수 있는데 모듈 패턴의 자세한 내용은 [Javascript : 함수(function) 다시 보기]을 참고 하시면 될 것 같습니다. 마지막으로 함수 내부의 함수를 이용해 함수 내부변수 또는 함수에 접근 함으로써 JavaScript에 없는 class의 역할을 대신해 비공개 속성/함수, 공개 속성/함수에 접근을 함으로 class를 구현하는 근거 입니다.
###3.2 클로저 사용시 주의할 점
클로저를 사용할 때 주의해야 할 점이 여럿 있습니다. 제가 알려드리고 싶은 주의 점은 다음과 같습니다.
**for 문 클로저는 상위 함수의 변수를 참조할 때 자신의 생성될 때가 아닌 내부 변수의 최종 값을 참조합니다.**
Subscribe to Nextreesoft
Get the latest posts delivered right to your inbox
TOP
You've successfully subscribed to Nextreesoft!
Could not sign up! Invalid sign up link.
Subscribe to Nextreesoft
Stay up to date! Get all the latest & greatest posts delivered straight to your inbox