[JavaScript] 8. Scope

100tick·2022년 12월 20일
0

JavaScript Deep Dive

목록 보기
8/16
post-thumbnail

1. Local, Gloal Scope

코드의 가장 바깥 영역은 Global Scope, 블록 안의 영역은 Local Scope가 된다.
블록 안으로 들어갈수록 하위 지역이 되며, 각 변수들은 자신의 지역과 하위 지역에서 유효하다.
상위 지역의 변수와 같은 이름을 가진 변수가 있고, 해당 변수를 참조한다면 가장 가까운 범위의 변수가 채택된다.
아래 예시를 보면 쉽게 이해할 수 있다.

let x = "global";

{
	let x = "local";
	console.log(x); // "local"
	{
		let x = "local2";
		console.log(x); // "local2"
    }
}
console.log(x); // "global"

2. Scope Chain

함수는 전역 또는 함수 내부에서 정의될 수 있다.
다른 함수 내부에서 정의된 함수를 중첩 함수, 그 중첩 함수를 포함하는 함수를 외부 함수라고 한다.

함수가 중첩될 수 있으므로, Local Scope도 중첩될 수 있다.
이는 Scope가 함수의 중첩에 의해 계층적 구조를 갖는다는 것을 의미한다.

// global scope: [x, y, outer]
let x = 1;
let y = 2;
function outer() {
	// outer scope: [z, inner]
	let z = 3;
  	function inner() {
    	// inner scope: [o]
    	let o =4;
    }
}

inner scope: [o] -> outer scope: [z, inner] -> global scope: [x, y, outer]
위와 같이 하위 Scope에서 상위 Scope로 연결된 것을 Scope Chain이라고 하며, JS 엔진은 변수를 참조할 때 이런 식으로 Scope를 거슬러 올라가며 검색한다.
이를 통해 상위 Scope의 변수를 하위 Scope에서 참조할 수 있으며, 변수명이 같은 경우에 가장 가까운 지역 변수가 사용되는 것이다.


3. Function Level Scope

var x = 1;

if (true) {

  	var x = 10; 
}

x; // 10

let, const로 생성된 변수는 Block Level Scope이므로, if, for, while 등의 Block 안에서 Local Scope가 생성된다.

그러나 var 로 생성된 변수는 Function Level Scope기 때문에 오직 Function Block만을 Block으로 인정한다.

그래서 위 코드에서 if 내의 x가 지역 변수임에도 불구하고, Function Block이 아니므로 전역 변수를 덮어버리는 것이다.


4. Lexical Scope

함수의 상위 Scope를 결정하는 방식은 Dynamic, Lexical 2가지가 존재한다.

Dynamic Scope는 함수가 호출되는 시점에 동적으로 상위 Scope를 결정한다.
Lexical Scope는 함수 정의가 평가되는 시점에 상위 Scope를 결정한다.

JS를 비롯한 대부분의 프로그래밍 언어는 Lexical Scope를 따른다.

고로 함수가 호출된 위치는 상위 Scope 결정에 어떠한 영향도 미치지 않는다.
즉, 함수의 상위 Scope는 언제나 자신이 정의된 Scope다.

아래의 코드를 살펴 보자.

var x = 1;

function fn1() {
	var x = 10;
  	fn2();
}

function fn2() {
	console.log(x); 
}

fn2가 어디서 호출되었는지는 상관이 없다.
fn2가 전역 변수에 정의되었으므로, x는 호출 되는 Scope인, fn1의 지역 변수가 아닌, 생성된 Scope인 전역 변수 x, 1을 출력한다.

그래서 함수를 처음 생성할 때의 상위 Scope를 기억해야 할 필요가 있다.
함수가 호출될 때마다 이를 참조할 것이기 때문이다.


5. Lifecycle of Local Variables

변수는 선언에 의해 생성되고 할당에 의해 값을 갖는다.
그리고 더 이상 사용이 되지 않을 때(Scope를 벗어난 이후) drop된다.

이 주기를 Lifecycle이라고 한다.

function fn() {
	let a = "local";
  	console.log(a); // "local"
	return a;
}

fn();
console.log(a); // ReferenceError: a is not defined

afn 함수 내부에서 생성되고 함수가 종료될 때 반환된다.
그러나 반환된 값을 어떤 변수에도 할당하지 않았기 때문에 함수 종료 후 접근할 수 있는 identifier가 없기 때문에 console.log에서 a로 접근할 수 없으며, 이후 drop될 것이다.

만약 fn으로부터 반환 받은 값을 Global Scope에서 변수 x에 할당한다고 가정해보자.
let x = fn();와 같은 statement가 될텐데, 이는 지역 변수 a가 갖고 있던 값을 x가 갖게 될 새로운 메모리 공간에 copy하는 것이기 때문에 a는 소멸되고 x라는 새로운 변수가 생긴다고 볼 수 있다.

즉, 지역 변수 a는 함수 fn의 종료와 함께 사실상 소멸된다.(더 이상 접근이 불가능하므로)
따라서 지역 변수의 Lifecycle은 함수의 생성, 소멸 시점과 같다고 볼 수 있겠다.

var x = "global";

function fn() {
	console.log(`x: ${x}`); // "x: undefined"
	var x = "local";
}

fn();
console.log(x); // "global"

fn 함수 내부의 console.logx를 주의 깊게 살펴보자.
지역 변수 x 선언 전에 x를 참조했으므로 전역 변수 x를 참조하여 "x: global"이 출력될 것이라고 예상할 수 있겠지만, Local Scope 내부에 동일한 이름을 가진 x가 존재하기 때문에, 미리 JS 엔진에 의해 지역 변수 x는 함수 Scope가 시작될 때 이미 undefined로 초기화 된 상태다.

Scope Chain에서 가장 가까운 Local Scopex가 어쨌던 undefined로 존재하므로 그것을 참조하는 것이다.

좀 더 자세히 알아보면, JS의 변수는 Hoisting되어 항상 맨 위에 위치하게 되는데, 그것이 Scope 단위로 이루어지기 때문이다.

function fn() {
	// var a = b = c = undefined -> Hoisting에 의해 Scope 맨 위에서 undefined로 초기화

	var a = b = c = 1;
}

6. Lifecycle of Global Variables

함수 내의 코드는 반드시 호출해야 실행되지만, Global Scope에 작성된 코드는 호출 없이 실행된다.
함수는 return문을 마주하거나 함수 Block에 있는 모든 코드를 실행한 뒤 종료되며, Global Scopepanic 이 일어나거나 무한 루프에 걸리지 않으면 끝까지 실행하고 종료된다.

var로 선언한 전역 변수는 Global ObjectProperty가 된다.
이는 전역 변수가 Global Object의 생명 주기와 일치함을 뜻한다.


7. Problems of Global Variables

대부분의 서적에서 변수에 대해 설명할 때, 유효 범위를 최대한 작게 만들라는 조언을 한다.
변수의 범위가 작아질수록 각 변수의 Lifetime이 짧아지기 때문에, 사용이 끝난 이후 최대한 빠르게 Lifetime이 끝난 변수를 drop시킬 수 있기 때문이다.

전역 변수는 지역 변수보다 훨씬 Lifetime이 길기 때문에 메모리 공간을 훨씬 오랫동안 차지할 것이며, 모든 하위 Scope에서 접근이 가능하기 때문에 의도치 않게 재할당이 일어나는 등의 실수가 발생할 가능성도 높다.

게다가 Scope Chain의 종점에 존재하기 때문에, 최하위 Scope부터 거슬러 올라가며 검색을 해야 하기 때문에 변수 검색 속도도 가장 느리다.(큰 차이는 아니지만)

위와 같은 이유로 전역 변수는 가능하다면 사용하지 않는 것이 좋다.
몇가지 전역 변수의 사용을 억제하는 기법에 대해서 알아보도록 하자.

7.1 IIFE(Immediately Invoked Function)

이미 봤던 내용이기에 이제는 조금 익숙할 것이다.

(function() {
	let a = 10; // local variable
}());

(function fn() {
	let b = 10; // local variable
}());

console.log(a); // ReferenceError: a is not defined
console.log(b); // ReferenceError: b is not defined

그룹 연산자 () 안에 선언된 함수는 JS 엔진에 의해 expression으로 평가되기 때문에 반환과 동시에 할당이 일어나지 않으면 해당 함수에 접근할 수 있는 identifier가 존재하지 않는 dangling reference가 되고, drop되는 것을 확인했었다.

Global Scope에서 이런식으로 사용하면 함수의 실행은 일어나지만, 함수 종료와 함께 Lifetime이 종료되므로, 자원 낭비를 최소화 할 수 있을 것이다.

7.2 Namespace Object

위 방법이 Global Scope에서 자원 낭비를 줄이는 효과가 있었다면, 이 방식은 Global Scope에 선언된 변수가 예기치 못하게 변경되는 실수를 막을 수 있는 효과가 있다.

let obj = {}; // global namespace object

obj.name = "A";

console.log(obj.name); // "A"

objGlobal Scope에 생성하고, 그 안에 필요한 값들을 하위 Property로 작성한다.
만약 변수들을 Global Scope에 펼쳐서 생성했다면 덮어 쓰지 않게 주의해야 할 변수명들이 많이 생기지만, 이렇게 하나의 object 안에 Property로서 존재한다면, global namespace objectobj 하나만 주의하면 되기 때문에 실수가 발생할 가능성을 훨씬 줄일 수 있을 것이다.

단, Lifetime은 그대로 유지되기 때문에 자원 낭비는 막을 수 없다.

0개의 댓글