본문으로 바로가기

값 타입과 참조타입 그리고 자바스크립트 프로그램 실행 단계

이 포스팅에서는 데이터 타입에서 값 타입과 참조 타입 그리고 자바스크립트 프로그램 실행 단계에 대해 알아보는 시간을 가져보도록 합니다.




값 타입의 데이터와 참조 타입의 데이터

변수에 데이터를 할당하는 것은 나중에 그 값을 다시 사용하기 위해서입니다.

변수 공간에 할당된 값을 다시 프로그램 코드에서 사용하게 되는 과정을 알아봅니다.

javascript
a.num = 3;
b = "3";
c = a.num + b;

프로그램을 실행하다가 c = a.num + b; 문장을 만났다고 해 봅시다.

자바스크립트는 우선 + 연산에 사용되는 좌우측 피연산자의 최종 값을 찾아가게 될 것입니다.

a.num 의 값을 찾아가보면 "." 연산자 때문에 a가 가지고 있는 속성 중에서 num을 찾게 됩니다.

그래서 그 값을 + 연산자의 좌측 값으로 사용하게 될 것입니다.


이러한 연산 과정에서 자바스크립트는 변수의 공간에 있는 값을 최종 값으로 사용할 것인지(b의 경우) 아니면 다른 메모리를 참조하는 값으로 판단해야 할지(a의 경우)를 결정할 수 있어야 합니다.

즉, 변수에 있는 데이터가 값 타입(value type)인지 참조 타입(reference type)인지 알수 있어야 합니다.


자바스크립트에서 제공하는 값 타입의 데이터는 숫자(number), 문자열(string), 불린(boolen)이 있고 조금 특수하게 "정의되지 않음(undefined)", "객체가 없음(null)"이 있습니다.

이런 값 타입 데이터를 제외하고 자바스크립트에서 제공하고 사용자가 정의한 타입은 모두 참조 타입의 데이터라고 보면 됩니다.


undefined 는 값이 정의되지 않음(값이 할당되지 않음)을 의미하고 null 은 객체가 없음, 즉 값을 할당은 했으나 값으로서 아무런 의미가 없는 값이고 일반적으로 코드를 작성하면서 어떤 변수에 null 이 할당되어 있다면 암묵적으로 나중에 객체 값을 할당할 것이라는 의미가 내포되어 있다고 볼수 있습니다.


다시 자바스크립트 입장이 되어 위 코드의 세번째 식에서 사용되는 변수의 최종 값을 검색하는 과정으로 돌아가 봅니다.

자바스크립트는 우선 연산에 사용된 a, b가 값 타입 변수인지 참조타입 변수인지를 판별합니다.

그래서 a는 참조 타입이라고 판단하게 되고 해당 참조값이 가리키는 메모리로 이동해서 num이라는 것을 찾습니다.

num을 찾으면 그것이 값 타입의 데이터라는 것을 알게 되고, 그럼 num이 가지고 있는 값을 반환하게 됩니다.

b는 값 타입의 데이터라는 판단을 하게 되고, b에 저장된 데이터를 곧바로 반환합니다.

반환된 두 값은 + 연산에 사용하게 됩니다.

자바스크립트는 + 연산에 사용되는 두 데이터의 타입이 좌측은 숫자, 우측은 문자열이라는 사실을 알게 되는 것입니다.

그러면 자바스크립트가 가지고 있는 타입 간의 변환 규칙에 따라 숫자인 3을 문자열로 변환합니다.

결국 다음과 같은 연산이 됩니다.

javascript
c = "3" + "3";


자바스크립트 엔진이 a.num, b의 최종값을 찾아가는 절차를 그림으로 보면 다음과 같습니다.

a.num, b 검색절차



위에 설명된 과정이 자바스크립트에서 변수의 최종 값을 찾아 사용하는 절차입니다.

자바스크립트가 사용하는 약한 타입 체계에서는 num이 a 객체의 멤버인지는 사전에 알 수 없고 알 필요도 없습니다.

a 가 가리키는 곳으로 실제로 이동해서 그곳에 멤버 중에 num이 있는지를 알게 되는 시기는 프로그램이 실행되는 런타임이 되고 나서입니다.


약한 타입 체계의 언어에서는 런타임이 돼서야 객체의 멤버가 존재하는지 확인할 수 있다.


이에 비해 강력한 타입 언어에서는 변수의 타입이 컴파일할 때 결정됩니다.

이것은 변수의 데이터 구조가 사전에, 즉 실행되기 전에 파악할 수 있다는 의미입니다.

컴파일할 때 a 객체의 구조가 파악되면 num이 a의 멤버인지 알 수 있습니다.

따라서 컴파일 단계에 a에 없는 num2 같은 멤버를 사용하면 컴파일 에러를 런타임 전에 개발자에게 보여줄 수 있는 것입니다.


자바스크립트에서 변수값을 검색해 나가는 과정이 실행 단계에 수행된다는 것은 var 변수가 어떤 타입의 데이터라도 받아들일 수 있다는 이론의 근거로 볼 수 있습니다.

이런 이유로 자바스크립트에서는 객체에 없는 멤버를 조회할 때도 컴파일 단계에서는 에러가 발생하지 않고 실제로 실행해 봐야 에러를 볼 수 있습니다.


사실 자바스크립트 객체의 멤버는 모두 var 변수입니다.

속성과 메서드라는 용어는 객체지향과 관련된 용어로서 사람들의 커뮤니케이션에 편의를 제공할 뿐입니다.

자바스크립트 엔진 입장에서는 속성, 메서드, 변수를 구분하지 않습니다.

모두 var 변수로서 값 타입이냐 참조 타입이냐만 구분할 수 있으면 됩니다.


이처럼 런타임(프로그램이 실행)이 되어서야 타입을 확인한다는 것이 바로 약한 타입 체계의 특징이다.

자바스크립트 객체의 멤버(속성, 메서드)는 모두 var 변수이다.


예를 들어, name, setName 을 멤버로 포함하는 사용자 정의 타입인 Person이 있다고 가정해 봅니다.

그리고 인스턴스를 생성해 봅니다. 

javascript
function Person(name) {
    this.name = name;
    this.setName = function () {
        return this.name;
    }
}

var mySon = new Person('홍길동'); // 인스턴스 생성 및 문자열(값타입) 전달
console.log(mySon.setName()); // 함수 호출

mySon 인스턴스의 name 속성은 값 타입의 변수이고 setName 메서드는 참조타입의 변수입니다.

변수 setName이 함수를 가리키고 있는 참조타입의 변수라는 것도 런타임에 확인 할 수 있습니다.

즉, 자바스크립트가 이 코드를 만나면 우선 setName 이 가리키는 곳으로 이동하고 그런 다음 연산자 "()" 때문에 그 위치에 정의된 실행 코드 블록을 실행합니다.

만약 코드 블록이 없다면 에러가 발생하는 것입니다.


자바스크립트 객체의 멤버는 런타임에 존재 여부 및 멤버 타입(속성, 메서드)을 확인 할 수 있다.




프로그램 실행 단계

인스턴스가 메모리에 생성되는 과정은 일반 강력한 타입의 언어와 자바스크립트가 조금 차이가 있습니다.

이 약간의 차이로 일반 언어(C#, java 등)의 관점으로는 이해할 수 없는 상황이 연출되기도 하고 때로는 일반 언어를 기준으로 자바스크립트 코드를 이해하려 하면 디버깅할 때 고생하는 경우가 발생할 수 있습니다.



자바스크립트 프로그램 실행 절차



자바스크립트 프로그램을 시작하면 바로 프로그램이 실행되는 것이 아니라 프로그램의 전역 레벨에서의 파싱 단계를 거칩니다.

전역 레벨에서의 파싱이란 어떤 함수에도 포함되지 않은 변수와 어떤 함수에도 포함되지 않는 이름 있는 함수(named function)에 대해 함수명과 동일한 변수를 만들고 이 변수를 실행 코드가 담긴 함수에 대한 참조로 초기화하는 것을 말합니다.


앞으로 '함수명과 동일한 이름의 변수'를 함수 변수라고 명명하겠습니다.


파싱 단계를 마치고 나면 프로그램이 실행되는데, 프로그램을 실행하다가 함수 호출을 만나게 되면 해당 함수 레벨의 파싱 단계를 반복합니다.

즉, 함수의 코드에서 그 함수의 지역 변수와 함수 변수를 정의하고 나서 비로소 함수 코드를 실행하게 됩니다.

함수 호출을 만날 때마다 이런 파싱과 함수 실행이 반복되면서 프로그램을 끝까지 실행합니다.


파싱을 마치고 나면 해당 레벨의 var 변수와 함수 변수가 정의된다.

이 단계에서는 코드 블록(실행블록)은 실행되지 않는다.


좀 더 정확히 말하면 전역 레벨의 파싱 단계에서 정의되는 변수와 함수 변수는 사실 루트 객체, 즉 웹브라우저에서 프로그램이 실행되는 환경이라면 Window 객체의 멤버로 추가됩니다.

그리고 함수 레벨의 파싱에서는 해당 함수와 연결되어 있는 변수 스코프라는 객체의 멤버로 추가됩니다.

사실 루트 객체도 최상위 변수 스코프 객체입니다.


"객체의 멤버로 추가"된다는 것이 어떤 의미인지 이해하기 어려울 수도 있으나 한가지 알아두어야 할 것은 모든 변수, 즉 var 변수든 함수 변수든 그것이 속하는 객체가 있습니다.

프로그램에서 어떤 함수에도 속하지 않는 var 변수와 함수가 있다면 이것들은 런타임 과정에 루트 객체에 속하게 되고, 함수에 속하는 변수와 내부 함수는 해당 함수와 연결된 변수 스코프 객체에 속하게 됩니다.


프로그램의 모든 var 변수와 함수 변수는 그것과 연관된 변수 스코프 객체의 멤버로 추가된다.


다음의 간단한 예제 코드를 통해 파싱 단계에서 무슨 일이 일어나는지 알아보도록 합니다.

javascript
// 파싱 단계에서 변수 x가 메모리에 undefined 로 정의됨
// 런타임에 0 이 할당됨
var x = 0;
// 파싱 단계에서 변수 add가 정의되고 함수의 코드 블록({})에 대한 참조가 할당됨
    // 파싱 단계에서 함수 구현 코드는 실행되지 않음
function add(a, b) {
    // 런타임에 이 코드 블록에 있는 지역 변수 c에 a + b 값이 할당됨
    var c = a + b;
    return c;
}

자바스크립트는 위 코드를 파싱하면서 코드에서 x와 add를 정의합니다.

전역 변수 x는 undefined로 초기화되고 add 변수(함수 변수)는 실행 코드가 담긴 함수의 참조로 초기화 됩니다.

파싱 단계를 마치고 런타임이 되면 이전에 정의된 변수와 함수를 사용해 작성한 문장이 실행됩니다.


그럼 이제 다음과 같은 코드를 살펴봅니다.

javascript
console.log(square(4)); // 16 출력
var square = 0;         // 파싱할 때 정의된 square 변수를 런타임에 덮어쓴다.
function square(x) {    // 함수 정의
    return x * x;
}
console.log(square);    // 0 출력


위 과정을 그림으로 그리면 다음과 같은 절차로 실행됩니다.


자바스크립트 프로그램 실행 단계



코드를 실행하면 파싱 단계에서 전역 변수인 square 와 함수인 square 가 정의됩니다.

먼저 square 변수가 정의(①)되고 다음으로 square 함수가 정의되면서 square 변수를 덮어(②)쓰게 됩니다.

자바스크립트에서는 변수 square와 함수 square를 메모리에 정의할 때 변수와 함수를 구분해서 별도로 관리하지 않습니다.

관리하는 장소가 동일하므로 이름이 같으면 덮어쓰게 됩니다.

따라서 최종적으로 함수를 가리키는 square 만 남게 되는 것입니다.

여기까지가 파싱 단계에서 일어나는 일입니다.


이제 코드가 실행되는 단계를 알아봅니다.

square 는 함수를 정의하고 있는 코드 블록을 가리키고 console.log(square(4)); 를 실행하면 출력(③)됩니다.

그러나  그 다음 문장인 var square = 0; 을 통해 square 가 가리키고 있는 메모리에는 0이 할당(④)됩니다.

따라서 마지막 문장인 console.log(square) 는 0을 출력(⑤)하게 되는 것입니다.


자바스크립트에서는 변수와 함수를 구분해서 관리하지 않습니다. 함수를 변수에 할당하든 변수를 함수에 할당하든 정상적인 작업입니다.

따라서 컴파일할 때 함수와 변수는 언제든지 다른 함수와 변수에 의해 내용이 바뀔 수 있기 때문에 주의를 기울여 코드 작성을 해야 할 것입니다.

물론 네임스페이스를 지정하여 코드를 작성하면 위와 같은 문제 소지를 피할 수 있습니다.

네임스페이스에 관한 내용은 다음에 다뤄보도록 하겠습니다.



Jaehee's WebClub