2025-09-23 00:01
-
**스코프**는 변수가 살아서 숨 쉬는 유효 범위이며, 코드의 질서와 안정성을 보장하는 기본 규칙이다.
-
**클로저**는 함수가 자신이 태어난 환경을 기억하여, 외부에서도 그 환경에 접근할 수 있게 하는 자바스크립트의 핵심 원리다.
-
이 두 가지 개념을 이해하면 데이터 은닉, 상태 관리 등 고차원적인 프로그래밍 기법을 자유자재로 구사할 수 있다.
자바스크립트 스코프와 클로저 완벽 정복 핸드북
1. 들어가며: 왜 스코프와 클로저를 알아야 하는가?
만약 모든 변수가 아무런 규칙 없이 코드 어디에서나 접근하고 수정할 수 있다면 어떻게 될까? 아마 수백, 수천 줄의 코드 속에서 변수 이름이 충돌하고, 값이 예상치 못하게 바뀌는 대혼란이 펼쳐질 것이다. **스코프(Scope)**는 바로 이런 혼란을 막기 위해 변수가 활동할 수 있는 자신만의 영토를 지정해주는 규칙이다. 코드의 질서와 안정성을 보장하는 가장 기본적인 약속인 셈이다.
한편, 함수가 자신이 실행되는 위치가 아닌, ‘태어난 곳’의 환경을 기억한다면 어떨까? 이 신비로운 현상이 바로 **클로저(Closure)**다. 클로저를 이해하면 단순히 함수를 호출하는 것을 넘어, 상태를 기억하고 데이터를 외부로부터 안전하게 보호하는 등 훨씬 더 정교하고 강력한 코드를 작성할 수 있다.
스코프와 클로저는 자바스크립트의 핵심 메커니즘을 관통하는 매우 중요한 개념이다. 이 두 가지를 완벽히 이해하는 순간, 당신의 코드는 이전과는 다른 차원의 견고함과 유연성을 갖추게 될 것이다. 이 핸드북은 당신을 ‘코드를 작성하는 개발자’에서 ‘코드의 동작 원리를 이해하는 개발자’로 이끌어 줄 단단한 디딤돌이 될 것이다.
2. 스코프 (Scope) 깊이 파헤치기: 변수의 집
2.1 스코프란 무엇인가? “이 변수, 여기서 쓸 수 있나요?”
스코프는 한마디로 **‘변수와 함수에 접근할 수 있는 유효 범위’**를 의미한다. 자바스크립트 엔진이 특정 변수를 찾을 때 참조하는 규칙의 집합이기도 하다. 스코프가 존재하기에 우리는 코드의 각기 다른 부분에서 같은 이름의 변수를 사용해도 서로 충돌 없이 안전하게 프로그램을 운영할 수 있다.
스코프를 이해하기 가장 쉬운 비유는 ‘주소’ 체계다.
-
전역 스코프: ‘대한민국’처럼 어디서든 통용되는 가장 큰 범위.
-
함수 스코프: ‘서울특별시’나 ‘경기도’처럼 특정 함수 내에서만 유효한 범위.
-
블록 스코프: ‘강남구’나 ‘수원시’처럼
{}로 둘러싸인 더 작은 지역 단위.
어떤 변수를 찾을 때, 우리는 가장 좁은 범위(내 방)부터 시작해서 점차 범위를 넓혀가며(우리 집 → 대한민국) 찾는다. 스코프도 이와 똑같은 방식으로 동작한다.
2.2 스코프의 종류: 코드가 결정하는 운명 (렉시컬 스코프)
자바스크립트의 스코프는 어디서 함수가 ‘호출’되었는지가 아니라, 어디서 ‘정의’되었는지에 따라 결정된다. 이를 렉시컬 스코프(Lexical Scope) 또는 정적 스코프(Static Scope)라고 부른다. 즉, 개발자가 코드를 작성하는 그 순간에 스코프의 경계가 이미 정해진다는 의미다. 이는 코드의 예측 가능성을 높여주는 매우 중요한 특징이다.
| 스코프 종류 | 설명 | 주요 키워드 | 특징 |
|---|---|---|---|
| 전역 스코프 (Global Scope) | 코드의 가장 바깥 영역. 전역 변수는 어디서든 접근 가능. | var, let, const (함수 밖) | 애플리케이션의 생명주기와 운명을 함께 함. 남용 시 변수 충돌 위험. |
| 함수 스코프 (Function Scope) | 함수에 의해 생성되는 스코프. 함수 내부에서만 접근 가능. | var | ES6 이전의 유일한 지역 스코프. 함수 단위로 변수가 격리됨. |
| 블록 스코프 (Block Scope) | {}(블록)에 의해 생성되는 스코프. | let, const | for, if 문 등에서 더 직관적이고 안전한 변수 관리를 가능하게 함. |
렉시컬 스코프 vs 동적 스코프
자바스크립트가 채택한 렉시컬 스코프와 반대되는 개념으로 동적 스코프(Dynamic Scope)가 있다. 동적 스코프는 함수가 호출되는 시점에 따라 스코프가 결정된다.
JavaScript
var x = 1;
function foo() {
console.log(x); // 렉시컬 스코프에서는 1, 동적 스코프였다면 2
}
function bar() {
var x = 2;
foo();
}
bar();
위 코드에서 foo 함수는 bar 함수 내부에서 호출되었다. 만약 자바스크립트가 동적 스코프를 따랐다면, foo는 자신을 호출한 bar의 스코프에 있는 x 값인 2를 출력했을 것이다. 하지만 자바스크립트는 렉시컬 스코프를 따르므로, foo는 자신이 정의된 위치의 바깥 스코프, 즉 전역 스코프에 있는 x 값인 1을 출력한다. 이것이 바로 코드를 작성하는 대로 동작하게 만드는 핵심 원리다.
2.3 스코프 체인: 변수를 찾아 떠나는 여정
자바스크립트 엔진이 변수를 찾을 때, 현재 스코프에서 시작하여 변수를 찾지 못하면 바깥쪽 스코프로 점차 이동하며 찾는 과정이 발생한다. 이처럼 스코프가 사슬처럼 연결된 것을 **스코프 체인(Scope Chain)**이라고 한다.
-
현재 스코프: 변수를 현재 실행 중인 스코프에서 먼저 찾는다.
-
외부 스코프: 없으면, 바로 바깥쪽(상위) 스코프에서 찾는다.
-
전역 스코프: 계속해서 바깥으로 이동하다가 최상위인 전역 스코프까지 탐색한다.
-
에러 발생: 전역 스코프에서도 변수를 찾지 못하면
ReferenceError가 발생한다.
이 스코프 체인은 함수가 정의될 때 결정되는 **실행 컨텍스트(Execution Context)**와 깊은 관련이 있다. 모든 함수는 자신의 실행 컨텍스트 내에 Outer Environment Reference(외부 환경 참조)를 가지고 있는데, 이것이 바로 스코프 체인을 통해 상위 스코프를 가리키는 포인터 역할을 한다.
3. 클로저 (Closure) 완벽 이해: 기억하는 함수
3.1 클로저란 무엇인가? “나는 내가 태어난 곳을 기억한다.”
MDN에서는 클로저를 다음과 같이 정의한다.
“클로저는 함수와 그 함수가 선언될 당시의 렉시컬 환경(Lexical Environment)의 조합이다.”
쉽게 말해, 클로저는 자신이 생성될 때의 환경(스코프)을 기억하는 함수다. 이 특징 덕분에 함수는 자신의 렉시컬 스코프 밖에서 실행될 때에도, 자신이 태어난 환경의 변수들에 접근할 수 있다.
클로저는 우리가 의도적으로 ‘만드는’ 기능이라기보다는, 렉시컬 스코프를 따르는 언어에서 자연스럽게 발생하는 현상에 가깝다.
JavaScript
function outer() {
const outerVar = 'I am outside!';
function inner() {
console.log(outerVar); // 바깥 함수의 변수에 접근
}
return inner;
}
const innerFunc = outer(); // outer 함수는 실행이 끝났지만,
innerFunc(); // 'I am outside!'가 출력된다.
위 예제에서 outer 함수는 실행이 끝나 호출 스택에서 사라졌다. 상식적으로 outer 함수 안에 있던 outerVar 변수도 메모리에서 해제되어야 한다. 하지만 inner 함수가 outerVar를 참조하고 있고, 이 inner 함수는 innerFunc라는 전역 변수에 의해 여전히 살아있다.
이때 자바스크립트의 **가비지 컬렉터(Garbage Collector)**는 inner 함수가 언젠가 outerVar를 사용할 수도 있다는 것을 인지하고, outerVar를 메모리에서 지우지 않고 남겨둔다. 바로 이 현상이 클로저의 핵심이다.
3.2 클로저의 작동 원리: [[Environment]]의 비밀
내부적으로 자바스크립트 엔진은 함수가 생성될 때, 그 함수의 렉시컬 환경에 대한 참조를 [[Environment]]라는 숨겨진 내부 슬롯에 저장한다.
-
outer함수가 호출되면,outer를 위한 새로운 렉시컬 환경이 생성되고, 이 환경에는outerVar가 기록된다. -
outer함수 내부에서inner함수가 정의되는 순간,inner함수는 자신의[[Environment]]에outer의 렉시컬 환경에 대한 참조를 저장한다. -
outer함수는inner함수를 반환하고 실행을 마친다. -
나중에
innerFunc()가 호출될 때, 엔진은innerFunc(inner함수)의 코드를 실행한다. -
코드 실행 중
outerVar라는 변수를 만나면, 먼저inner자신의 스코프에서 찾는다. -
변수가 없으면,
[[Environment]]에 저장해 둔 외부 환경 참조(스코프 체인)를 따라outer의 렉시컬 환경으로 이동하여outerVar를 찾아낸다.
이러한 메커니즘 덕분에 클로저는 마법처럼 외부 함수의 변수를 기억하고 접근할 수 있는 것이다.
3.3 클로저 실전 활용법: 단순한 개념을 넘어 무기로
클로저는 다양한 고급 프로그래밍 패턴을 구현하는 핵심 도구로 사용된다.
-
데이터 은닉과 캡슐화 (모듈 패턴)
객체지향 프로그래밍의 ‘private’ 멤버 변수처럼, 외부에서는 접근할 수 없고 오직 특정 함수들을 통해서만 조작할 수 있는 변수를 만들 때 사용된다.
JavaScript
function createCounter() { let count = 0; // 외부에서 직접 접근 불가능한 비공개 변수 return { increment: function() { count++; console.log(count); }, decrement: function() { count--; console.log(count); } }; } const counter = createCounter(); counter.increment(); // 1 counter.increment(); // 2 // console.log(counter.count); // undefined, 직접 접근 불가 -
상태 유지
함수 호출이 끝나도 특정 상태를 계속 유지하고 싶을 때 유용하다. 위의 createCounter 예제처럼 count 변수는 counter 객체가 살아있는 동안 계속해서 자신의 상태를 기억한다.
-
함수 팩토리와 고차 함수
클로저를 이용하면 특정 설정값을 기억하는 함수를 동적으로 생성할 수 있다.
JavaScript
function createMultiplier(factor) { return function(number) { return number * factor; }; } const double = createMultiplier(2); // 2를 기억하는 함수 const triple = createMultiplier(3); // 3을 기억하는 함수 console.log(double(5)); // 10 console.log(triple(5)); // 15
4. 심화 학습: 클로저, 제대로 알고 사용하기
4.1 클로저와 메모리 관리: 양날의 검
클로저는 매우 강력하지만, 메모리 측면에서는 주의가 필요하다. 클로저가 상위 스코프의 변수에 대한 참조를 유지하기 때문에, 이 변수들은 가비지 컬렉션의 대상에서 제외된다. 만약 클로저가 더 이상 필요 없음에도 불구하고 계속 살아있다면, 불필요한 메모리를 계속 점유하는 **메모리 누수(Memory Leak)**의 원인이 될 수 있다.
특히, 불필요한 DOM 요소에 대한 참조를 클로저가 계속 붙들고 있는 경우, 웹 페이지의 성능 저하를 유발할 수 있다.
해결 방법:
클로저의 사용이 끝났다면, 해당 클로저를 참조하는 변수에 null을 할당하여 가비지 컬렉터가 관련 메모리를 회수할 수 있도록 명시적으로 연결을 끊어주는 것이 좋다.
JavaScript
let myClosure = (function() {
let largeData = new Array(1000000).join('*'); // 큰 데이터
return function() {
// largeData를 사용하는 로직
};
})();
// ... 사용 후 ...
myClosure = null; // 참조를 끊어 메모리 해제를 돕는다.
4.2 성능에 대한 고찰
루프(loop) 안에서 클로저를 포함하는 함수를 반복적으로 생성하는 것은 성능에 미미한 영향을 줄 수 있다. 매번 새로운 함수 객체와 렉시컬 환경이 생성되기 때문이다. 대부분의 경우 현대 자바스크립트 엔진이 이를 매우 효율적으로 최적화하지만, 극단적인 성능 최적화가 필요한 상황에서는 함수를 루프 밖에서 한 번만 정의하는 것을 고려해볼 수 있다.
5. 맺음말: 스코프와 클로저, 그리고 성장
스코프와 클로저는 단순히 암기해야 할 문법이 아니다. 이것은 자바스크립트라는 언어가 어떻게 변수를 관리하고, 함수가 어떻게 동작하는지에 대한 근본적인 철학을 담고 있다.
스코프의 규칙을 통해 우리는 코드의 안정성과 예측 가능성을 확보하고, 클로저라는 현상을 통해 상태를 안전하게 관리하고 더 높은 수준의 추상화를 구현할 수 있다. 이 두 가지 개념을 깊이 이해하고 자유자재로 활용할 수 있을 때, 비로소 우리는 자바스크립트의 진정한 힘을 경험하게 될 것이다. 이 핸드북이 그 여정의 훌륭한 안내자가 되기를 바란다.
아래 영상은 렉시컬 스코프와 동적 스코프의 차이점을 설명하며, 자바스크립트가 왜 렉시컬 스코프를 채택했는지에 대한 이해를 돕습니다.
OCaml 프로그래밍 강의 영상람다