본문 바로가기
Javascript

자바스크립트(javascript) 변수 스코프

by 램쥐뱅 2016. 8. 28.

자바스크립트에서 변수를 관리하는 메커니즘은 일반 언어에 비해 특징적인 부분 중 하나다.

자바스크립트의 변수 관리를 잘 이해하면 자바스크립트에서 혼란스럽거나 예민한 부분을 이해할수 있다.


자바스크립트에서 변수를 관리하는 메커니즘의 특징적인 부분을 3가지로 정리하면 다음과 같다.


1. 변수는 함수 단위로 관리한다.

2. 실행 시의 변수 검색은 렉시컬 영역(함수내 정의 환경)을 기준으로 한다.

3. 실행 시의 변수 검색은 변수 스코프 체인을 이용한다.



1. 함수 단위의 변수관리.


다른 프로그래밍 언어에서는 중괄호를 사용해 변수의 영역을 결정하는 것이 일반적이다.

예를 들어 for문의 코드 블록 내부에서 정의된 변수는 외부에서 접근할수 없다.


하지만 자바스크립트에서는 함수를 이용해 변수 스코프를 정의한다.

이것은 함수를 단위로 해당 함수가 사용하는 변수를 관리한다는 의미다.


즉 함수 내부에 존재하는 if 또는 for 문 코드 블럭의 내부에서 정의된 지역 변수는 해당 코드 블럭 외부에 정의된 지역 변수와 동일한 변수 스코프를 사용한다는 것이다.


var a =1;

function f() {

if(true) {

 var c = 2;

}

return c;

}


f(); // 2반환


자바스크립트에서는 같은 함수 내부라면 위 코드에서처럼 if 문 블록이나 for문 블록 뿐아니라 어떤 블록 내에서 정의한 변수에도 접근할 수 있다.


자바스크립트는 다른 일반 언어처럼 중괄호가 아닌 함수 단위로 변수가 관리된다.


위 코드 변수 a처럼 어떤 함수에도 포함되어 있지 않은 변수나 함수는 전역 변수 스코프에 정의된다.


만약 if문 블록 내부에서 var c = 2; 개신 var 를 없애고 c = 2; 로만 사용하면 어떻게 될까?

var 가없으면 변수가 정의되는 것은 파싱 단계가 아니라 런타임이다.

즉 런타임에 전역 스코프에 동적으로 변수가 정의 된다. 따라서 f()의 외부 코드에서도 사용할 수 있는 전역 변수가 되어 버린다.


var 없이 변수를 정의하면 파싱 단계가 아니라 런타임에 전역 변수 스코프에 정의된다.


function f() {

g = "global";

}

f();

console.log(g)    // global 출력


함수 f 내부에서 정의된 변수 g는 함수f가 실행될 때 전역 변수 스코프(웹브라우저 환경이라면 Window 객체)에 속성 g로 추가된다.


var를 사용하지 않고 변수를 정의하는 방법은 위험하다.

코드가 복잡해지거나 독립적으로 재 사용하기 위한 목적으로 라이브러리를 만드는 경우 코드가 정상적으로 실행되지 않는 잠재적 버그가 될 수 있다.



2. 변수 스코프 객체


function f() {

var a = 1;

return a;

}

f(); // 1 반환


함수 f가 호출되어 내부 코드가 파싱되면 다음처럼 함수 f에 대한 변수 스코프가 정의된다.


이제 실행 단계가 되면 아래 부분의 코드가 실행되고 변수 스코프에 있는 a에는 1이 할당되어 a가 반환될 때의 값은 1이 된다.


변수 스코프의 실체는 객체다.

이 변수 스코프 객체에는 몇 가지 종류의 변수가 추가된다.

함수를 실제로 호출할 때 사용한 인자와 값 그리고 실체 함수에서 정의한 매개변수를 구분해서 관리할 필요가 있다.

함수를 호출할 떄 사용한 인자와 함수를 정의할 때 사용한 매개변수가 해당 함수의 변수 스코프 객체에 구분되어 추가된다.

그리고 함수 파싱을 총해 찾게 되는 var 변수와 중첩된 내부의 함수 변수가 변수 스코프 객체에 추가된다.


function add(x, y) {

var a = x+y;

return a;

}


var r = add(1,2,3);



함수 내부의 코드에서 변수를 사용하면 그 변수의 현재값을 찾기 위해 가장 먼저 함수 자신의 변수 스코프 객체에서 검색하게 된다.


변수 스코프 객체는 함수의 호출 인자, 매개변수, 그리고 파싱 후에 얻게 되는 함수 내부 변수에 대한 값을 관리하는 객체다.


변수 스코프에는 내부 변수 외엗도 함수의 매개변수로 정의된 값고 호출하는 인자값이 모두 포함된다.

이러한 변수를 모두 그 함수의 지역 변수라고 한다.


변수 스코프 객체는 해당 함수의 지역 변수를 관리하는 객체다.


자바스크립트의 객체는 맴버, 즉 속성 또는 메서드가 동적으로 추가될 수 있다.

함수를 호출하면 자바스크립트는 동적으로 해당 함수의 변수 스코프 객체를 생성하고 함수 인자 및 매개변수, var 변수를 차례로 추가해서 해당 함수 호출과관련된 변수 스코프 객체를 완성한다.


var가 없거나 어떤 함수에도 포함되지 않는 변수는 어떻게 될까?


var g1 = "전역 변수1";

function f() {

g2 = "전역 변수2";

}


제일 번저 변수 g1은 현재 자바스크립트 프로그램이 실행되는 환경에서의 루트 객체, 예를 들어 웹 브라우저 환경이라는 window 객체에 추가된다. 그러고 나서 함수 f가 호출되면 그때 다시 런타임에 g2가 루트 객체에 추가 된다.


var a = 1;

fucntion f(){

var b = 1;

return a;

}

f(); // 1반환

b;  // b is not defined 예외 발생


함수의 변수 스코프에 선언된 변수는 해당 함수의 외부에서는 접근할 수 없다.



3. 렉시컬 특성


렉시컬(lexical)의 의미는 "단위, 어휘와 관련 있다" 라는 의미로서, 자바스크립트에서는 프로그램이 구현된 코드와 관련되 있음을 의미한다.

변수를 검색할 때 함수가 실행되는 환경을 근거로 판단하는 것이 아니라 함수를 정의한 코드의 문맥을 근거로 판단한다는 것이다.


var x = "global";

function f() {

console.log(x);    // undefined

var x = "local";

console.log(x)    // local

}

f();


위 코드를 실행하면 먼저 전역 레벨의 파싱이 일어난다.

이 파싱의 결과로 전역 변수 x와 함수 변수f가 정의된다. 그런다음 f()를 실행하면 함수 f가 호출되고 f 레벨의 파싱이 일어난다.

이파싱의 결과로 함수 내부에 있는 x가 함수 f의 변수 스코프 객체에 정의된다.


f의 파싱이 끝나고 나면 f의 코드가 실행 될때 최초로 만난 console.log(x)를 실행하기 위해 x의 값을 찾아야 하는데, 민제는 실행 환경상에서 x를 찾을 것이냐 렉시컬 환경상에서 x를 찾을 것이냐다.


렉시컬 특성이라는 제목이 암시하듯이 렉시컬한 환경을 기준으로 정의된 f의 변수 스코프 객체에 정의된 x를 이용한다.

따라서 최초로 만난 console.log(x)에서는 undefined가 출력된다.

다음에 x="local"이 실행되고 나서야 f의 변수 스코프에 있는 x의 값이 "local"로 변경된다.


function f1() {

var a = 1;

f2();

}


function f2() {

return a;

}


f();    // a is not defined


전역 변수 스코프에 변수 f1, f2가 정이된다. 그리고 함수 f1의 변수 스코프에는 a가 정의되고 f2의 변수 스코프에는 정의된 변수가 없다.

f1()을 실행하면 f1내부에서 함수 f2가 호출된다. f2코드에서는 변수 a를 검색하게 되는데, a를 실행환경에서 찾는 것이 아니라 렉시컬한 환경에서 찾는다. 즉 f2가 정의 된 곳에는 a가없다. 또한 전역 변수 스코프에도 없다. 따라서 a는 정의되지 않았다는 예외가 발생한다.


실행시 각 문장이 참조하는 변수는 렉시컬 환경에서 정의한, 즉 코드 그대로의 환경을 기준으로 정의한 변수 스코프에서 검색한다.


자바스크립트는 변수가 코드상에서 반드시 먼저 선언되어 있어야 한다는 규칙이 없다. 함수가 사용하는 변수가 함수를 정의할 당시에는 없을지라도 프로그램을 실행할 때 함수가 접근할 수 있는 유효한 영역에 해당 변수가 추가되어 있기만 하면 된다.


파싱 단계와 실행 단계가 구분되어 있고 함수 단위의 렉시컬한 변수 스코프가 존재한다는 것에 주의하지 않으면 결과 예상이 빗나갈 수 있다.



4. 변수 스코프 체인


변수 스코프를 결정하는 단위가 함수라고 했다.

자바스크립트에서는 중첩 함수가 가능 하다고 했는데, 이러한 경우 함수별로 생성되는 변수 스코프 객체 간에는 부모, 자식 관계가 만들어진다.


var x = 1;

function outer() {

var y = 2;

function inner() {

var z = 3;

var a = x;

}

}

outer();


이 코드가 실행 되면 변수 x가 검색 되는데, 이떄 실행문 a = x;를 포함하고 있는, 즉 정의하고 있는 함수의 변수 스코프에서부터 먼저 검색된다.

만약 정의된 함수의 변수 스코프에서 x가 검색되지 못하면 해당 함수를 포함하고 있는 상위 outer의 변수 스코프에서 검색된다.

그곳에서도 찾지 못하면 전역 변수 스코프에서 x가 검색된다. 만약 그곳에 x가 정의되어 있지 않다면 변수가 정의되지 않았다는 에러가 발생한다.

이렇게 변수 스코프 간의 관계를 변수 스코프 체인 (scope chain)이라고 한다.


변수 검색이 가능한 영역은 변수가 정의된 함수의 변수 스코프와 부모 함수를 포함한 조상 함수의 변수 스코프다.


function outer(count) {

inner();

function inner() {

return --count;

}

}


outer 함수를 호출하면 inner()를 호출하는데, inner 내부에서는 함수 매개변수로 정의된 count 변수를 사용하고 있다. count가 outer의 변수 스코프에 정의되어 있기 때문에 변수 스코프 체인을 거슬러 올라가서 outer에 정의된 counter에 접근할 수 있게 되는 것이다.



5. 루트 객체


함수의 영역이 아닌 어떤 함수에도 속하지 않는 최상위 영역에서 변수를 선언하거나 함수를 정의하면 그것들은 모두 루트 객체의 속성과 메서드로 추가된다.


<script>

var a = 1, b = 2;

this.prop1 = "a";

function init(obj){

var c;

}

</script>


a, b 그리고 prop1, init은 모두 어떤 함수에도 속하지 않는, 즉 전역 영역에서 정의되는 변수로서 루트 객체의 속성과 메서드로 추가된다.

this.prop1 에서 this는 루트 객체에 prop1이라는 속성을 추가하고 그 값으로 "a"를 할당하는 코드이다.


전역적인 실행 환경 영역에서는 변수에 var를 붙여서 정의하든 var를 붙이지 않든 해당 변수가 정의되는 객체가 전역 변수 스코프 객체라는 점에서는 차이가 없다.


웹 페이지 실행 환경에서의 루트 객체는 Window 객체로서 코드에서는 window를 통해 접근할수 있다.

따라서 전역 영역의 코드에서는 this와 window는 같은 객체를 참조한다.


this === window // true


루트 객체는 생성자가 없다.

자바스크립트 코드에서 new Window()처럼 호출해서 객체를 생성할 수는 없다.



6. 클로저


클로저라는 개념은 자바스크립트에는 없는 class의 역할을 대신해 비공개 속성/메서도, 공개 속성/메서드를 구현할 수 있는 근거를 마련한다.

객체지향적인 특징인 캡슐화(encapsulation)와 정보 은닉(information hiding)을 이해하려면 클로저를 반드시 이해해야 한다.

클로저는 jQuery 같은 대형 하이브러리에서 흔히 채용되는 패턴이다.


앞의 변수 스코프 체인에서 본 외부 함수 outer, 내부 함수 inner와 같은 중첩 함수를 다시 떠올려 보자.

정의 단계에서 inner는 outer의 내부에 있고 실행 단계에서도 outer의 내부에서 실행되었기 때문에 inner에서 사용된 변수는 inner 자신의 변수 스코프 및 부모 영역에서 검색하게 되리라는 것을 쉽게 이해할 수 있다.


그런데 함수 inner가 반환값으로 사용되어 다른 영역의 코드에서 실행되는 경우를 상상해보자.


function outer() {

var x = 0;

return function() {

return ++x;

}

}


var x = -1;

var f = outer();

f();    // 1


이전에 본 코드와 달라진 점은 다음과 같다.


1. 내부 함수가 익명 함수로 되어 outer의 반환값으로 사용되었다.

2. inner는 outer의 실행 환경(execution environment)에서 실행된다.

3. inner에서 사용하는 변수x는 outer의 변수 스코프에 있다.


이 프로그램이 실행되면 var f = outer()에 의해 파싱 단계에서는 outer의 내부에서 정의됬던 익명 함수가 실행 단계에서는 outer의 외부로 전달되어 실행 된다.


실행 환경에 있는 f를 통해 outer가 반환한 익명 함수가 호출되면 return ++x;에서 사용된 변수 x를 어디에서 검색할까?

렉시컬 특성을 알아보면서 논의한 문제와 유사하다.

답은 런타임의 변수는 렉시컬 환경을 기준으로 정의된 변수 스코프 및 체인에서 검색한다는 것이다.


결국 inner를 호출하면 렉시컬 특성으로 outer 의 변수 x를 사용하여 ++x 연산에 의해 1이 반환된다.



중요한 문제를 이야기 해보자.

함수 inner를 계속해서 호출해서 결과를 보면 다음과 같다.


f();    // 2

f();    // 3

f();    // 4


어떻게 f의 호출이 끝나고 나서도 그 부모의 변수 스코프에 있는 x 값이 유지될 수 있을까?


내부 함수에서 선언된 변수가 아니면서 내부 함수에서 사용하는 outer의 x 같은 변수를 자유변수(free variable)라고 한다.

x가 메모리에서 제거되는 시기는 outer가 결정하지 못한다.

이런 자유 변수는 outer가 실행되고 있는 환경이 "닫는(close)" 역할을 한다. 즉 x의 경우는 변수 스코프가 outer가 실행되는 환경으로까지 확장된다.

외부 환경에서 내부 함수에 대한 참조 f를 가지고 있는 이상(즉 f가 메모리에서 사라지지 않는 이상) outer 함수는 "실행중" 상태가 된다.

따라서 자유 변수x 및 해당 변수 스코프 체인 관계는 메모리에서 계속 유지 된다.


이처럼 outer호출이 종료되더라도 outer 의 지역 변수 및 변수 스코프 객체의 체인 관계를 유지할 수 있는 구조를 클로저(closure)라고 한다.


함수 호출이 종료되더라도 그 함수의 지역 변수 및 지역 변수 체인 관계를 유지할 수 있는 구조를 클로저라 한다.


자유 변수의 경우 그 값은 렉시컬 환경의 영향을 받으면서 그 생명주기는 실행 환경의 영향을 받는다는 것이 결국 클로저를 만들 수 있는 근거가 된다.


Function 으로 생성한 함수는 클로저를 만들지 못한다.

Function 생성자를 이용해 생성한 함수는 렉시컬 영역을 사용하지 않는다. Function 함수는 항ㅅ상 전역 영역에서 생성된 것처럼 컴파일 된다.


var x = "g";

function f() {

var x = "1";

return new Function("return x");

}

f();    // "g"



클로저 인스턴스 1


클로저를 함수 인스턴스를 만들어내는 특수한 함수로 해석할수 있다.


클로저란 호출하면 다른 함수 인스턴스를 생성해내는 특수한 구조의 함수다.


앞에서 본 outer 함수를 클래스로 생각해본다면


비공개 영역 -> var x = 0;

공개 영역 -> function() { return ++x; }


outer 클로저를 이렇게 비공개 변수를 정의하는 부분과 외부에서 호출이 가능한 공개 영역으로 나눠서 생각해보면 다른 언어의 클래스와 더욱더 유사해 보일 것이다.

이제 outer를 호출 하는 것을 바로 함수 객체를 생성 하는 것으로 생각하면 된다.


var f = outer(); // outer의 인스턴스 생성


클로저를 호출하는 것은 "클로저의 인스턴스를 생성" 하는 것이다.


클로저가 반환한 함수 f를 호출하는 것을 outer 가 외부에 공개한 메서드를 호출하는 것으로 간주할 수 있다.


f(); // outer의 공개 함수 사용


클로저 인스턴스를 호출한다는 것은 클로저가 외부에 공개한 멤버를 호출하는 것으로 이해할 수 있다.


f가 사라지지 않는 이상 인스턴스 f가 가지고 있는 변수도 계속 유지된다.

일반 객체지향 프로그래밍 언어의 관점에서 보면 너무나 당연한 이야기다.


var f = outer();

f();    // 1

f();    // 2


var g = outer();

g();    // 1

g();    // 2


outer()를 호출하여 생성된 함수를 f에 할당한다.

f를 호출해서 값을 1 증가시키면 다음에 f가 호출될 때는 이전에 증가된 값이 유지되어 두 번째 호출의 시작값이 된다.

함수 f 호출이 종료 되어도 내부 변수  x는 그대로 유지되는 클로저의 속성을 이용하고 있다.


다시 한번 outer()를 호출해서 새로운 인스턴스를 g에 할당한후 g를 호출해서 결과를 보면 내부 변수 x가 새롭게 초기화 됬음을 알 수 있다.

내부 변수 x가 새롭게 초기화 됬다는 것은 이전의 함수 f와 새롭게 생성된 함수 g는 전혀 다른 변수 공간을 사용하는 별도의 존재라는 것이다.



클래스로 인스턴스를 생성할 때마다 자신만의 닫혀진 공간을 가진 인스턴스가 생성된다.

그래서 인스턴스에서는 다른 인스턴스의 닫혀진 공간에 있는 내부 변수에는 직접 접근할수 없다.


클로저를 호출하면 단순히 익명 함수가 반환되는 것이 아니다.

익명 함수와 함께 거기에 연결된 닫혀진 공간이 함께 반환되는 것이다.

그 닫혀진 공간에 내부 변수가 존재한다.


클래스와 new를 사용해 여러 개의 닫혀진 공간을 가진 인스턴스를 만들어 내듯이 클로저와 ()를 이용하면 닫혀진 공간을 갖는 인스턴스를 여러 개 반복해서 만들어낼 수 있다.


클로저란 비공개 내부 변수를 갖는 함수 인스턴스 생성자다.

그리고 클로저로 생성한 독립된 변수 공간을 가진 인스턴스를 클로저 인스턴스라고 한다.


지금까지는 클로저가 함수를 반환했다.

만약 클로저가 함수대신 객체를 반환한다면 어떻게 될까?

이와 관련된 내용은 이후 자바스크립트 객체 맴버에서 다뤄본다.












참고.


자바스크립트 객체지향 프로그래밍.

댓글