본문 바로가기

0/javascript

Learning JavaScript 요약 정리 (2) 7장 (스코프)

반응형

7장 스코프

스코프는 변수와 상수, 매개변수가 언제 어디서 정의되는지 결정한다.

가시성(visibility) 이라고도 불리는 스코프는 프로그램의 현재 실행 중인 부분, 즉 실행 컨텍스트에서 현재 보이고 접근할 수 있는 식별자들을 말한다. 반면 존재한다는 말은 그 식별자가 메모리가 할당된(예약된) 무언가를 가리키고 있다는 뜻이다.

정적 스코프와 동적 스코프

자바스크립트의 스코프는 정적이다. 소스 코드만 봐도 변수가 스코프에 있는지 판단할 수 있다는 뜻이다. 다만, 소스 코드만 봐도 즉시 스코프를 분명히 알 수 있다는 뜻은 아니다.

정적 스코프는 어떤 변수가 함수 스코프 안에 있는지 함수를 정의할 때 알 수 있다는 뜻이다. 호출할 때 알 수 있는 것은 아니다.

const x = 3;

function f(){
	console.log(x);
	console.log(y);
}

// 새 스코프
{
	const y = 5;
	f();
}

변수 x는 함수 f를 정의할 때 존재하지만, y는 그렇지 않다. y는 다른 스코프에 존재한다. 다른 스코프에서 y를 선언하고 그 스코프에서 f를 호출하더라도, f를 호출하면 x는 그 바디 안의 스코프에 있지만 y는 그렇지 않다. 이것이 정적 스코프이다. 함수 f는 자신이 정의될 때 접근할 수 있었던 식별자에게 여전히 접근할 수 있지만, 호출할 때 스코프에 있는 식별자에 접근할 수는 없다.

자바스크립트의 정적 스코프는 전역 스코프와 블록 스코프, 함수 스코프에 적용된다.

전역 스코프

스코프는 계층적이며 트리의 맨 아래에는 바탕이 되는 무언가가 있어야 한다. 즉, 프로그램을 시작할 때 암시적으로 주어지는 스코프가 필요하다. 이 스코프를 전역 스코프라고 한다. 자바스크립트 프로그램을 시작할 때, 즉 어떤 함수도 호출하지 않았을 때 실행 흐름은 전역 스코프에 있다. 바꿔 말해, 전역 스코프에서 선언한 것은 무엇이든 프로그램의 모든 스코프에서 볼 수 있다.

블록 스코프

let과 const는 식별자를 블록 스코프에서 선언한다. 블록 스코프는 그 블록의 스코프에서만 보이는 식별자를 의미한다.

console.log('before block');
{
	console.log('inside block');
	const x = 3;
	console.log(x); // 3
}
console.log(`outside block; x=${x}`); // ReferenceError: x는 정의되지 않았습니다.
{
	// 외부 블록
    let x = {color: "blue"};
    let y = x; // y와 x는 같은 객체를 가리킨다.
    let z = 3;
    {
    	// 내부 블록
        let x = 5; // 바깥의 x는 가려졌다.
        console.log(x); // 5
        console.log(y.color); // "blue"; y가 가리키는, 외부 스코프 x가 가리키는 객체는
        					// 스코프 안에 있다.
        y.color = "red";
        console.log(z); // 3
    }
    console.log(x.color); // "red"; 객체는 내부 스코프에서 수정됐다.
    console.log(y.color); // "red"; x와 y는 같은 객체를 가리킨다.
    console.log(z); // 3
}

* 외부 스코프에 있는 같은 이름의 변수에 그늘이 진 듯 만든다는 의미에서 변수 숨김을 변수 섀도우라고 부르기도 한다. 변수를 숨기면 그 변수는 해당 이름으로는 절대 접근할 수 없다.

스코프의 계층적인 성격 때문에 어떤 변수가 스코프에 있는지 확인하는 스코프 체인이란 개념이 생겼다. 현재 스코프 체인에 있는 모든 변수는 스코프에 있는 것이며, 숨겨지지 않았다면 접근할 수 있다.

함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 경우가 많다. 이런 것을 보통 클로저라고 부른다. 스코프를 함수 주변으로 좁히는 것이라고 생각해도 된다.

let globalFunc; // 정의되지 않은 전역 함수
{
	let blockVar = 'a'; // 블록 스코프에 있는 변수
	globalFunc = function(){
		console.log(blockVar);
	}
}
globalFunc(); // 'a'

globalFunc을 호출하면, 이 함수는 스코프에서 빠져나왔음에도 불구하고 blockVar에 접근할 수 있다. 일반적으로 스코프에서 빠져나가면 해당 스코프에서 선언한 변수는 메모리에서 제거해도 안전하다. 하지만 여기서는 스코프 안에서 함수를 정의했고, 해당 함수는 스코프 밖에서도 참조할 수 있으므로 자바스크립트는 스코프를 계속 유지한다.

즉, 스코프 안에서 함수를 정의하면 해당 스코프는 더 오래 유지된다. 또 일반적으로는 접근할 수 없는 것에 접근할 수 있는 효과도 있다.

let f; // 정의되지 않은 함수
{
	let o = { note: 'Safe' };
	f = function(){ return o; }
}
let oRef = f();
oRef.note = "Not so safe after all";

일반적으로는 스코프 바깥쪽에 있는 것들에는 접근할 수 없다. 함수를 정의해 클로저를 만들면 접근할 수 없었던 것들에 접근할 방법이 생긴다.

즉시 호출하는 함수 표현식 (IIFE)

IIFE는 함수를 선언하고 즉시 실행한다. 스코프와 클로저에 대해 충분히 이해했으니 왜 IIFE를 사용해야 하는지 이해할 수 있을 것이다.

(function(){
	// IIFE 바디
})()

함수 표현식으로 익명 함수를 만들고 그 함수를 즉시 호출한다. 이의 장점은 내부에 있는 것들이 모두 자신만의 스코프를 가지지만, IIFE 자체는 함수이므로 그 스코프 밖으로 무언가를 내보낼 수 있다는 것이다.

const message = (function(){
	const secret = "I'm a secret!";
	return `The secret is ${secret.length} characters long.`;
})();
console.log(message);

변수 secret은 IIFE의 스코프 안에서 안전하게 보호되며 외부에서 접근할 수 없다. IIFE는 함수이므로 무엇이든 반환할 수 있다.

함수 스코프와 호이스팅

ES6에서 let을 도입하기 전에는 var를 써서 변수를 선언했고, 이렇게 선언된 변수들은 함수스코프라 불리는 스코프를 가졌다.

// let을 사용하는 경우
x; // ReferenceError: x는 정의되지 않았다.
let x = 3; // 에러가 일어나서 실행이 멈췄으므로 여기에는 결코 도달할 수 없다.

// var를 사용하는 경우
x; // undeinfed
var x = 3;
x; // 3 

변수를 선언하지도 않았는데 그 변수에 접근할 수 있다는 건 이해할 수 없는 일이다. var로 선언한 변수는 끌어올린다는 뜻의 호이스팅이라는 매커니즘을 따른다. 자바스크립트는 함수나 전역 스코프 전체를 살펴보고 var로 선언한 변수를 맨위로 끌어올린다. 

* 선언만 끌어올려진다, 할당은 끌어올려지지 않는다.

var x; // 선언이 끌어올려진다.
x; // undefined
x = 3;
x; // 3

함수 호이스팅

var로 선언된 변수와 마찬가지로, 함수 선언도 스코프 맨 위로 끌어올려진다. 따라서 함수를 선언하기 전에 호출할 수 있다.

f(); // 'f'
function f(){
	console.log('f');
}

변수에 할당된 함수 표현식은 끌어올려지지 않는다. 이들은 변수의 스코프 규칙을 그대로 따른다.

f(); // ReferrenceError : f는 정의되지 않았다.
let f = function(){
	console.log("f');
}

 

이 글은 이선 브라운. n.d. Learning JavaScript. n.p.: 한빛미디어(주).를 참고하여 정리한 글입니다. 

반응형