본문 바로가기
전공공부

JavaScript의 클로저와 실행 컨텍스트

by 시아나 2025. 4. 8.

클로저란 무엇인가

  • 외부함수가 종료되어도 클로저 함수는 외부함수의 스코프(함수가 선언된 어휘적 환경)에 접근할 수 있도록 하는 개념
function makeFunc() {
  const name = "Mozilla";
  function displayName() {
    console.log(name);
  }
  return displayName;
}

const myFunc = makeFunc();
myFunc();

이 코드를 실행하면 makeFunc이 종료되어도 makeFunc의 내부 변수 name을 조회할 수 있다.

어떻게 가능한가?

  • 실행 컨텍스트가 외부 함수에 대한 렉시컬 환경에 대한 참조값을 가지고 있어서 상위 스코프에 접근할 수 있다.

이를 이해하기 위한 개념들

  • 스코프

    • 선언된 변수에 대해 접근 가능한 유효범위!
    • 하위 스코프는 상위 스코프에 접근 가능하지만 반대는 불가능
  • 정적 스코프와 동적 스코프

    • 정적 스코프(렉시컬 스코프) : 스코프가 컴파일 타임에 결정된다.

        var name = 'Tom'
      
        function myName() {
          var name = 'Janet';
          getName();
        }
      
        function getName() {
          console.log(name);
        }
      
        myName(); // Tom
    • 동적 스코프 : 런타임시에 스코프가 결정

        var name = 'Tom'
      
        function myName() {
          var name = 'Janet';
          getName();
        }
      
        function getName() {
          console.log(name);
        }
      
        myName(); // Janet
    • JS는 정적 스코프 방식을 택한다.

  • 스코프 체인

    현재 스코프 레벨에서 참조값이 없는 경우 상위 레벨의 스코프에서 참조값을 찾아 나가는 현상

    스코프를 안쪽에서 바깥쪽으로 단계적으로 탐색하는 과정이다.

  • 실행 컨텍스트 : 실행할 코드에 제공할 환경 정보들을 모아놓은 객체

실행 컨텍스트는 복잡한 개념이라 자세히 설명해 보겠다.

실행 컨텍스트(Execution Context)

실행할 코드에 제공할 환경 정보들을 모아놓은 객체

JS 엔진의 Call Stack에 담는 단위이다

처음 프로그램을 실행하면 전역 컨텍스트(Global Context)가 콜스택에 담긴다.

  • 변수 객체, 스코프 체인, this 값에 대한 정보가 담겨 있음

실행 컨텍스트 내부 구조

  • Variable Environment (변수 환경) : 초기화 시점의 변수, 함수 선언 정보 저장
    • Environment Record (번수 저장소) : 실제 식별자와 그 값을 저장하는 공간
    • Outer Lexical Environment (스코프 체인) : 상위 스코프(외부 렉시컬 환경)를 참조하여 스코프 체인을 만듬
  • Lexical Environment (렉시컬 환경) : 변수 환경 + 스코프 체인
  • This Binding : 컨텍스트에서의 this 값

으로 이루어져 있다.

  • Outer Lexical Environment 은 상위 스코프의 Lexical Environment를 참조한다.
  • 때문에 현재 스코프에 없는 값이라도 Outer를 타고 올라가서 상위 스코프에 있는 값을 조회할 수 있다.
function outer() {
  let a = 1;

  function inner() {
    console.log(a); // 여기서 a는 어디서 찾을까?
  }

  inner();
}

해당 예시에서도 스코프 체인이 아래와 같이 연결되어 있기 때문에 inner에서 a를 찾을 수 있는 것이다.

innerContext.LexicalEnvironment
   └── outerContext.LexicalEnvironment
         └── globalContext.LexicalEnvironment

⁉️조금 깊이 들어가 볼까⁉️

ES6 이전에는 variable object / Scope chain / thisValue 로 표현했지만 최신 표준(ES6+)에서는 위와 같의 정의한다. (더 세분화)

ES5에서는 Variable Environment는 var, let, const, function 모두 저장했다.

ES6부터 LexicalEnvironment 에는 let, const, function 을 저장하고

Variable Environment에는 var 변수를 저장한다

프로그램 실행 초기에는 VariableEnvironment와 LexicalEnvironment가 같은 참조(=객체)를 가리킨다.

이후에 let/const가 보이면 새로운 Record가 생성될 수 있다. (var와 let/const는 선언 시기가 다르다)

시점 VE와 LE 관계 var let/const/function
실행 컨텍스트 생성 초기 같은 객체를 참조함 ✅ VE에 저장됨 ❌ 아직 없음
let/const/function 선언 시 LE에만 추가됨 (VE에 이미 있음) ✅ LE에만 저장됨
실행 단계 이후 둘은 구조상 따로 관리됨 (같은 객체였어도!) var 있음 let, const 없음

변수 검색 시 Lexical Environment에서만 탐색한다.

그래서 클로저에 대해 다시 설명하자면

실행 컨텍스트는 함수가 실행이 종료되면 사라진다.

하지만, 클로저란 함수가 선언될 당시의 Lexical Environment를 기억해서

그 환경에 있는 변수들에 함수가 실행된 이후에도 접근할 수 있게 하는 기능

→ "렉시컬 환경이 GC(Garbage Collection) 대상이 되지 않고 살아남기 때문에" 가능한 기능이다.

function outer() {
  let count = 0;

  return function inner() {
    count++;
    console.log(count);
  }
}

const fn = outer();  // 여기서 inner 함수가 반환됨
fn(); // 1
fn(); // 2

원래라면:

  • outer() 실행이 끝나면, 그 안에서 만든 count 변수는 메모리에서 사라져야 함

그런데 실제로는:

  • inner() 함수가 count에 접근하니까
  • JS 엔진은 outer의 LexicalEnvironment를 메모리에서 제거하지 않음
    ➝ 즉, count가 살아있는 상태로 유지됨

실행 컨텍스트로 보는 동작 원리

var food = '🍕';

function outer() {
  var food = '🍔';

  function inner() {
    console.log('I like', food);
  }

  return inner;
}

var myFunc = outer(); // inner 함수 리턴받음
myFunc();             // 호출은 전역에서!

🧭 흐름 설명

순서 설명 실행 컨텍스트 스택 상태
1 프로그램 시작 [Global]
2 outer() 호출 [outer, Global]
3 outer() 내부 실행 완료 → inner 리턴 [Global]
4 myFunc() 호출 (inner() 실행) [inner, Global]
5 console.log(...) 실행 콘솔에 "I like 🍔"
6 inner() 종료 [Global]
7 종료 [Global] 해제됨

outer가 종료되었지만, inner에서 outer를 참조하고 있기 때문에 outer의 실행 컨텍스트는 사라지지 않는다.

크롬에서 해당 코드를 디버깅 해보면

실제 스코프를 볼 수 있다.

outer는 스코프 체인이 local - global 이지만

inner는 스코프 체인이 local - outer - global 이다.

활용방안

  • 함수 팩토리

      function makeAdder(x) {
        return function (y) {
          return x + y;
        };
      }
    
      const add5 = makeAdder(5);
      const add10 = makeAdder(10);
    
      console.log(add5(2)); // 7
      console.log(add10(2)); // 12

    makeAdder는 함수를 만들어 내는 팩토리로 각각 5, 10을 더하는 함수를 만들수 있다.

    여기서 클로저는 add5와 add10으로 같은 로직을 공유하지만 서로 다른 환경을 저장한다.

  • Private 변수 구현

    메서드를 비공개로 선언 / 내부의 다른 메서드만 그 메서드를 호출하도록 함.

      const counter = (function () {
        let privateCounter = 0;
        function changeBy(val) {
          privateCounter += val;
        }
    
        return {
          increment() {
            changeBy(1);
          },
    
          decrement() {
            changeBy(-1);
          },
    
          value() {
            return privateCounter;
          },
        };
      })();
    
      console.log(counter.value()); // 0.
    
      counter.increment();
      counter.increment();
      console.log(counter.value()); // 2.
    
      counter.decrement();
      console.log(counter.value()); // 1.

    이렇게 코드를 구성하면 외부에서 privateCouter 변수와 changeBy 함수에 접근 할 수 없지만

    반환된 increment, decrement, value 함수를 통해 값의 증가, 감소, 조회 기능을 실행할 수 있습니다.

    만약 여러개의 카운터를 생성한다 하더라도

      const makeCounter = function () {
        let privateCounter = 0;
        function changeBy(val) {
          privateCounter += val;
        }
        return {
          increment() {
            changeBy(1);
          },
    
          decrement() {
            changeBy(-1);
          },
    
          value() {
            return privateCounter;
          },
        };
      };
    
      const counter1 = makeCounter();
      const counter2 = makeCounter();
    
      console.log(counter1.value()); // 0.
    
      counter1.increment();
      counter1.increment();
      console.log(counter1.value()); // 2.
    
      counter1.decrement();
      console.log(counter1.value()); // 1.
      console.log(counter2.value()); // 0.

    각각의 클로저는 독립적인 환경을 조성하며, 서로 영향을 미치지 않습니다.

장단점

클로저의 장점

  1. 전역변수 사용의 최소화 / 의도하지 않은 전역변수 값 변경 예방
  2. 데이터 보존 가능
  3. 모듈화를 통한 코드 재사용에 편리
// myModule.js
let x = 5;
export const getX = () => x;
export const setX = (val) => {
  x = val;
};
import { getX, setX } from "./myModule.js";

console.log(getX()); // 5
setX(6);
console.log(getX()); // 6
  1. 정보의 접근 제한(캡슐화)

     function outer() {
       let getY;
       {
         const y = 6;
         getY = () => y;
       }
       console.log(typeof y); // undefined
       console.log(getY()); // 6
     }
    
     outer();

클로저 사용시 주의할점

  • 메모리 사용의 주의
    • 클로저를 사용하면 외부함수의 생명주기가 끝났음에도 가비지 콜렉터에 의해 메모리가 해제되지 않음.

→ 클로저를 할당한 변수에 null을 할당하여 메모리를 해제할 수 있음.

참고

'전공공부' 카테고리의 다른 글

JavaScript 엔진과 동작원리  (0) 2025.03.31
Command 패턴  (0) 2025.03.29
프론트 버블링에 대하여  (0) 2024.12.12
자바스크립트 변수 호이스팅이란?  (1) 2024.11.25
CleanCode 4장, 5장  (1) 2024.11.14