Summary
- JavaScript는 렉시컬 스코프(정적 스코프)를 채택하고 있다.
- 함수나 변수가 어디서 "정의"되었는지가 스코프를 결정한다.
- 함수 호출 위치가 아니라, 함수 정의 위치가 중요하다.
- 스코프 체인을 통해 내부 함수는 자신이 정의될 당시의 외부 스코프 변수에 접근할 수 있다.
- 이 스코프 체인을 활용한 개념이 클로저이다.
- ES6 이후 `let`, `const`에 의한 블록 스코프 역시 렉시컬 스코프의 개념을 강화한다.
JS의 렉시컬 스코프(Lexical Scope)란?
JavaScript에서 스코프(Scope)는 변수, 함수, 객체 등이 코드 어디에서 유효하며 접근 가능한지를 결정하는 개념입니다. 이러한 스코프는 크게 렉시컬(lexical) 스코프와 동적(dynamic) 스코프로 나눌 수 있습니다. JavaScript는 렉시컬 스코프(정적 스코프)를 채택하고 있으며, 프로그램을 작성할 때 코드의 구조(소스 코드에서의 변수와 함수의 선언 위치)에 따라 스코프가 결정되다는 것을 의미합니다. 다시 말해, 자바스크립트 엔진은 코드를 해석하고 실행하기 전에 변수와 함수 선언을 바탕으로 스코프 체인을 구성하고, 이 스코프 구조는 런타임 중에 변경되지 않습니다.
렉시컬 스코프 채택의 이점
예측 가능성 및 가독성
- 변수가 어디에서 선언되었는지에 따라 스코프가 결정되므로 코드를 읽을 때 쉽게 변수의 유효 범위를 추적할 수 있습니다.
- 함수가 정의된 위치를 기준으로 스코프가 결정되므로 실행 중에 변수가 어떤 값을 참조할지 혼동이 없습니다.
성능 최적화
- 렉시컬 스코프는 실행 컨텍스트 생성 시점에 스코프 체인이 고정됩니다. 런타임에서 동적으로 스코프를 찾는 작업이 필요 없기 때문에 성능이 향상됩니다.
안정성 및 유지보수성
- 동적 스코프(dynamic scope)에 비해 의도치 않은 변수 덮어쓰기 및 참조 문제가 적습니다.
- 코드가 길어지거나 복잡해져도 스코프 규칙이 변하지 않아 유지보수가 용이합니다.
클로저(closure) 구현 용이
- 렉시컬 스코프는 클로저를 구현하는 데 필수적입니다. 클로저는 함수가 자신이 선언된 스코프를 기억하고, 해당 스코프에 접근할 수 있도록 합니다.
캡슐화 및 데이터 은닉
- 렉시컬 스코프를 활용해 변수와 함수를 특정 블록이나 함수 안에 감춰 캡슐화할 수 있습니다.
- 외부에서 직접 접근할 수 없는 데이터 영역을 생성하여 보안성이 향상됩니다.
렉시컬 스코프를 이해하는 핵심 포인트
소스코드 구조 기반
변수에 접근 가능 여부는 "어디서 해당 변수가 선언되었는지"에 의해 정해집니다. 즉, 함수가 어디에서 정의되었는지가 중요하며, 어디서 호출되었는지는 중요하지 않습니다.
스코프 체인(Scope Chain)
함수나 블록은 정의될 때 주변 스코프를 기억합니다. 함수가 실행될 때, 그 함수 내부에서 변수를 찾기 위해 우선 함수 자신의 스코프를 확인하고, 그 다음 내부에서 변수를 찾기 위해 우선 함수 자신의 스코프를 확인하고, 그 다음 상위 스코프(함수가 정의된 위치에 따른 상위 영역)를 차례로 검색하는 식의 체인이 형성됩니다. 이러한 계층적인 검색 체계를 스코프 체인이라고 합니다.
정적 결정(Static Determination)
런타임에 함수가 어디서 호출되었는지는 렉시컬 스코프에 영향을 주지 않습니다. 함수 선언 당시의 주변 스코프 환경을 그대로 기억하고, 이를 기반으로 변수를 해석합니다.
예제 살펴보기
const x = 1;
function outer() {
const x = 10;
function inner() {
console.log(x);
}
return inner;
}
const innerFunc = outer();
innerFunc(); // 무엇이 출력될까?
`inner` 함수는 `outer` 함수 내부에 정의되어 있습니다. 이때 `inner` 함수가 반환되어 `outer`의 실행 컨텍스트가 종료되더라도 `inner` 함수는 여전히 `outer` 함수가 가진 스코프 체인 정보를 기억하고 있습니다(클로저). 따라서 `inner`를 호출하고 `inner` 내부에서 `x`를 찾을 때, 바로 자신이 정의될 때 포착했던 상위 스코프인 `outer`의 `x`를 참조하게 됩니다. 이로 인해 `innerFunc()` 호출 시 값은 `10`이 출력됩니다.
만약 동적 스코프를 따르는 언어였다면, 함수가 호출되는 시점의 환경에 따라 참조 변수를 판단하게 되므로 전역의 `x`인 1을 출력할 수도 있지만, JavaScript는 정적 스코프, 즉 렉시컬 스코프를 따르므로 이러한 현상이 발생하지 않습니다.
var, let, const와 블록 스코프
ES6 이전의 자바스크립트는 함수 스코프(function scope)를 기반으로 `var` 키워드를 통해 변수를 선언했습니다. 이 경우, 블록(`{ }`) 내에서 선언된 변수도 함수 단위로 스코프를 결정했기 때문에 예상치 못한 스코프 범위를 가질 수 있었습니다. 그러나 `let`, `const`를 통해 선언된 변수는 블록 스코프(block scope)를 가지며, 이 또한 렉시컬 스코프 원리에 따라 코드 작성 시점에 결정됩니다. 다만 블록이 스코프의 경계가 된다는 차이가 있습니다.
{
let a = 10;
{
let a = 20;
console.log(a); // 20
}
console.log(a); // 10
}
여기서 `a` 변수는 각각 다른 블록에 의해 스코프가 나뉘며, 이는 코드 구조(렉시컬 스코프)로 결정됩니다.
함수 파라미터 스코프
함수의 매개변수(parameter) 역시 함수 스코프 안에 속한 변수와 동일한 렉시컬 스코프 규칙을 따릅니다. 함수 정의 시 매개변수는 해당 함수 블록 내부 변수와 동일한 스코프 레벨로 잡히며, 함수 외부에서는 접근할 수 없습니다.
function foo(x) {
// x는 foo 함수 스코프 내부에서만 유효
let y = 10;
return x + y;
}
console.log(foo(5)); // 15
// console.log(x); // ReferenceError
클로저(Closure)와 렉시컬 스코프의 관계
자바스크립트의 클로저 개념은 렉시컬 스코프를 기반으로 합니다. 함수가 자신이 선언될 때의 스코프를 기억하고 그 스코프에 있는 변수에 계속 접근할 수 있는 기법이 클로저입니다. 이를 통해 외부 함수의 실행이 끝났음에도, 해당 함수 내에서 정의된 내부 함수가 여전히 해당 스코프 내의 변수에 접근할 수 있습니다.
예를 들어:
function makeCounter() {
let count = 0;
return function() {
count++;
console.log(count);
}
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
`makeCounter()` 함수를 호출하면 내부 변수를 `count`로 가지는 익명 함수를 반환합니다. 이 익명 함수는 `makeCounter` 함수가 처음 실행될 때 정의되었고, 그 순간의 스코프(어휘적 문맥)를 기억하고 있습니다. 따라서 `makeCounter`가 끝나서 `count` 변수가 전부 "사라진 것"처럼 보여도 반환된 익명 함수는 여전히 그 스코프에 접근할 수 있습니다. 이것이 클로저이며, 렉시컬 스코프 없이 클로저는 불가능합니다.
클로저 활용 예시
프라이빗 변수 만들기
function createAccount(initialBalance) {
let balance = initialBalance; // 클로저로 보호된 변수
return {
deposit: function(amount) {
balance += amount;
console.log(`Deposited: ${amount}, Balance: ${balance}`);
},
withdraw: function(amount) {
if (balance >= amount) {
balance -= amount;
console.log(`Withdrew: ${amount}, Balance: ${balance}`);
} else {
console.log('Insufficient funds');
}
},
getBalance: function() {
return balance;
}
};
}
const account = createAccount(1000);
account.deposit(500); // Deposited: 500, Balance: 1500
account.withdraw(300); // Withdrew: 300, Balance: 1200
console.log(account.getBalance()); // 1200
console.log(account.balance); // undefined (직접 접근 불가)
이벤트 핸들러
function createLogger(name) {
return function(message) {
console.log(`[${name}] ${message}`);
};
}
const errorLogger = createLogger('Error');
errorLogger('An error occurred'); // [Error] An error occurred
클로저의 장점
- 코드 재사용성 : 상태를 유지하면서도 여러 인스턴스를 만들 수 있습니다.
- 보안성 향상 : 외부에서 변수에 직접 접근하는 것을 막아 보안을 강화합니다.
- 모듈화 : 함수와 변수를 캡슐화해 코드의 모듈화와 조직화를 돕습니다.
클로저 주의점
- 메모리 누수 : 클로저는 참조를 유지하므로 메모리에서 해제되지 않을 수 있습니다. 따라서, 불필요한 클로저는 참조를 제거해야 합니다.
- 디버깅 어려움 : 클로저로 인해 상태가 숨겨져 있어 디버깅이 까다로울 수 있습니다.
Reference
'JAVASCRIPT' 카테고리의 다른 글
자바스크립트와 브라우저의 비동기 처리 구조: 이벤트 루프와 큐 (1) | 2024.12.28 |
---|---|
배열 생성 심화: Array / Array.of / Array.from (0) | 2024.12.24 |
자바스크립트 배열(Array)의 모든 것 (0) | 2024.12.21 |
브라우저 환경에서의 자바스크립트 - this (0) | 2024.12.20 |
JavaScript 변수의 모든 것: var, let, const 완벽 비교와 활용법 (0) | 2024.12.13 |