icon
6장 : 자바스크립트 심화 I

클로저와 실행 컨텍스트

우리는 5장에서 자바스크립트의 기본적인 문법, 데이터 타입, 함수, 그리고 DOM 조작과 이벤트 처리까지 웹 개발에 필요한 필수적인 지식들을 학습했습니다. 이제 여러분은 기본적인 웹 페이지에 동적인 기능을 추가할 수 있는 단계에 이르렀습니다.

하지만 자바스크립트의 진정한 힘과 유연성은 그 내부 동작 원리를 이해할 때 비로소 발휘됩니다. 단순히 코드를 작성하는 것을 넘어, 코드가 어떻게 평가되고 실행되는지, 변수와 함수가 메모리에서 어떻게 관리되는지 등을 이해하는 것은 더욱 견고하고 효율적인 코드를 작성하는 데 필수적입니다.

6장 "자바스크립트 심화 I"에서는 이러한 자바스크립트의 심층적인 개념들을 다룹니다. 그 첫걸음으로 이번 장에서는 많은 개발자들이 어려워하지만 자바스크립트의 특성을 이해하는 데 가장 중요한 개념인 클로저(Closure)실행 컨텍스트(Execution Context) 에 대해 깊이 있게 탐구해 보겠습니다. 이 개념들을 이해하면 코드를 더 명확하게 작성하고, 예상치 못한 동작을 디버깅하며, 더 고급 패턴을 활용할 수 있는 기반을 다질 수 있습니다.

자바스크립트는 다른 많은 언어와 달리 'Lexical Scoping (어휘적 스코핑)'이라는 특징을 가집니다. 이는 함수를 어디서 선언했는지에 따라 해당 함수의 스코프가 결정된다는 의미입니다. 그리고 이 어휘적 스코핑과 함수가 실행될 때의 환경이 결합되어 실행 컨텍스트클로저라는 강력한 개념을 탄생시킵니다. 이 두 가지 개념은 자바스크립트가 비동기 처리, 모듈 패턴, 데이터 은닉 등을 구현하는 핵심 메커니즘이 됩니다.


실행 컨텍스트: 코드가 실행되는 환경

우리가 작성한 자바스크립트 코드가 실행되기 위해서는 특정 '환경'이 필요합니다. 이 환경을 자바스크립트 엔진에서는 실행 컨텍스트(Execution Context) 라고 부릅니다. 실행 컨텍스트는 코드를 실행하는 데 필요한 모든 정보를 담고 있는 추상적인 개념이자 실제적인 구조입니다.

자바스크립트 코드가 실행되는 순간, 엔진은 이 실행 컨텍스트를 생성하고, 이를 통해 변수를 관리하고, 스코프 체인을 구성하며, this 값을 결정하는 등의 작업을 수행합니다.

실행 컨텍스트의 종류

자바스크립트에는 크게 두 가지 종류의 실행 컨텍스트가 있습니다.

  1. 전역 실행 컨텍스트 (Global Execution Context)

    • 자바스크립트 코드가 처음 실행될 때 생성되는 최상위 컨텍스트입니다.
    • 웹 브라우저 환경에서는 window 객체(전역 객체)가 생성되고, thiswindow를 가리킵니다.
    • Node.js 환경에서는 global 객체가 전역 객체가 됩니다.
    • 코드 실행이 종료될 때까지 유지됩니다.
  2. 함수 실행 컨텍스트 (Function Execution Context)

    • 함수를 호출할 때마다 별도로 생성되는 컨텍스트입니다.
    • 함수가 호출될 때마다 새로운 변수 환경, 스코프 체인, this 바인딩 등이 만들어집니다.
    • 함수의 실행이 끝나면 해당 함수 실행 컨텍스트는 스택에서 제거되고 소멸됩니다.

실행 컨텍스트 스택 (Call Stack)

자바스크립트 엔진은 여러 개의 실행 컨텍스트를 동시에 관리하기 위해 스택(Stack) 자료구조를 사용합니다. 이를 실행 컨텍스트 스택 또는 호출 스택(Call Stack) 이라고 부릅니다.

  • 가장 아래에는 항상 전역 실행 컨텍스트가 깔려 있습니다.
  • 함수를 호출할 때마다 새로운 함수 실행 컨텍스트가 스택의 맨 위에 추가(push)됩니다.
  • 함수의 실행이 완료되면 해당 함수 실행 컨텍스트는 스택에서 제거(pop)됩니다.
  • 스택의 가장 위에 있는 컨텍스트가 현재 실행 중인 코드의 환경입니다.

실행 컨텍스트 스택의 동작 예시

function first() {
    console.log('첫 번째 함수 실행');
    second(); // second 함수 호출
    console.log('첫 번째 함수 종료');
}

function second() {
    console.log('두 번째 함수 실행');
    // third 함수는 호출되지 않음
    console.log('두 번째 함수 종료');
}

first(); // first 함수 호출
console.log('전역 코드 종료');

위 코드가 실행될 때 호출 스택의 변화

  1. 전역 실행 컨텍스트 생성 및 스택에 push. (console.log('전역 코드 종료'); 대기)
  2. first() 호출: first 함수 실행 컨텍스트 생성 및 스택에 push. (console.log('첫 번째 함수 실행'); 실행)
  3. second() 호출: second 함수 실행 컨텍스트 생성 및 스택에 push. (console.log('두 번째 함수 실행'); 실행)
  4. second 함수 종료: second 함수 실행 컨텍스트 스택에서 pop.
  5. first 함수 종료: first 함수 실행 컨텍스트 스택에서 pop.
  6. 모든 전역 코드 실행 종료: 전역 실행 컨텍스트 스택에서 pop.

이러한 스택 구조 덕분에 자바스크립트는 함수 호출 순서를 기억하고, 올바른 실행 흐름을 유지할 수 있습니다.

실행 컨텍스트의 구성 요소 (세부 심화)

하나의 실행 컨텍스트는 논리적으로 다음과 같은 구성 요소를 가집니다.

  1. 변수 환경 (Variable Environment)

    • 현재 컨텍스트 내에서 선언된 변수(var, let, const)와 함수 선언(function)에 대한 정보를 저장합니다.
    • 초기에는 arguments 객체(함수에 전달된 인자들)도 여기에 포함됩니다.
    • 특히 var 변수와 함수 선언은 컨텍스트가 생성되는 시점에 메모리에 할당됩니다. (호이스팅과 관련)
  2. 렉시컬 환경 (Lexical Environment)

    • Variable Environment와 유사하게 현재 스코프 내의 식별자(변수, 함수 선언) 정보를 담고 있습니다.
    • 하지만 Lexical EnvironmentEnvironment RecordOuter Environment Reference로 구성되어 스코프 체인을 형성합니다.
    • Environment Record: 현재 스코프 내의 변수와 함수 선언을 기록하는 곳입니다.
      • 선언적 환경 레코드(Declarative Environment Record): let, const, function 선언을 저장하고 중복 선언을 방지합니다.
      • 객체 환경 레코드(Object Environment Record): var 선언과 함수 선언을 저장하고 window 객체(전역 객체)의 속성으로도 노출됩니다.
    • Outer Environment Reference: 현재 Lexical Environment가 생성될 당시의 외부(상위) Lexical Environment를 참조합니다. 이 참조가 바로 스코프 체인을 형성하는 핵심적인 부분입니다.
  3. this 바인딩 (This Binding)

    • 현재 실행 컨텍스트 내에서 this 키워드가 가리키는 객체를 결정합니다. this 값은 함수가 어떻게 호출되었는지에 따라 동적으로 결정됩니다. (추후 자세히 다룰 내용)

렉시컬 환경과 스코프 체인: 변수 검색의 원리

자바스크립트 엔진이 어떤 변수를 찾을 때, 먼저 현재 실행 컨텍스트의 Lexical Environment에 있는 Environment Record에서 해당 변수를 찾습니다. 만약 찾지 못하면, Outer Environment Reference가 가리키는 외부 Lexical Environment로 이동하여 다시 변수를 찾습니다. 이 과정이 전역 Lexical Environment에 도달할 때까지 반복됩니다. 이 연결된 체인이 바로 스코프 체인이며, 변수 검색의 기본 원리입니다.

let a = 10; // 전역 렉시컬 환경의 Environment Record에 저장

function outer() {
    let b = 20; // outer 함수의 렉시컬 환경의 Environment Record에 저장
    // outer 함수의 Outer Environment Reference는 전역 렉시컬 환경을 가리킴

    function inner() {
        let c = 30; // inner 함수의 렉시컬 환경의 Environment Record에 저장
        // inner 함수의 Outer Environment Reference는 outer 함수의 렉시컬 환경을 가리킴

        console.log(a + b + c); // 10 + 20 + 30 = 60
        // c는 현재 스코프에서 찾음
        // b는 Outer (outer 함수) 스코프에서 찾음
        // a는 Outer (전역) 스코프에서 찾음
    }
    inner();
}
outer();

이러한 스코프 체인 덕분에 중첩된 함수는 외부 함수의 변수에 접근할 수 있게 됩니다.


클로저: 함수와 렉시컬 환경의 조합

이제 실행 컨텍스트와 렉시컬 환경, 스코프 체인에 대한 이해를 바탕으로 클로저 (Closure) 라는 중요한 개념을 살펴보겠습니다.

클로저'함수가 선언될 때의 렉시컬 환경(Lexical Environment)을 기억하여, 함수가 외부에서 호출될 때도 그 렉시컬 환경에 접근할 수 있는 현상' 을 의미합니다. 또는 더 간단히 말해, "자신의 외부 함수가 실행 컨텍스트에서 사라진 이후에도 외부 함수의 변수에 접근할 수 있는 함수" 를 클로저라고도 부릅니다.

조금 복잡하게 들릴 수 있지만, 예시를 통해 이해하면 훨씬 명확해집니다.

function makeCounter() {
    let count = 0; // 이 변수는 makeCounter의 렉시컬 환경에 포함됩니다.

    return function() { // 이 익명 함수가 클로저입니다.
        count++; // 외부 함수(makeCounter)의 count 변수에 접근
        return count;
    };
}

const counter1 = makeCounter(); // makeCounter 호출, 새로운 count=0을 가진 클로저 반환
const counter2 = makeCounter(); // makeCounter 또 호출, 또 다른 count=0을 가진 클로저 반환

console.log(counter1()); // 결과: 1 (counter1만의 count 변수를 증가시킴)
console.log(counter1()); // 결과: 2
console.log(counter2()); // 결과: 1 (counter2만의 count 변수를 증가시킴)
console.log(counter1()); // 결과: 3

클로저가 동작하는 원리

  1. makeCounter() 함수가 호출되면, 새로운 함수 실행 컨텍스트가 생성되고, 그 안에 count 변수를 포함하는 렉시컬 환경이 생성됩니다.
  2. makeCounter()return function() { ... } 이라는 익명 함수를 반환합니다. 이 익명 함수는 자신이 선언될 당시의 렉시컬 환경(makeCounter의 렉시컬 환경, 즉 count 변수가 있는 환경)을 기억합니다. 이 기억된 렉시컬 환경을 클로저 환경(Closure Environment) 이라고도 부릅니다.
  3. makeCounter() 함수의 실행은 끝났고, 그 실행 컨텍스트는 스택에서 제거됩니다. 일반적으로 함수 컨텍스트가 사라지면 그 안의 변수들도 사라져야 합니다.
  4. 하지만 counter1counter2 변수에 할당된 익명 함수(클로저)는 여전히 자신이 기억하는 makeCountercount 변수에 접근할 수 있습니다! 이는 가비지 컬렉터가 count 변수를 회수하지 않고 클로저에 의해 참조되고 있음을 알기 때문입니다.
  5. counter1counter2는 각각 별개의 makeCounter 호출로 생성되었기 때문에, 각자 자신만의 count 변수를 기억하고 유지합니다.

이처럼 클로저는 함수가 생성될 당시의 환경(스코프)을 포획(capture) 하여, 나중에 함수가 실행될 때 그 환경에 접근할 수 있도록 해주는 강력한 메커니즘입니다.

클로저의 활용 사례

클로저는 자바스크립트에서 매우 다양하게 활용됩니다.

  1. 데이터 은닉 (Private Variables): 특정 변수에 외부에서 직접 접근하는 것을 막고, 오직 클로저를 통해서만 접근할 수 있도록 하여 정보 은닉을 구현합니다.

    function createWallet(initialBalance) {
        let balance = initialBalance; // 외부에서 직접 접근 불가
    
        return {
            getBalance: function() {
                return balance;
            },
            deposit: function(amount) {
                balance += amount;
                console.log(`${amount}원 입금. 현재 잔액: ${balance}`);
            },
            withdraw: function(amount) {
                if (balance >= amount) {
                    balance -= amount;
                    console.log(`${amount}원 출금. 현재 잔액: ${balance}`);
                } else {
                    console.log("잔액이 부족합니다.");
                }
            }
        };
    }
    
    const myWallet = createWallet(10000);
    myWallet.deposit(5000);    // 5000원 입금. 현재 잔액: 15000
    myWallet.withdraw(7000);   // 7000원 출금. 현재 잔액: 8000
    console.log(myWallet.getBalance()); // 8000
    // console.log(myWallet.balance); // undefined (balance 변수는 외부에서 직접 접근 불가)
  2. 상태 유지 (State Management): 특정 상태를 기억하고, 그 상태를 기반으로 동작하는 함수를 만들 때 사용됩니다. (위의 makeCounter 예시와 유사)

  3. 콜백 함수 (Callback Functions)와 비동기 처리: setTimeout, 이벤트 핸들러 등 비동기적으로 실행되는 함수는 대부분 클로저의 특성을 활용합니다.

    function delayedMessage(message, delay) {
        setTimeout(function() {
            console.log(message); // 외부 함수의 message 변수에 접근
        }, delay);
    }
    
    delayedMessage("5초 뒤에 출력될 메시지", 5000);

    setTimeout에 전달된 익명 함수는 delayedMessage 함수의 message 변수에 대한 클로저를 형성합니다. delayedMessage 함수는 이미 실행이 끝났지만, 익명 함수는 여전히 message 값을 기억하고 있다가 5초 후에 출력합니다.

  4. 부분 적용 (Partial Application) / 커링 (Currying): 함수에 인자를 미리 일부만 적용하여 새로운 함수를 만드는 기법에 활용됩니다.

    function multiply(a) {
        return function(b) { // 이 내부 함수가 클로저
            return a * b; // 외부 함수의 a 변수에 접근
        };
    }
    
    const multiplyBy5 = multiply(5); // a에 5를 고정시킨 새로운 함수 생성
    console.log(multiplyBy5(10)); // 결과: 50
    console.log(multiplyBy5(20)); // 결과: 100

클로저는 자바스크립트의 비동기성, 함수형 프로그래밍 패러다임과 밀접하게 연결되어 있으며, 자바스크립트의 강력함을 이해하는 데 필수적인 개념입니다.


마무리하며

이번 장에서는 자바스크립트의 코드가 실제로 어떻게 실행되고 변수가 관리되는지에 대한 근본적인 원리인 실행 컨텍스트클로저에 대해 학습했습니다.

실행 컨텍스트는 코드가 실행되는 환경이며, 스택 구조로 관리되어 함수의 호출과 종료에 따라 생성되고 소멸됩니다. 특히 실행 컨텍스트 내부의 렉시컬 환경과 그 안의 Outer Environment Reference 가 어떻게 스코프 체인을 형성하여 변수를 탐색하는지 이해하는 것이 중요했습니다.

그리고 이러한 스코프 체인의 특성과 함수의 렉시컬 환경 포획 능력 덕분에 클로저라는 강력한 개념이 탄생함을 배웠습니다. 클로저는 외부 함수가 소멸된 후에도 외부 함수의 변수에 접근할 수 있게 해주는 현상이며, 데이터 은닉, 상태 유지, 비동기 콜백 등 실제 웹 개발에서 매우 다양하고 유용하게 활용됩니다.

클로저와 실행 컨텍스트는 자바스크립트 개발의 심화 단계로 진입하는 중요한 관문입니다. 처음에는 어렵고 추상적으로 느껴질 수 있지만, 이 개념들을 이해하면 여러분의 자바스크립트 코드에 대한 통찰력이 크게 향상될 것입니다. 다양한 예제를 직접 실행하고, 각 변수가 어떤 스코프에 속하며 어떻게 접근되는지 스스로 분석해보는 연습을 꾸준히 해보시길 바랍니다.