본문 바로가기

Frontend/JavaScript

[JS] 클로저

자바스크립트 클로저(Closure), 제대로 이해해보자

클로저는 처음 접하면 좀 헷갈릴 수도 있지만, 개념만 제대로 잡으면 생각보다 간단한 개념이다.


1. 클로저 정의

  • 내부 함수가 외부 함수의 변수에 접근할 수 있는 현상
  • 외부 함수의 렉시컬환경이 가비지 컬렉팅되지 않는 현상
  • 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상

클로저를 함수라고 표현하는 문장도 많은데, 사실 클로저는 함수라기보다는 현상에 가까움

 


2. 예제로 보기

function outer() {
  const name = '길동';

  function inner() {
    console.log(`안녕, ${name}`);
  }

  return inner;
}

const greet = outer();  // outer 함수 실행
greet(); // 👉 안녕, 길동

 

outer() 함수는 이미 실행이 끝났는데 그 안에 있던 name 변수는 어떻게 greet()에서 접근할 수 있는 걸까? => 클로저 때문

  • 자바스크립트는 함수를 정의할 때, 그 함수가 생성된 시점의 outer lexical environment에 대한 참조를 함께 저장한다
  • 함수는 정의된 위치 기준으로 스코프를 결정하고 환경에 접근할 수 있다
  • 그래서 outer()가 끝나더라도, inner()는 name에 접근가능

3. 왜 이런 구조가 필요할까?

(1) 데이터 보호 (캡슐화)

function createCounter() {
  let count = 0;

  return {
    increase() {
      count++;
      console.log(count);
    },
    decrease() {
      count--;
      console.log(count);
    }
  };
}

const counter = createCounter();
counter.increase(); // 1
counter.increase(); // 2
counter.decrease(); // 1

여기서 count 변수는 외부에서 직접 접근할 수 없고, 오직 increase와 decrease 메서드를 통해서만 접근 가능
이런 걸 데이터 은닉, 또는 캡슐화라고 한다

(2) 특정 상태를 기억

function makeGreeting(greeting) {
  return function(name) {
    console.log(`${greeting}, ${name}`);
  };
}

const sayHello = makeGreeting('안녕');
sayHello('길동');  // 👉 안녕, 길동
sayHello('은우');  // 👉 안녕, 은우

const sayBye = makeGreeting('잘 가');
sayBye('길동');    // 👉 잘 가, 길동

makeGreeting은 실행이 끝났지만, 리턴된 내부 함수는 greeting 값을 기억하고 있음
이렇게 클로저를 활용하면, 일종의 함수 팩토리처럼 쓸 수도 있다


4. 주의할 점

메모리 누수 가능성

클로저는 변수를 기억하고 있기 때문에, 불필요한 변수를 계속 참조하면 가비지 컬렉터(GC)가 정리하지 못할 수도 있다
필요 없어진 함수는 참조를 끊어줘야 함

 

function createHeavyClosure() {
  const largeData = new Array(1000000).fill('*'); // 매우 큰 배열
  return function () {
    console.log('클로저 접근:', largeData[0]);
  };
}

const closures = [];

for (let i = 0; i < 1000; i++) {
  closures.push(createHeavyClosure()); // 클로저가 largeData를 계속 참조함
}

// closures 배열이 존재하는 한 largeData들이 GC 대상이 되지 않음
  • closures 배열이 largeData를 참조하고 있기 때문에 largeData를 GC가 정리하지 못함
  • 즉, 1,000개의 큰 배열이 계속 메모리에 남아 있게 되어 메모리 누수 발생 
function createHeavyClosure() {
  const largeData = new Array(1000000).fill('*');
  return function () {
    console.log('클로저 접근:', largeData[0]);
  };
}

let closure = createHeavyClosure();
closure(); // 사용

// 이제 더 이상 필요 없으므로 참조 해제
closure = null;
closure = null을 통해 참조를 제거하면, 클로저 내부의 largeData도 GC의 대상이 된다
 
function processOnce() {
  const tempData = new Array(1000000).fill('*');
  console.log('처리 중:', tempData[0]);
}
processOnce(); // 한 번 쓰고 끝. 클로저 사용하지 않음

 

클로저 사용을 최소화하거나 꼭 필요한 범위로 제한

나는 아직까지 '클로저를 활용해봐야지' 하고 코드를 작성해본 적이 없다.

클로저가 반드시 필요한 상황이 있나? 나는 항상 쉽게쉽게만 코드를 짰던 것 같다


5. 마무리하면서

  • 클로저는 외부함수의 실행이 끝난 후에도 내부 함수가 외부 함수의 변수에 접근할 수 있는 현상이다.
  • 함수가 정의될 당시의 환경을 기억하기 때문에 가능하다. (outerEnviromentReference 와 관련) 
  • 클로저를 활용하면 상태 저장, 정보 은닉, 함수 커스터마이징 등이 가능하다.
  • 참조가 끝난 시점에는 메모리를 소모하지 않도록 참조를 끊어주자
  • 클로저를 활용해서 코드를 짜는 것보다 코드를 짜고보니 클로저인 경우가 많은듯