자바스크립트 스코프, 호이스팅 정리 - 기초부터 완성까지, 프런트엔드 5장

khakiD·2022년 7월 8일
0
post-thumbnail

5장 목차

  1. 프로토타입
  2. 스코프
  3. 호이스팅
  4. 클로저
  5. 모듈

TL;DR


5.2. 스코프 (Scope)

  • 스코프(scope)는 변수나 매개변수에 접근할 수 있는 범위를 결정
  • 자바스크립트의 스코프는 함수블록 단위의 스코프로 나뉘며, 함수나 블록의 선언 위치에 따라 중첩 스코프를 정의
  • 스코프의 종류와 렉시컬 스코프 규칙, 스코프 체인을 통한 검색 방법
  • 자바스크립트의 호이스팅의 동작 방식과 모듈 스코프 정의와 사용법 정리

5.2.1. 함수 스코프와 블록 스코프

가장 흔하게 사용되는 함수 스코프부터 알아보자.

5.2.1.1. 함수 스코프와 var

  • 선언된 함수 단위로 생성되는 스코프
  • 함수 안에 선언된 변수는 모두 함수 스코프에 포함
function foo() { // foo() 함수 스코프가 생성됨
	var a = 1; // foo() 함수 스코프에 포함되는 변수 a
	function bar(b) { // foo() 함수 스코프에 포함되는 함수 bar()
		console.log(a, b); // 1, 2
	}
	bar(2);
}
foo();
function foo() { // foo() 함수 스코프가 생성됨
	if (true) {
		var a = 1;  // var 키워드로 생성된 변수 a
	}
	console.log(a); // 1
}
foo();
  • var 키워드로 선언한 변수 a함수 스코프를 따르기에 조건문 블록을 무시하고 함수 몸체 안에 접근할 수 있다.
  • 외부에서 조건문 안의 변수에 접근하는 경우는 거의 없으며 좋은 코드도 아니다.
  • 블록 단위의 스코프로 변수를 선언하는 습관을 들이자.

5.2.1.2. 블록 스코프와 let, const

  • 블록 스코프에서는 변수의 유효 범위를 블록({}) 단위로 제한하여 사용한다.
  • let, const로 선언된 변수는 블록 스코프를 따른다. → 함수 스코프 문제를 해결할 수 있다.
function foo() {
	if (true) {
		const a = 1;
	}
	console.log(a); // Uncaught ReferenceError: a is not defined
}
foo();

const 키워드로 선언된 a 변수는 해당 블록 안에서만 유효하므로 블록을 벗어나서는 접근할 수 없다. varlet, const는 스코프를 통해 확실히 구분할 수 있다. 책에서 계속해서 강조하는 부분은 변수는 블록 스코프를 갖도록 선언하는 것이 직관적이며 버그를 줄일 수 있다.

5.2.2. 렉시컬 스코프

자바스크립트만 그런 것이 아니라 대부분의 프로그래밍 언어가 동적 스코프와 렉시컬 스코프 두 가지 방식으로 동작하는데, 동적 스코프는 함수의 호출에 의해 결정되며 렉시컬 스코프는 변수나 함수를 어디에 작성하였는가에 따라 결정된다. 대부분의 현대 프로그래밍 언어들은 렉시컬 스코프 규칙을 따르며 자바스크립트 또한 마찬가지이다.

this 바인딩스코프를 착각하지 말자. 자바스크립트는 코드가 작성된 문맥에 따라 정적으로 결정되는 렉시컬 스코프를 따르지만, this 바인딩은 함수를 호출하는 방법에 따라 동적으로 달라진다는 것!

function foo() {    // 1. 전역 스코프
	var a = 1;        // 2. 함수 스코프 foo()
	function bar(b) { // 3. 함수 스코프 bar(b)
		console.log(a, b); // 1, 2
	}
	bar(2);
}
foo();

위 예제에 중첩되어 있는 3개의 스코프를 알아보자.

  1. 전역 스코프 - foo() 함수
  2. 함수 스코프(foo()) - a 변수, bar() 함수
  3. 함수 스코프(bar()) - b 매개변수

📌 console.log(a, b) 코드가 변수 a를 검색하는 과정

  1. console.log() 메소드에서 참조된 변수 a를 찾기 위해 bar() 함수 스코프부터 검색을 시작
  2. bar() 함수의 스코프에서는 변수 a를 찾을 수 없으므로 가장 가까운 상위 스코프 foo() 함수 스코프로 올라가 검색
  3. foo() 함수 스코프에서 변수 a를 찾아 사용하며 검색 중단

📌 주의할 점

  • 스코프는 함수를 어디서 작성했는가에 따라 경계를 가진다. → 렉시컬 스코프의 규칙에 따라!
  • 동일한 경계를 가진 스코프는 존재할 수 없다.
  • 내부 스코프에서 상위 스코프로 올라가며 검색하기에 상위 스코프에서는 내부 스코프의 변수나 함수에는 접근할 수 없다.

이러한 스코프들의 연결 관계를 ✨스코프 체인(scope chain)이라 하며, 이를 따라 검색하는 과정을 ✨스코프 체이닝(scope chaining)이라 한다.

자바스크립트에서는 with문eval() 함수를 사용하여 렉시컬 스코프를 동적으로 변경할 수 있다. 동적 스코프 변경은 성능을 저하시키며 보안상 문제를 야기할 수 있다는 점을 알자. 두 방법 모두 사용하지 않는 것이 권장된다.

📝 함수 스코프와 블록 스코프는 스코프의 단위이며, 렉시컬 스코프는 이 스코프들의 범위를 결정하는 규칙이다. 헷갈리지 않도록 하자.


5.3. 호이스팅 (hoisting)

호이스팅이란 선언문이 스코프 내의 가장 최상단으로 끌어올려지는 것을 의미한다. 자바스크립트하면 호이스팅이라는 단어를 들어보지 않을 수가 없을 정도로 유명한 특징이지 뭐…

console.log(a); // undefined
var a = 1;

// 위 코드는 아래 코드처럼 처리된다.
// **스코프의 최상단에서 변수 a의 선언과 초기화가 한 번에 실행된다.**

var a;
console.log(a); // undefined
a = 1;

📌 자바스크립트의 변수 생성의 세 가지 단계

  • 선언 : 스코프에 변수를 선언한다.

  • 초기화 : 변수의 값을 undefined로 초기화하며, 실제로 변수에 접근 가능한 단계이다.

  • 할당 : 할당문을 만나면 변수에 실제 값을 할당한다.

    var 키워드로 선언한 변수는 먼저 스코프의 최상단으로 끌어올려진 뒤, 선언과 초기화 돤계를 한 번에 실행한다. 때문에 선언하기 전에 변수에 접근하여도 이미 초기화되어 접근이 가능하다. 이것이 호이스팅이다.

5.3.1. 스코프별로 동작하는 호이스팅

function foo() {
	console.log(a);
	var a = 1;
}

// 위 코드는 아래 코드처럼 처리된다.

function foo() {
	var a; // **선언** + **초기화가 같이 됨!**
	console.log(a); // 변수에 접근이 가능
	a = 1; // **할당**
}

var로 선언한 변수는 선언과 초기화가 한번에 같이 실행된다. 때문에 선언하기 전에 변수에 접근해도 이미 초기화되어 접근이 가능하다는 것이다. 근데 letconst로 선언한 변수는 다르다.

5.3.1.1. let과 const

letconst로 선언한 변수는 var로 선언한 변수와 다르게 초기화 단계가 분리되어 실행된다. 선언 단계는 스코프의 최상단으로 끌어올려져 실행되지만, 초기화 단계는 선언문을 만나면 실행된다. 초기화 단계 이전에 변수에 접근하려하면 ReferenceError가 발생한다.

📌 선언 단계가 실행되는 스코프의 최상단부터 초기화 단계를 실행하는 선언문이 나오기 전까지는 변수에 접근할 수 없다. 이 구간은 ✨TDZ(Temporal Dead Zone)이라고 부른다.

뭔 소리여?
단계로 따지면...

console.log(a); // Uncaught ReferenceError: a is not defined
let a = 1;

let 키워드로 선언된 변수 a를 초기화 단계를 실행하기 전인 TDZ 구간에서 참조하였기에 에러가 발생한다.

let a = 1; 라인에서 변수 a선언은 위로 끌어올려졌지만 초기화는 안되어있는 상태로 첫 줄인 console.log(a);가 실행된다는 거다.

얘네는 선언 전에는 접근이 안된다는 거임.

5.3.1.2. 선언은 끌어올려진다.

🤷‍ 그렇지만 선언 단계는 스코프의 최상단으로 끌어올려져 실행된다.

let a = 1;

function foo() {
	console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
	let a = 1;
}
foo();
  • 위 예제에서 foo() 함수는 함수 스코프 내에 a 변수부터 검색한다.
  • 함수 스코프 내 a 변수의 선언 단계는 foo() 함수 스코프 최상단에서 실행이 된다.
  • 이 지점부터 선언문(let a = 1;)을 만나기 전까지 TDZ 구간이 생성된다.
  • 이 TDZ 구간에서 내부 a 변수에 접근하여서 에러가 발생한 것.

실제로 let과 const의 호이스팅은 논란이 많은 부분이라고 한다. 선언 단계는 최상단으로 끌어올려지니 let과 const도 호이스팅이 발생한다고 주장하는 개발자들이 있고, 초기화 단계가 실행되지 않아 변수에 직접 접근이 불가능하니까 호이스팅이 발생하지 않는다고 주장하는 개발자도 있다. 이는 ECMAScript 명세에도 명확하게 내용이 없어 의견이 분분하다.

😒 아무튼간에 let과 const의 선언 단계도 최상단으로 끌어올려져 실행된다는 것은 팩트다.

근데 왜 선언은 위로 올려지도록 설계한거지...?

5.3.2. 함수 선언문의 호이스팅

함수 선언문도 선언문이니 호이스팅이 발생한다. 다른 점은 함수 선언문은 선언, 초기화, 할당 모두 스코프 최상단에서 실행되므로 어느 위치에서든 함수를 사용할 수 있다는 점이다.

📌 함수 선언문이 아닌 함수 표현식은 변수의 호이스팅 규칙에 따라 동작한다.

(이건 뭐 직관적이니까 의문이 생기진 않는다.)


✅ 호이스팅 우선순위

  • 변수 선언이 함수 선언보다 높은 우선순위를 가진다!
profile
(이해 못했음) (개인 블로그로 이전)

0개의 댓글