자바스크립트 코드의 유효 범위 Scope
Scope
스코프는 변수에 접근 가능한 범위를 의미합니다. 자바스크립트에서 스코프는 함수 레벨 스코프와 블록 레벨 스코프 두 가지로 나뉘며, 함수나 블록을 선언한 위치에 따라 스코프가 중첩될 수 있습니다. 스코프는 정말 중요한 개념 중 하나이며, 클로저와 같은 문법들의 동작을 이해할 때 도움이 되니 알아두면 좋습니다.
함수 레벨 스코프
함수 레벨 스코프는 자바스크립트에서 흔히 만날 수 있는 스코프입니다.
function foo() {
var a = 1;
function bar(b) {
if (true) {
var c = 5;
}
// 8
console.log(a + b + c);
}
bar(2);
}
foo();
함수를 정의하면 그에 맞춰 함수 단위로 스코프가 결정됩니다. 그리고 var 키워드로 선언한 변수의 유효 범위는 함수 레벨 스코프를 따르기 때문에 조건문 안에서 변수를 만들었어도, 스코프를 생성하지 않아 블록 밖에서 접근 가능합니다. 그렇기에 직관적이지 않아 이런 경우에는 블록 레벨 스코프를 생성하는 문법을 사용하는 것이 좋습니다.
블록 레벨 스코프
블록 레벨 스코프는 스코프를 블록 단위({})로 나누는 방식입니다.
function foo() {
let a = 1;
function bar(b) {
if (true) {
const c = 5;
}
// ReferenceError: c is not defined
console.log(a + b + c);
}
bar(2);
}
foo();
let과 const 키워드로 선언된 변수는 블록 레벨 스코프를 따르며, 변수의 유효 범위가 직관적이기에 버그가 발생할 수 있는 위험을 사전에 방지할 수 있습니다.
Lexical Scope
프로그래밍 언어의 스코프는 동적 스코프(Dynamic Scope)와 렉시컬 스코프(Lexical Scope) 두 가지 방식으로 결정됩니다. 동적 스코프는 함수가 호출될 때 결정되는 방식이고, 렉시컬 스코프는 함수가 정의된 위치에 의해 결정되는 방식입니다. 자바스크립트는 렉시컬 스코프의 규칙을 따르고 있습니다.
자바스크립트의 스코프 생성 규칙이 동적 스코프를 따른다 생각할 수 있는데, 이는 this 바인딩과 혼동을 해서 생기는 오해입니다. this 바인딩은 함수 호출 방식에 의해 결정되기에 동적이지만, 스코프는 정의된 위치에 의해 정적으로 결정됩니다. 👏
var a = 1;
function x() {
var b = 2;
function y(c) {
// 6
console.log(a + b + c);
z(4);
}
y(3);
}
function z(d) {
// 5
console.log(a + d);
}
x();
예제 코드를 실행하면 다음 그림처럼 스코프가 생성됩니다.
코드를 실행하면 전역 스코프가 생성되고, 함수가 정의된 위치에 따라 스코프가 중첩됩니다. console.log 메서드가 실행되면서 인자를 하나씩 참조하기 시작하고, 하위에서 상위 스코프로 이동하며 식별자를 탐색합니다. 이러한 탐색을 스코프 체이닝이라 하며, 탐색 순서가 이렇기 때문에 상위 스코프에서는 하위 스코프 내부를 참조할 수 없습니다.
이처럼 자바스크립트는 렉시컬 스코프 규칙에 의해 스코프를 함수 또는 블록 단위로 생성합니다. 😆
클로저
클로저(Closure)는 함수의 렉시컬 스코프를 기억하는 특별한 함수이며, 이를 참조할 수 없는 외부 스코프에서도 참조할 수 있도록 합니다. 함수를 강력하게 활용할 수 있는 문법이며, 렉시컬 스코프와 스코프 체이닝을 이해하고 있다면 쉽게 사용할 수 있습니다.
다음 코드의 foo 함수는 실행이 끝나면 스코프도 함께 사라집니다.
function foo(x) {
function bar(y) {
// 30
console.log(x + y);
}
bar(20);
}
foo(10);
foo 함수 내의 변수는 foo 함수의 스코프 안에서만 사용 가능하기에, 외부에서는 참조가 불가능합니다. 그렇다면 이 코드를 클로저로 바꿔 보면 어떻게 될까요?
function foo(x) {
return function bar(y) {
console.log(x + y);
};
}
const adder10 = foo(10);
adder10(20); // 30
이 코드는 foo 함수와 스코프 체인이 연결된 bar 함수를 반환하여, adder10 변수에 할당합니다. 이후 adder10 변수를 사용하여 bar 함수를 호출하고, 스코프 체이닝을 통해 foo 함수의 스코프에서 x의 값을 탐색합니다. 이와 같이 내부 함수가 자신을 감싸고 있는 함수를 벗어나 독립적인 스코프에서 실행되는 경우를 클로저라 부릅니다. 독립적인 스코프에서 실행되지만 참조하는 것은 기존에 정의된 위치에서의 렉시컬 스코프입니다.
클로저를 활용하면 프로퍼티나 메서드를 캡슐화하여 모듈처럼 안전하게 사용할 수 있습니다.
function counter() {
let counter = 0;
function increment() {
counter++;
}
function decrement() {
counter--;
}
function getCount() {
return counter;
}
return {
increment,
decrement,
getCount,
};
}
const myCounter = counter();
myCounter.increment();
myCounter.increment();
console.log(myCounter.getCount()); // 2
이렇게 내부의 프로퍼티나 메서드를 숨기는 코드의 패턴을 모듈 패턴이라 합니다. 지금은 모듈 문법이 존재하기 때문에 사용하지는 않지만, 모듈 패턴은 클로저를 활용한 대표적인 예시이므로 숙지해 두면 좋습니다.
References
'🌈 기술스택 > JavaScript' 카테고리의 다른 글
코드 실행에 필요한 정보를 담은 실행 컨텍스트 (0) | 2021.10.05 |
---|---|
자기 자신을 가리키는 값 this (0) | 2021.10.03 |
얕은 복사 & 깊은 복사 (0) | 2021.10.03 |
유사 배열 객체 만들기 (0) | 2021.10.02 |
일급 객체 (First Class Object) (0) | 2021.10.02 |
댓글
이 글 공유하기
다른 글
-
코드 실행에 필요한 정보를 담은 실행 컨텍스트
코드 실행에 필요한 정보를 담은 실행 컨텍스트
2021.10.05 -
자기 자신을 가리키는 값 this
자기 자신을 가리키는 값 this
2021.10.03 -
얕은 복사 & 깊은 복사
얕은 복사 & 깊은 복사
2021.10.03 -
유사 배열 객체 만들기
유사 배열 객체 만들기
2021.10.02