JavaScript 클로저와 스코프 간단 설명

Frontend2026-09-19TryQuickToolBox

다른 함수를 반환하는 JavaScript 함수를 작성하고 있다고 가정해 봅시다. 내부 함수는 외부 함수의 변수를 사용하지만, 나중에 호출할 때 외부 함수는 이미 실행을 마쳤습니다. 그런데도 변수는 여전히 존재합니다. 어떻게 가능할까요? 바로 클로저와 스코프의 마법입니다. 많은 개발자가 이 개념에 어려움을 겪으며, 예상치 못한 변수 값이나 메모리 누수 같은 버그를 만듭니다. 이 글에서는 JavaScript 클로저와 스코프를 실제 예제와 함께 쉽게 설명하겠습니다.

JavaScript에서 스코프란?

스코프는 코드에서 변수에 접근할 수 있는 범위를 결정합니다. JavaScript에는 세 가지 주요 스코프 유형이 있습니다:

간단한 예를 들어보겠습니다:

let globalVar = 'I am global';

function myFunc() {
  let functionVar = 'I am in function scope';
  if (true) {
    let blockVar = 'I am in block scope';
    console.log(globalVar); // accessible
    console.log(functionVar); // accessible
    console.log(blockVar); // accessible
  }
  console.log(blockVar); // ReferenceError: blockVar is not defined
}

myFunc();
console.log(functionVar); // ReferenceError: functionVar is not defined

스코프를 이해하는 것은 변수 충돌을 방지하고 메모리를 관리하는 데 도움이 되므로 매우 중요합니다.

렉시컬 스코핑: 클로저의 기초

JavaScript는 렉시컬 스코핑을 사용합니다. 즉, 변수의 스코프는 소스 코드에서의 위치에 따라 결정됩니다. 내부 함수는 외부(둘러싸는) 스코프의 변수에 접근할 수 있습니다. 이를 정적 스코핑이라고도 합니다.

다음을 살펴보세요:

function outer() {
  let outerVar = 'outer';
  function inner() {
    console.log(outerVar); // 'outer'
  }
  inner();
}
outer();

inner 함수는 outer 내부에 렉시컬하게 위치하므로 outerVar에 접근할 수 있습니다. 이는 동적이 아니며, 함수가 어떻게 호출되는지에 의존하지 않습니다.

클로저란?

클로저는 함수가 해당 스코프 외부에서 실행될 때도 자신의 렉시컬 스코프를 기억하는 함수입니다. 다시 말해, 클로저는 내부 함수에서 외부 함수의 스코프에 접근할 수 있게 해줍니다. 클로저는 함수가 생성될 때마다, 함수 생성 시점에 만들어집니다.

예:

function makeCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2

여기서 makeCounter는 count를 증가시키는 내부 함수를 반환합니다. makeCounter의 실행이 끝난 후에도 반환된 함수는 여전히 count에 접근할 수 있습니다. 이것이 클로저입니다.

클로저의 내부 작동 방식

함수가 생성될 때, 함수는 자신의 렉시컬 환경(스코프 체인)에 대한 참조를 저장합니다. 함수가 호출되면, 그 환경을 사용하여 변수를 해석합니다. 환경은 클로저가 존재하는 한 계속 유지됩니다.

클로저의 실용적인 사용

클로저는 이론적인 것만이 아니라 JavaScript 전반에서 사용됩니다.

1. 데이터 프라이버시 (모듈 패턴)

ES6 모듈 이전에는 클로저를 사용하여 프라이빗 변수를 만들었습니다:

const counterModule = (function() {
  let count = 0;
  return {
    increment: function() { count++; },
    getCount: function() { return count; }
  };
})();

counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined

여기서 count는 프라이빗이며 반환된 메서드를 통해서만 접근할 수 있습니다.

2. 이벤트 핸들러와 콜백

클로저는 이벤트 핸들러에서 외부 스코프의 변수에 접근하기 위해 흔히 사용됩니다:

function setupButton() {
  let clickCount = 0;
  document.getElementById('myButton').addEventListener('click', function() {
    clickCount++;
    console.log(`Button clicked ${clickCount} times`);
  });
}

이벤트 핸들러 클로저는 clickCount를 기억합니다.

3. 함수 팩토리

미리 설정된 인수를 가진 함수를 만들 수 있습니다:

function multiplyBy(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = multiplyBy(2);
console.log(double(5)); // 10

흔한 함정과 피하는 방법

클로저를 이해하지 못하면 예상치 못한 동작을 초래할 수 있습니다.

루프 변수 캡처

루프에서 var를 사용하면 단일 공유 바인딩이 생성됩니다:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // prints 3, 3, 3
  }, 100);
}

모든 콜백이 동일한 i를 공유합니다. 해결하려면 let(블록 스코프) 또는 IIFE를 사용하세요:

for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // prints 0, 1, 2
  }, 100);
}

메모리 누수

클로저는 변수를 계속 살려둡니다. 클로저가 장수명 객체(예: DOM 요소)에 연결되어 있으면 가비지 컬렉션을 방해할 수 있습니다. 더 이상 필요하지 않으면 항상 이벤트 리스너를 제거하세요.

스코프 체인과 변수 해석

변수에 접근할 때 JavaScript는 스코프 체인을 따라 조회합니다: 먼저 현재 스코프, 그 다음 외부 스코프, 전역 스코프까지. 찾지 못하면 ReferenceError가 발생합니다.

예:

let a = 'global';
function outer() {
  let b = 'outer';
  function inner() {
    let c = 'inner';
    console.log(a, b, c); // all accessible
  }
  inner();
}
outer();

inner의 스코프 체인은 inner -> outer -> global입니다.

비교: var, let, const

키워드스코프호이스팅재할당
var함수예 (undefined로 초기화됨)예
let블록예 (초기화되지 않음, TDZ)예
const블록예 (초기화되지 않음, TDZ)아니오

이러한 차이를 이해하는 것이 스코프 관련 버그를 피하는 핵심입니다.

FAQ

JavaScript에서 클로저란?

클로저는 해당 스코프 외부에서 실행될 때도 자신의 렉시컬 스코프에 대한 접근을 유지하는 함수입니다. 함수가 정의된 곳의 변수를 "기억"할 수 있게 해줍니다.

클로저가 왜 중요한가요?

클로저는 데이터 프라이버시, 함수 팩토리, 상태를 유지하는 이벤트 핸들러를 가능하게 합니다. 많은 JavaScript 패턴의 기초입니다.

클로저 관련 버그를 어떻게 피할 수 있나요?

블록 스코핑을 위해 let과 const를 사용하고, 루프를 주의하며, 메모리 누수를 방지하기 위해 이벤트 리스너를 정리하세요.

이해도를 테스트할 준비가 되셨나요? JSON Formatter를 사용해 코드 스니펫을 실험하고 클로저가 실시간으로 어떻게 작동하는지 확인해 보세요.