자바스크립트의 변수는 왜 선언도 안 했는데 참조가 될까요?

의심의 여지없이 현재 가장 많이 쓰이고 사랑받는 언어는 단연코 자바스크립트일 겁니다.

기술이 발전돼서 현재는 자바스크립트로 웹 애플리케이션도 만들고, 백엔드 API 개발도 가능하고, 심지어 모바일 애플리케이션도 만들 수 있습니다.

프로그래밍에 있어 자바스크립트의 이런 대유행에도 불구하고, 일반 사람들은 자바스크립트라고 하면 질색하는데요.

왜냐하면, 다른 프로그래밍 언어와는 다른 특이점이 있기 때문입니다.

저도 자바스크립트를 처음 배울 때 가장 헷갈렸던 게, 변수나 함수가 선언되기도 전에 액세스가 가능한 점이 가장 헷갈렸습니다.

아마도 이 글을 읽고 계신 자바스크립트 초보자분들도 이 점이 헷갈릴 수 있는데요.

이번 블로그에서는 이 점에 대해 왜 그런지 심도 있게 살펴보겠습니다.

Execution Context(실행 콘텍스트)

변수나 함수에 대해 생각해 보기 전에 먼저, 자바스크립트의 가장 기본적인 콘셉트부터 이해할 필요가 있습니다.

바로 실행 콘텍스트(execution context)인데요.

자바스크립트 코드를 이루는 가장 기본적인 실행 유닛은 뭐니 뭐니 해도 함수죠.

우리는 함수를 통해 뭔가를 계산하고, UI를 계산해서 다시 보여주고, 그리고 기존에 만들었던 코드를 다시 사용하게 할 수 있게 하고, 결국은 함수로 많은 걸 할 수 있는데요.

함수는 함수 안에서 다른 함수를 호출할 수 있고 호출한 함수에서도 다른 함수를 호출할 수도 있고, 또 계속 호출할 수 있습니다.

그리고 언젠가는 호출된 함수가 끝나면 호출한 함수로 코드 실행 위치가 다시 돌아가는데요.

예제를 통해 살펴보겠습니다.

const hello = name => {
	console.log("Hello, ", name);
}

const dialog = () => {
	hello("Squid");
}

dialog();

위 코드에서는 dialog 함수를 실행하면 그 안에서 또 hello 함수를 실행하고 있습니다.

hello 함수가 끝나면 다시 코드 실행 위치는 dialog 함수로 돌아가고 dialog 함수의 실행이 끝나면 다시 원래 위치로 돌아가는데요.

그러면, 자바스크립트 엔진은 어떻게 함수 실행 후 되돌아가는 위치를 정확히 기억하고 있을까요?

이걸 이해하기 전에 먼저 알아 두어야 할 게 있습니다.

자바스크립트 코드에서는 두 개의 메인 타입이 있습니다.

바로 글로벌 코드와 함수 코드인데요.

  • 글로벌 코드(Global code)는 함수 정의 바깥쪽에 정의된 코드를 말합니다.

  • 함수 코드(Function code)는 함수 안에 정의된 코드를 말하는데요.

자바스크립트 엔진에 의해 코드가 실행될 때 각각의 코드 명령문은 특정 실행 콘텍스트(execution context) 위에서 실행됩니다.

위에서 얘기한 두 개의 메인 코드에 따라 실행 콘텍스트(execution context)도 두 개가 존재합니다.

바로 글로벌 실행 콘텍스트(global execution context)와 함수 실행 콘텍스트(function execution context)입니다.

위 두 개의 실행 콘텍스트의 큰 차이점이라면 글로벌 실행 콘텍스트는 유일하게 한 개만 존재한다는 점입니다.

글로벌 실행 콘텍스트는 자바스크립트 코드가 제일 처음 실행될 때 생성됩니다.

그리고 함수 실행 콘텍스트는 당연히 함수가 호출될 때마다 생성되고요.

그래서 이 두 개의 실행 콘텍스트 상에서 자바스크립트가 코드를 실행하고, 잠시 멈추고 콜백 등 여러 가지 작업을 하고 그런 겁니다.

자바스크립트는 싱글 스레드에서 실행되는 모델인데요.

즉, 자바스크립트는 한 번에 한 개의 코드만 실행할 수 있다는 얘기입니다.

그래서 함수가 호출될 때마다 현재의 실행 콘텍스트는 잠시 멈추게 되고, 함수를 호출하는 위치에서 새로운 함수 실행 콘텍스트가 생성되게 됩니다.

그러고 나서 함수가 할 일을 다 하고 끝나게 되면 아까 만들어졌던 함수 실행 콘텍스트는 버려지게 되고 다시 예전의 실행 콘텍스트로 회귀하게 됩니다.

그래서 자바스크립트는 이 두 개의 콘텍스트를 추적 관리할 필요가 있는데요.

바로 스택을 통해 관리하고 있습니다.

렉시컬 환경(Lexical Environment)

실행 콘텍스트를 조금 이해했다면 이제는 렉시컬 환경을 이해해야 하는데요.

렉시컬(Lexical)의 뜻은 어휘적인 또는 사전적이라는 뜻인데요.

프로그래밍 측면에서 우리말로 번역하기 조금 애매합니다.

글을 계속 읽어 보시면 어느 정도 이해할 수 있을 겁니다.

다음과 같은 코드가 있다고 가정합시다.

const name = "SangWoo";

console.log(name);

위 코드의 경우 console.log 함수를 실행하면 새로운 함수 실행 콘텍스트가 생성된다는 걸 지금까지 살펴본 바로 알 수 있을 겁니다.

그런데, console.log 함수는 어떻게 name이라는 변수의 값을 얻을 수 있을까요?

console.log 함수가 name이라는 변수의 값을 얻는 과정을 자바스크립트에서는 identifier resolution이라고 하는데요.

기본적으로 변수가 어떤 참조를 가리키는지 찾아내는 과정이라고 생각하시면 됩니다.

이 identifier resolution 과정을 실행 콘텍스트는 렉시컬 환경에서 하는 겁니다.

identifier resolution은 지정자 참조?(해결)이라고 번역할 수 있는데요.

지정자를 찾는다는 의미로 참조가 더 적절한 표현 같기도 합니다.

렉시컬 환경은 자바스크립트의 내부 작동 구조인데요.

바로 특정 변수의 지정자(identifiers)를 매핑(mapping)하고 추적하는 내부 작동 구조라고 보시면 됩니다.

다시 위의 코드로 돌아가서 보면, 렉시컬 환경은 name 변수가 접근될 때 즉, console.log 함수가 선언됐을 때, 이때 렉시컬 환경에서 조언을 구하게 됩니다.

렉시컬 환경은 자바스크립트 스코프(scopes)의 내부 구현이라고 볼 수 있는데요,

그래서 보통 렉시컬 환경을 스코프(scopes)라고 부르기도 합니다.

일반적으로 렉시컬 환경은 특정 코드 구조와 조합을 이루는데요.

바로 함수, 코드 블록, 그리고 try-catch 블록이 그 특정 코드 구조가 될 수 있습니다.

그리고 각각의 코드 구조는 나름의 지정자(identifiers)의 매핑 구조로 되어 있습니다.

자바스크립트에서의 변수 선언

자바스크립트에서는 세 개의 예약된 문구로 변수를 선언할 수 있습니다.

바로 “var”, “let”, “const”입니다.

그리고 이 세 개의 변수 선언 문구는 변동성(Mutability)과 렉시컬 환경과의 관계에 따라 각각의 특징이 달라지는데요.

먼저, 변동성(Mutability)을 볼 때 “const”와 “var, let” 문구를 상반된 성격으로 볼 수 있습니다.

당연히 “const”는 변동성이 없는 변수를 선언할 때 쓰는 예약어이고요.

반대로 “var, let”으로 선언된 변수는 변동될 수 있습니다.

렉시컬 환경 관점 즉, 스코프(scopes) 관점에서 보면 “var”와 “let, const”가 각각 다른 특징이 있습니다.

우리가 “var” 예약어를 이용해서 변수를 정의하면 그 변수는 근처의 함수나 글로벌 렉시컬 환경 안에서 선언된다고 볼 수 있습니다.

다음 예제를 보시죠.

var name = "Ali";

const hello = () => {
	var message = "Hello";
	for (var i = 0; i< 4; i++ ) {
		var text = message + ' ' + name;
		console.log(text);
	}
	console.log(text);
}

hello();

자바스크립트를 처음 접하시는 분이 헷갈리시는 게 바로 위 코드에서 보면 “text” 변수인데요.

변수 선언 블록 밖에서도 그 변수를 참조할 수 있어 다른 프로그래밍을 하던 분들은 이해를 못 하실 거 같습니다.

왜냐하면 자바스크립트에 “var”로 변수를 선언하면 코드 블록(block)에 상관없이 그 변수는 가까운 함수나 글로벌 렉시컬 환경에 변수가 저장되기 때문입니다.

이런 헷갈리는 특성 때문에 자바스크립트 ES6에서는 “let”과 “const”로 변수를 선언할 수 있게 새롭게 개정되었습니다.

만약에 “let”이나 “const”를 이용해서 변수를 선언한다면 어떻게 될까요?

“var”와는 다르게 “let”과 “const”로 선언된 변수는 변수가 선언되는 곳의 가까운 렉시컬 환경 안에 존재하게 됩니다.

가까운 렉시컬 환경은 블록, 루프(loop), 함수 또는 글로벌 스코프가 될 수 있습니다.

그래서 위 코드를 “let”과 “const”를 사용하는 형태로 바꿔보면 다음과 같습니다.

const name = "Ali";

const hello = () => {
	const message = "Hello";
	for (var i = 0; i< 4; i++ ) {
		let text = message + ' ' + name;
		console.log(text);
	}
}

hello();

위와 같이 코드를 짜면 “text” 변수는 “text” 변수가 선언된 for-loop 외부에서는 참조가 안 됩니다.

그리고 message 변수도 hello 함수 외부에서는 접근할 수 없습니다.

자바스크립트라는 언어를 만든 목적이 프로그래머가 좀 더 쉽게 프로그램을 짤 수 있게 도와주려고 만든 것인데요.

그래서 자바스크립트에서는 함수 리턴 타입도 안 적어도 되고, 파라미터 타입도 안 적어도 되고, 변수 타입도 안 적어도 됩니다.

또 자바스크립트의 코드는 한 줄 한 줄 실행되는데요.

다음 코드를 보고 얘기를 이어 나가도록 하겠습니다.

hello('Jason');

function hello(name) {
	console.log("hello", name);
}

자바스크립트는 코드 한 줄 한 줄 실행한다고 하는데, 왜 hello 함수 선언이 나오기 전에 hello 함수를 호출(실행)할 수 있을까요?

이걸 이해하기 위해서는 자바스크립트 실행이 어떤 형태로 이루어지는지 이해할 필요가 있습니다.

자바스크립트는 두 가지 단계(phase)로 실행되는데요.

첫 번째 단계(phase)는 새로운 렉시컬 환경이 생성되면서, 이때 코드 자체는 실행되지는 않지만, 자바스크립트 엔진은 모든 코드를 훑어보고, 모든 변수, 모든 함수를 현재 렉시컬 환경에 등록하게 됩니다.

두 번째 단계(phase)는 첫 번째 단계가 끝나고 자바스크립트가 실행되는 단계로 이때 변수의 생선 방법(”var”인지, “let”, “const”인지), 그리고 환경 타입이 글로벌인지, 함수인지 블록 안인지에 따라 자바스크립트 실행 행태가 변할 수 있습니다.

다음 코드를 보시죠.

console.log(name);

var name = "Robin";

console.log(name);

이 코드 예제에서는 console.log 함수가 undefined를 출력할 겁니다.

왜냐하면 자바스크립트의 첫 번째 단계(phase)에서 “var”로 선언된 변수 name은 먼저 undefined로 정의되고 렉시컬 환경에 저장되기 때문입니다.

그래서 코드 첫 번째 줄에서는 undefined가 출력되고 마지막 console.log 함수에서는 정상적으로 해당 값이 출력될 겁니다.

만약, “let”이나 “const”로 변수를 선언했다면 첫 번째 줄에서 ReferenceError가 발생할 겁니다.

왜냐하면 “let”이나 “const”로는 undefined로 변수가 초기화되지 않고 단순히 변수의 메모리만 확보하기 때문입니다.

지금까지 왜 자바스크립트 이상한 취급을 받는지에 대해 실행 콘텍스트와 렉시컬 환경을 통해 그 이유를 살펴보았는데요.

자바스크립트도 ES6로 넘어오고 타입스크립트가 일반화되면서 위와 같은 오해를 안 받고 좀 더 멋진 언어가 되고 있습니다.

필자도 최근에는 타입스크립트만 쓰는 중이니까요.

그럼.