원문: Medium에서 보기 Medium 글을 이 블로그로 이관한 버전입니다.
들어가며
자바스크립트로 개발을 하다 보면 클로저와 스코프라는 용어를 자주 접하게 됩니다. 클로저와 스코프는 자바스크립트의 핵심 개념으로, 이를 제대로 이해하면 코드의 가독성과 성능향상이 크게 향상됩니다.
제가 쓴 글에서는 클로저와 스코프의 차이점을 자세히 설명하고, 이를 활용한 다양한 예제를 통해 실제 개발에 어떻게 적용할 수 있는지 살펴봅니다. 특히 클로저를 이용한 프라이빗 변수 생성, this 바인딩, 재귀 함수 등 실무에서 자주 마주치는 패턴을 소개합니다.
스코프와 클로저 정의
스코프는 변수가 정의되고 접근할 수 있는 코드의 영역을 의미합니다. 자바스크립트에서 스코프는 변수 선언 위치에 따라 결정됩니다.
- 클로벌 스코프: 함수나 블록 외부에서 선언된 변수는 글로벌 스코프를 가지며, 코드 어디에서든 접근할 수 있습니다.
- 로컬 스코프: 함수나 블록 내에서 선언된 변수는 로컬 스코프를 가지며, 해당 함수나 블록 내에서만 접근할 수 있습니다.
반면 클로저는 자신의 스코프와 부모 함수의 스코프에 접근할 수 있는 함수입니다. 클로저는 함수가 다른 함수 내에서 정의될 때 생성되며, 외부 함수가 반환된 후에도 내부 함수는 외부 함수의 변수를 “기억”할 수 있습니다.
다음 코드는 이 차이를 명확하게 설명합니다:

이 코드에서, inner 함수는 클로저입니다. 왜냐하면 outer 함수의 반환 후에도 x 변수를 기억하고 접근할 수 있기 때문입니다.
요약하자면, 스코프는 변수가 접근할 수 있는 범위를 결정하고, 클로저는 자신의 스코프와 부모 함수의 스코프에 접근할 수 있는 함수로, 생성 환경의 변수를 “기억”할 수 있습니다.
클로저 사용 예제
1. 외부 함수에 전달된 인수를 반환하는 클로저를 반환하는 함수
외부 함수에 전달된 인수를 반환하는 클로저를 반환하는 함수의 예입니다:
function outer(arg) {
return function inner() {
return arg;
};
}
이 예에서, outer 함수는 인수 arg를 받아서 새로운 함수 inner를 반환합니다. inner 함수는 클로저를 통해 arg 변수에 접근하고, 이를 반환합니다.
const getArg = outer('hello');
console.log(getArg()); // 'hello' 출력
이 예에서, ‘hello’라는 인수를 가진 outer 함수를 호출하면 inner 함수를 반환합니다. 이를 getArg 변수에 할당합니다. getArg를 호출하면 클로저를 통해 원래의 인수 'hello'를 반환합니다.
내부 함수는 외부 함수가 반환된 후에도 arg 변수에 접근할 수 있으며, 클로저 덕분에 원래 인수를 기억하고 나중에 반환할 수 있습니다.
2. 클로저를 사용하여 자바스크립트에서 프라이빗 변수를 생성하는 방법
자바스크립트에서 클로저를 사용하여 프라이빗 변수를 생성하는 방법은 스코프와 함수 캡슐화를 활용하는 것입니다. 다음은 그 예입니다:

이 예에서 Counter 함수는 increment와 getCount 메서드가 있는 객체를 반환합니다. privateCount 변수는 Counter 함수 내에 정의되어 있으며, 해당 스코프 내에서만 접근할 수 있습니다.
프라이빗 변수를 캡슐화하여 increment와 getCount 메서드를 통해 프라이빗 변수를 수정하고 접근할 수 있게 합니다. 이 패턴은 자바스크립트에서 프라이빗 변수와 캡슐화를 구현하는 일반적인 방법입니다.
단, privateCount는 반환된 객체의 프로퍼티로 노출되지 않으므로 Object.getOwnPropertyNames() 같은 일반적인 객체 열거만으로는 직접 접근할 수 없습니다. 이 예제의 핵심은 클로저를 통해 외부에서 바로 만질 수 없는 상태를 캡슐화하는 데 있습니다.
3. 클로저가 자바스크립트의 this 키워드와 어떻게 상호작용 하는지
this는 함수의 현재 실행 문맥을 참조하는 키워드입니다. 함수가 호출되는 방식에 따라 서로 다른 객체를 참조할 수 있습니다.
클로저는 생성 시점의 렉시컬 스코프 변수를 보존하지만, this를 같은 방식으로 자동 보존하지는 않습니다.
클로저와 this의 상호작용 방식 예:

실행 포인트:
- outer 함수는 this.name을 'outer'로 설정합니다.
- inner 함수는 outer 내부에서 클로저로 생성됩니다.
- innerFunc가 호출될 때, this는 call() 메서드로 설정된 { name: 'newContext' } 객체를 참조합니다.
- inner가 다른 컨텍스트에서 생성되었더라도, 현재
this값은 호출 방식에 의해 결정되므로 'newContext'를 출력합니다.
주요 포인트:
- 클로저는 생성 시점의 렉시컬 변수를 유지합니다.
this는 렉시컬 변수와 달리 호출 시점에 의해 결정됩니다.- 특정
this를 유지하려면bind(),call(), 또는apply()로 명시적으로 바인딩해야 합니다.
4. 함수 호출 횟수를 로그하는 클로저를 반환하는 함수
다음은 함수 호출 횟수를 로그하는 클로저를 반환하는 함수의 예입니다:

작동 방식은 다음과 같습니다:
- logCallCount 함수는 함수 fn을 인수로 받습니다.
- callCount 변수를 0으로 초기화합니다.
- wrapper라는 새로운 함수를 반환합니다.
- wrapper 함수는 호출될 때마다 callCount 변수를 증가시키고, 현재 호출 횟수를 로그로 출력합니다.
- fn 함수를 apply() 메서드를 사용하여 원래의 문맥과 인수를 보존한 채 호출합니다.
사용 예:
const loggedAdd = logCallCount(function add(a, b) {
return a + b;
});
console.log(loggedAdd(1, 2)); // "Function called 1 times" 출력 후 3 반환
console.log(loggedAdd(3, 4)); // "Function called 2 times" 출력 후 7 반환
이 예에서, loggedAdd라는 새로운 함수는 원래의 add 함수를 감싸고 있습니다. loggedAdd가 호출될 때마다 호출 횟수를 로그로 출력하고, 원래의 add 함수 결과를 반환합니다.
5. 재귀 함수에서 클로저가 어떻게 작동하는지
재귀 함수에서도 클로저는 기본적으로 동일하게 작동합니다. 각 재귀 호출은 새로운 스코프를 만들고, 클로저는 자신이 정의된 시점의 렉시컬 환경에 접근합니다.
즉, 클로저가 모든 이전 호출의 스코프에 직접 접근한다기보다, 캡처된 변수와 재귀 호출 흐름을 통해 상태가 이어지는 것으로 이해하는 편이 정확합니다.

작동 방식은 다음과 같습니다:
- recursiveClosure 함수가 호출되면, x 값이 설정되고 count 변수가 0으로 초기화됩니다. 이 함수는 내부에 inner 함수를 정의하고 이를 반환합니다.
- inner 함수는 클로저로서**recursiveClosure 함수의 스코프에 접근할 수 있습니다.** inner 함수는 호출될 때마다 count 변수를 증가시키고, 현재 x와 y 값을 콘솔에 출력합니다.
- recursiveFunc 변수에**inner 함수가 할당됩니다.** 이는 recursiveClosure(5) 호출에 의해 반환된 함수입니다. 이제 recursiveFunc는 inner 함수를 참조합니다.
- recursiveFunc(3) 호출 시, inner 함수가 y 값을 3으로 받아 실행됩니다. 이때 count 변수가 1 증가하고, Call 1: x = 5, y = 3이 출력됩니다.
- inner 함수는 재귀적으로 호출되어**y 값을 1씩 감소시키면서 계속 실행됩니다.** y 값이 0이 될 때까지 재귀 호출이 반복되며, 각 호출마다 count 변수가 증가하고 현재 x와 y 값이 콘솔에 출력됩니다.
- 재귀 호출 종료 시점에서는**y 값이 0이 됩니다.** 이 시점에서 더 이상 재귀 호출이 이루어지지 않고 함수 호출이 종료됩니다.
최종 출력 결과:
- 첫 번째 호출: Call 1: x = 5, y = 3
- 두 번째 호출: Call 2: x = 5, y = 2
- 세 번째 호출: Call 3: x = 5, y = 1
- 네 번째 호출: Call 4: x = 5, y = 0