좋은 생각입니다. 앞선 글에 정적 스코프와 동적 스코프에 대한 내용을 추가하여 렉시컬 스코프에 대한 이해를 더욱 깊이 있게 만들 수 있습니다. 아래는 이를 포함한 업데이트된 글입니다.
프로그래밍을 하다 보면 "스코프"라는 개념을 자주 접하게 됩니다. 스코프란 변수가 유효한 범위를 의미합니다. 그 중에서도 렉시컬 스코프(Lexical Scope)는 함수가 선언된 위치에 따라 스코프가 결정되는 방식을 말합니다. 이 글에서는 렉시컬 스코프가 무엇인지, 어떻게 동작하는지, 그리고 왜 중요한지를 자세히 살펴보겠습니다.
렉시컬 스코프는 변수의 스코프가 코드의 렉시컬(또는 소스 코드) 구조에 따라 결정되는 것을 의미합니다. 즉, 함수가 어디에서 선언되었는지가 중요하며, 함수가 호출되는 위치와는 무관합니다. 이를 통해 변수가 어디서 접근 가능한지를 명확히 알 수 있습니다.
function outerFunction() {
let outerVariable = '나는 외부 변수!';
function innerFunction() {
console.log(outerVariable);
}
innerFunction();
}
outerFunction(); // '나는 외부 변수!' 출력
위 예제에서 outerFunction은 innerFunction을 포함하고 있습니다. innerFunction은 outerVariable에 접근할 수 있습니다. 왜냐하면 innerFunction이 선언될 때 이미 outerFunction의 스코프를 포함하고 있기 때문입니다. 이것이 렉시컬 스코프의 기본 개념입니다.
렉시컬 스코프는 함수가 선언된 위치에 따라 스코프가 결정됩니다. 이를 이해하기 위해 조금 더 복잡한 예제를 살펴보겠습니다:
let globalVariable = '나는 전역 변수!';
function outerFunction() {
let outerVariable = '나는 외부 변수!';
function innerFunction() {
let innerVariable = '나는 내부 변수!';
console.log(globalVariable); // '나는 전역 변수!' 출력
console.log(outerVariable); // '나는 외부 변수!' 출력
console.log(innerVariable); // '나는 내부 변수!' 출력
}
innerFunction();
}
outerFunction();
여기서 innerFunction은 세 가지 변수를 출력합니다:
1. 전역 변수 globalVariable
2. 외부 함수 outerFunction의 변수 outerVariable
3. 자신의 내부 변수 innerVariable
각 변수는 자신이 선언된 스코프 내에서 접근 가능합니다. innerFunction은 globalVariable과 outerVariable에 접근할 수 있는 이유는 렉시컬 스코프 덕분입니다. 함수가 선언된 위치에서부터 상위 스코프를 차례대로 탐색하기 때문입니다.
프로그래밍 언어에서는 다양한 스코프 개념이 사용됩니다. 그 중 주요한 스코프의 종류는 다음과 같습니다:
전역 스코프는 프로그램의 모든 부분에서 접근 가능한 스코프입니다. 전역 변수는 프로그램이 종료될 때까지 메모리에 상주합니다.
함수 스코프는 함수 내에서 선언된 변수들이 해당 함수 내에서만 유효한 스코프를 말합니다. 함수가 호출될 때마다 새로운 스코프가 생성됩니다.
블록 스코프는 함수 내부의 중괄호 {}
안에서 선언된 변수들이 해당 블록 내에서만 유효한 스코프를 말합니다. 이는 let
, const
와 같은 ES6에서 도입된 키워드를 통해 구현됩니다.
렉시컬 스코프는 정적 스코프(Static Scope)와 동일한 개념입니다. 이 두 용어는 종종 혼용되며, 대부분의 현대 프로그래밍 언어는 정적 스코핑을 채택하고 있습니다. 반면, 동적 스코프(Dynamic Scope)는 다른 스코핑 규칙을 따릅니다.
정적 스코프와 렉시컬 스코프는 함수나 변수가 선언된 위치에 따라 스코프가 결정됩니다. 따라서 함수가 호출되는 위치와는 상관없이, 함수가 정의된 위치에 따라 변수를 참조합니다.
예제:
let globalVar = "전역 변수";
function outerFunction() {
let outerVar = "외부 변수";
function innerFunction() {
console.log(globalVar); // "전역 변수"
console.log(outerVar); // "외부 변수"
}
innerFunction();
}
outerFunction();
위 코드에서 innerFunction
은 자신이 선언된 위치를 기준으로 변수 접근을 결정합니다.
동적 스코프는 함수가 호출되는 시점에 따라 스코프가 결정됩니다. 즉, 함수 호출이 이루어질 때의 호출 스택을 기준으로 변수의 유효 범위가 결정됩니다. 동적 스코프를 사용하는 언어는 많지 않지만, 일부 고대의 프로그래밍 언어와 특정 스크립트 언어에서 사용되었습니다.
예제:
(defvar *global-var* "전역 변수")
(defun outer-function ()
(let ((*outer-var* "외부 변수"))
(inner-function)))
(defun inner-function ()
(print *global-var*)
(print *outer-var*))
(outer-function)
위의 LISP 예제에서 inner-function
은 동적 스코핑을 사용하여 outer-function
의 로컬 변수 *outer-var*
에 접근할 수 있습니다. 이는 호출 시점에 따라 변수를 해석하기 때문에 가능한 일입니다.
렉시컬 스코프를 이해하는 것은 여러 면에서 중요합니다:
1. 예측 가능성: 코드의 선언 위치에 따라 스코프가 결정되므로 변수가 어디서 유효한지 쉽게 예측할 수 있습니다.
2. 가독성: 코드가 선언된 위치에 따라 변수의 유효 범위를 알 수 있어 코드가 더 읽기 쉬워집니다.
3. 디버깅 용이: 변수가 어디서 변경되었는지 쉽게 추적할 수 있어 디버깅이 더 쉬워집니다.
4. 최적화 가능성: 컴파일러가 코드를 더 쉽게 최적화할 수 있습니다.
5. 클로저 이해: 클로저는 렉시컬 스코프와 밀접한 관련이 있습니다. 클로저는 함수가 선언된 렉시컬 스코프를 기억하는 함수입니다.
클로저는 함수와 함수가 선언될 당시의 렉시컬 환경의 조합입니다. 클로저는 함수가 선언된 렉시컬 스코프를 기억하고, 함수가 호출될 때 그 스코프에 접근할 수 있습니다. 이를 통해 함수가 종료된 후에도 외부 함수의 변수에 접근할 수 있게 됩니다. 클로저를 이해하는 것은 렉시컬 스코프를 이해하는 데 큰 도움이 됩니다.
클로저는 함수와 그 함수가 선언될 당시의 렉시컬 환경을 함께 저장한 것입니다. 자바스크립트에서 함수는 일급 객체이므로, 함수 내에서 다른 함수를 반환하거나 함수를 변수에 할당할 수 있습니다. 이러한 특성으로 인해 클로저가 생성됩니다.
클로저는 다음과 같은 조건을 만족합니다:
1. 내부 함수가 외부 함수의 변수를 참조할 수 있다.
2. 외부 함수가 실행을 마친 후에도 내부 함수가 참조한 외부 함수의 변수는 계속 유지된다.
클로저가 작동하는 방식을 이해하기 위해 다음 예제를 살펴보겠습니다:
function outerFunction() {
let outerVariable = '나는 외부 변수!';
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const myFunction = outerFunction();
myFunction(); // '나는 외부 변수!' 출력
위 예제에서 outerFunction
은 innerFunction
을 반환합니다. 중요한 점은 outerFunction
이 반환될 때, innerFunction
은 여전히 outerVariable
에 접근할 수 있다는 것입니다. 이는 클로저 덕분에 가능한 일입니다. myFunction
은 innerFunction
을 가리키며, innerFunction
은 자신이 선언된 렉시컬 스코프를 기억하고 있기 때문에 outerVariable
에 접근할 수 있습니다.
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1 출력
counter(); // 2 출력
counter(); // 3 출력
이 예제에서 createCounter
함수는 count
변수를 포함한 내부 함수를 반환합니다. 반환된 내부 함수는 count
변수를 증가시키고 그 값을 출력합니다. 중요한 점은 count
변수가 createCounter
함수가 종료된 후에도 내부 함수에서 유지된다는 것입니다.
클로저는 데이터 캡슐화와 정보 은닉에도 유용하게 사용됩니다. 다음 예제를 통해 이를 살펴보겠습니다:
function person(name) {
let privateName = name;
return {
getName: function() {
return privateName;
},
setName: function(newName) {
privateName = newName;
}
};
}
const me = person('John');
console.log(me.getName()); // 'John' 출력
me.setName('Doe');
console.log(me.getName()); // 'Doe' 출력
위 예제에서 person
함수는 privateName
변수를 포함한 객체를 반환합니다. 객체는 getName
과 setName
메서드를 가지며, 이 메서드들은 privateName
변수에 접근할 수 있습니다. 이렇게 하면 privateName
변수는 외부에서 직접 접근할 수 없게 되며, 오직 getName
과 setName
메서드를 통해서만 접근이 가능합니다.
렉시컬 스코프와 클로저는 자바스크립트의 핵심 개념으로, 이를 통해 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다.
렉시컬 스코프는 함수가 선언된 위치에 따라 변수의 유효 범위가 결정되는 개념으로, 코드가 어디에서 실행되든 상관없이 변수 접근이 예측 가능합니다.
클로저는 함수와 그 함수가 선언된 렉시컬 환경을 함께 저장하여 함수가 종료된 후에도 외부 함수의 변수에 접근할 수 있게 해주는 강력한 기능입니다.
클로저를 통해 데이터 캡슐화와 정보 은닉을 구현할 수 있으며, 이는 더 효율적이고 유지보수 가능한 코드를 작성하는 데 도움이 됩니다.
이 글을 통해 렉시컬 스코프와 클로저의 개념과 작동 방식을 예제 코드와 함께 살펴보았습니다.
이를 이해하면 더 깊이 있는 자바스크립트 프로그래밍을 할 수 있을 것입니다.
이 글이 렉시컬 스코프와 클로저에 대한 이해를 높이는 데 도움이 되었기를 바랍니다.