본문으로 바로가기

변수 스코프 

함수를 정의하고 있는 내부 코드에서 접근할 수 없는 곳에 있는 변수에 접근하거나 접근할 수 있는 영역 내에서 해당 변수가 없으면 에러가 발생합니다.

함수를 정의하는 개발자는 자신이 정의한 함수에서 어떤 변수에 접근할 수 있는지 이해하고 있어야 합니다.

이 글에서는 자바스크립트의 변수 스코프에 대해 알아봅니다.




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

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

변수 관리 메커니즘과 관련된 개념은 변수 스코프 체인, 클로저, 함수 또는 객체를 생성하는 클로저, 모듈 패턴, jQuery 코드 구조 등이 있는데, 만약 변수 관리 방법을 이해하지 못하면 이러한 굵직한 개념을 이해하는 데 어려움을 겪게 될지도 모릅니다.


변수 스코프 → 변수 스코프 체인 → 클로저 → 클로저의 함수, 객체생성 → 모듈 패턴 →  jQuery 코드구조


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

  • 변수는 함수 단위로 관리한다.
  • 실행 시의 변수 검색은 렉시컬 영역을 기준으로 한다.
  • 실행 시의 변수 검색은 변수 스코프 체인을 이용한다.



변수란?

프로그램은 작업을 처리하는 과정에서 필요에 따라 데이터를 메모리에 저장합니다. 이때 변수를 사용하는데, 변수(Variable)는 값을 저장할 수 있는 메모리 공간을 의미합니다.

변수란 이름을 갖게 된 이유는 프로그램에 의해서 수시로 값이 변동될 수 있기 때문입니다.



함수 단위의 변수 관리

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

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

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

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


중괄호 단위가 아닌 함수 단위로 변수 스코프가 정의된다는 것은 함수 내부에 존재하는 if 또는 for 문 코드 블럭의 내부에서 정의된 지역 변수는 해당 코드 블럭 외부에 정의된 지역 변수와 동일한 변수 스코프를 사용한다는 것입니다.

다음의 코드를 살펴보도록 합니다.

javascript
var a = 1;
function fn() {
    if(true) {
        var c = 2;
    }
    return c; // if 문 블럭에 정의된 변수 c를 반환한다.
}
console.log(fn()); // 2를 반환

함수 fn의 if 문 블럭에서 변수 c가 정의되어 있습니다. 그리고 if 문 블럭 외부의 함수 코드에서 c에 접근(return c;)하고 있습니다.

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


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


이런 특징은 다른 언어에서는 볼 수 없는 특성입니다.

예를 들어, 자바나 C# 같은 언어의 변수 스코프는 중괄호로 구분되기 때문에 if 문의 코드 블록 내부에서 사용하는 변수와 외부에서 사용하는 변수의 스코프는 다릅니다.

따라서 if 문의 코드 블록 외부에서 내부에 정의된 변수에 접근할 수 없습니다.

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


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

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

즉, 함수 내부에서 변수를 정의하더라도 var 없이 변수를 정의하면 런타임에 전역 스코프에 동적으로 변수가 정의됩니다.

따라서 fn()의 외부 코드에서도 사용할 수 있는 전역 변수가 되어 버리고 말 것입니다.


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


다음의 코드를 살펴봅니다.

javascript
function fn() {
    g = 'global'; // 전역 범위에 선언된다
}
fn();
console.log(g);

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


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

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

이는 외부에서 우연히 동일한 이름의 변수를 정의해서 사용할 수 있기 때문입니다.

특별한 목적이 아니라면 변수를 선언할 때는 var를 명시적으로 사용해 변수를 참조할 수 있는 스코프를 명확하게 정의하는 것이 좋습니다.




변수 스코프 객체

이제 변수 스코프 객체에 대해 알아보겠습니다.


다음과 같은 간단한 코드를 살펴봅니다.

javascript
function fn() {
    var a = 1;
    return a;
}
console.log(fn()); // 1 반환

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


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


그렇다면 변수 스코프는 무엇일까요? 변수 스코프의 실체는 객체인 것입니다.

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

자바스크립트 함수에서는 정의하는 매개변수(parameters)의 수와 실제로 호출에 사용되는 인자(arguments)의 수가 반드시 같지 않아도 되기 때문에 함수를 실제로 호출할 때 사용한 인자와 값, 그리고 실제 함수에서 정의한 매개변수를 구분해서 관리할 필요가 있습니다.

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

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


예를 들어 다음과 같은 함수를 있습니다.

javascript
// add 정의
function add(x, y) {
    var a = x + y;
    return a;
}

// add 호출
var r = add(1, 2, 3);

add(1,2,3) 을 실행하면 다음과 같이 add 함수의 변수 스코프 객체가 먼저 구성됩니다.

변수 스코프 객체는 위 그림처럼 함수 호출 시 사용된 인자 정보를 가지고 있는 arguments, 함수를 정의할 때 사용하고 있는 매개변수, 그리고 내부 변수를 (이름,값)의 쌍으로 관리하는 객체입니다.

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


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


흔히 함수의 변수 스코프에는 내부 변수만 있는 것으로 생각하는 경향이 있습니다.

그러나 변수 스코프 객체에는 이렇듯 내부 변수 외에도 함수의 매개변수로 정의된 값과 호출하는 인자값이 모두 포함됩니다.

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


변수 스코프 객체는 다시 말하면 해당 함수의 지역 변수를 관리하는 객체라고 할 수 있습니다.


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

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


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

javascript
var g1 = "전역변수#1";
function fn() {
    g2 = "전역변수#2";
}

프로그램을 실행하면 제일 먼저 변수 g1은 현재 자바스크립트 프래그램이 실행되는 환경에서 루트 객체, 예를 들어 웹브라우저 환경이라는 Window 객체에 추가됩니다. 

그리고 나서 함수가 fn() 이 호출되면 그때 다시 런타임에 g2가 루트 객체에 추가됩니다.


변수 스코프 객체의 특징

함수의 변수 스코프에 대한 또 다른 특징에 대해 알아보겠습니다.

javascript
var a = 1;
function fn() {
    var b = 1;
    return a;
}
console.log(fn()); // 1 반환
console.log(b); // b is not defined 란 예외가 발생함

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

즉, fn 내부에 정의된 b는 함수 fn 외부에서는 접근할 수 없다는 것입니다.




렉시컬 특성

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

즉, 함수를 정의하고 있는 "코드"의 상황과 관련되어 있다는 의미입니다.

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


다음의 코드를 살펴봅니다.

javascript
var x = "global";
function fn() {
    console.log(x); // "undefined" 출력 즉 global을 출력하지 않는다
    var x = "local"; // x 변수 선언
    console.log(x); // local 출력
}
fn();

위 코드를 실행하면 함수 fn 내의 첫 번째 console.log(x) 가 "global"을 출력할 것으로 기대할지도 모릅니다.

그러나 "undefined" 로 출력됩니다.


위 코드를 실행하면 먼저 전역 레벨의 파싱이 일어납니다. 이 파싱의 결과로 전역 변수 x와 함수 변수인 fn이 정의됩니다.

그런 다음 fn()을 실행하면 함수 fn이 호출되고 fn 레벨의 파싱이 일어납니다.

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


함수 fn 레벨까지의 파싱이 일어난 후의 모습을 그림으로 표현하면 다음과 같습니다.


                                     파싱결과


프로그램이 시작되면 전역 레벨의 파싱으로 그림의 루트(전역) 객체의 변수 스코프가 정의되고, 실행 단계에서 x = 'global'이 실행되고 나서 함수 fn()이 호출되면 다시 함수 fn 레벨의 파싱이 시작되고 이 파싱이 끝나면 그럼처럼 지역 변수 x 가 정의됩니다.


fn의 파싱이 끝나고 나면 fn의 코드 블록이 실행될 것입니다. 그렇게 해서 최초로 만난 console.log(x); 를 실행하기 위해 x의 값을 찾아야 하는데, 문제는 실행 환경상에서 x를 찾을 것이냐 렉시컬 환경상에서 x를 찾을 것이냐의 문제입니다.


"렉시컬 특성"이라는 것이 암시하듯이 렉시컬한 환경을 기준으로 정의된 fn 의 변수 스코프 객체에 정의된 x를 찾게 됩니다.

따라서 최초로 만난 console.log(x);에서는 undefined 가 출력될 것입니다.

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


자바스크립트의 렉시컬한 특성 때문에 생기는 또 다른 결과를 살펴보도록 하겠습니다.

javascript
function f1() {
    var a = 1;
    f2();
}
function f2() {
    return a;
}
console.log(f1()); // a is not defined 라는 예외가 발생

파싱 단계를 생각해 봅시다.

전역 변수 스코프에 변수(함수변수) f1, f2가 정의됩니다. 그리고 함수 fn1의 변수 스코프에 a가 정의되고, f2의 변수 스코프에는 정의된 변수가 없습니다.


이제 f1()을 실행하도록 합니다.

f1 내부에서 f2 를 호출하고 있습니다. f2의 코드를 보면 변수 a를 반환합니다.

변수 a를 검색하게 되는데, a는 실행환경에서 찾는 것이 아니라 렉시컬한 환경에서 찾습니다.

즉, f2가 정의된 곳에는 a가 존재하지 않습니다. 또한 전역 변수 스코프에도 없습니다.

따라서 a는 정의되지 않았다는 예외가 발생하게 됩니다.

f2()가 실행되는 것은 f1 내부이지만 f2가 정의된 곳에는 변수 a가 없습니다.


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


즉, 함수가 실행되고 있는 환경에서 a를 검색하는 것이 아니라 각 문장이 정의된 함수에서 검색합니다.


자바스크립트는 다른 프로그래밍 언어와는 다르게 변수가 코드상에서 반드시 먼저 선언되어 있어야 한다는 규칙이 없습니다.

함수가 사용하는 변수가 함수를 정의할 당시에는 없을지라도 프로그램을 실행할 때 함수가 접근할 수 있는 유효한 영역에 해당 변수가 추가돼 있기만 하면 됩니다.

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




변수 스코프 체인

앞에서 변수 스코프를 결정하는 단위가 함수라고 설명했습니다.

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


이해를 돕고자 다음과 같은 코드를 살펴보도록 합니다.

javascript
var x = 1;
function outer() {
    var y = 2;

    function inner() {
        var z = 3;
        var a = x;
    }
}
outer();

함수 outer 가 정의되어 있고 내부에 함수 inner 가 정의되어 있습니다.

이 코드가 파싱되면 다음과 같은 논리적인 구조로 메모리에 정의됩니다.


                                변수 스코프 체인


왼쪽 그림은 코드 그대로를 기준으로 렉시컬 변수 스코프이고 화살표(→) 오른쪽은 변수 스코프 객체라는 관점에서 외부 함수와 내부 함수의 각각의 변수 스코프 객체의 관계도를 나타냅니다.


그림에서 변수 z는 함수 inner의 지역 변수이고 코드가 실행되면 z, a는 inner 함수의 변수 스코프 객체의 속성이 됩니다.

y는 outer의 지역변수로서 outer 의 변수 스코프 객체의 속성이 됩니다.

이 상황에서 함수 inner의 내부에 있는 다음 코드가 실행될 때 어떤 일이 일어나는지 살펴보겠습니다.

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

이는 렉시컬한 특성을 설명하면서 언급한 내용입니다.


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

그곳에서도 찾지 못한다면 전역 변수 스코프(global object)에서 x가 검색될 것입니다. 만약 그곳에서도 x가 정의되어 있지 않다면 변수가 정의되지 않았다는 에러가 발생하게 됩니다.

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


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




Global Object(루트 객체)

이 글에서는 전역 변수 스코프를 나타내는 객체를 루트 객체(global object)라고 표현하겠습니다.

참고로 루트 객체를 "전역 객체"라고 표현하기도 합니다.

그러나 다른 언어에서는 "전역"이라는 의미를 부모 영역이라는 상대적인 의미로 사용하기도 합니다.

즉, 현재 함수의 부모 영역은 모두 전역 영역으로 표현하는 것입니다.

혼란을 피하고자 여기서는 모든 영역의 최상위 부모 영역을 나타내는 의미로 "루트"라는 표현을 사용하도록 하겠습니다.


런타임에 전역 변수 스코프의 코드에서 this 를 이용하면 루트 객체에 정의된 변수 및 속성, 함수를 사용할 수 있습니다.


전역 변수 스코프의 코드에서의 this 는 루트 객체를 참조한다.


루트 객체에 정의된 속성과 함수의 대표적인 예를 보면 다음과 같습니다.

javascript
// 전역 속성 정의
this.Infinity
this.undefined
this.NaN
...

// 내장 객체(내장 생성자) 정의
this.Object = function () { };
this.Array = function () { };
this.Function = function () { };
this.RexExp = function () { };

// 전역 함수(global function) 정의
this.parseInt = function () { };
this.encodeURI = function () { };
this.decodeURI = function () { };
...


이처럼 내장 객체는 전역 변수 스코프에서 정의된 함수입니다.

그러나 전역 변수 스코프에 정의된 함수 및 속성을 코드에서 사용할 때는 this 를 생략할 수 있습니다.


사실 전역적인 실행 환경, 즉 함수의 영역이 아닌 어떤 함수에도 속하지 않는 최상위 영역에서 변수를 선언하거나 함수를 정의하면 그것들은 모두 루트 객체의 속성과 메서드로 추가됩니다.

javascript
var a = 1, b = 2;
this.prop1 = "a";
function init(obj) {
    var c;
}

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

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


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


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

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


다음의 비교식은 참을 반환합니다.

javascript
console.log(this === window); // true 를 반환


참고로 루트 객체는 생성자가 없습니다.

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





Jaehee's WebClub