Closure 작동원리와 실제 사용 예시 + 메모리 누수 방지하기

하영·2024년 11월 28일
0

JavaScript

목록 보기
22/29

Closure 정리

클로저(Closure)는 함수와 함수가 선언된 어휘적 환경(Lexical Environment)을 함께 저장하고 있는 객체이다.
네? 🤪
간단하게 말하면 "함수가 외부 함수의 변수에 접근할 수 있는 권한을 가진 상태" 를 뜻한다.

클로저 작동 원리 📝

자바스크립트에서는 함수가 생성될 때 해당 함수가 선언된 환경, 즉 Lexical Environment 를 기억한다. 이 때문에 외부 함수 변수에 접근할 수 있다.

예시 코드 👩🏻‍💻

function outer() {
  const outerVariable = "outer에서 왔어요!";

  function inner() {
    console.log(outerVariable); // outerVariable 에 접근 가능
  }

  return inner;
}

const innerFunction = outer(); // outer 함수 호출
innerFunction(); // ✅ 출력값 : "outer에서 왔어요!"
  1. outer 함수가 실행되면서 outerVariableouter 함수의 로컬 변수로 선언된다.
  2. inner 함수는 outer 함수의 안에 선언되어 있기 때문에 outerVariable 변수에 접근할 수 있게 된다.
  3. outer 함수가 종료되어도 inner 함수는 여전히 outerVariable에 접근할 수 있다.
    why? 이게 바로 inner 함수가 outer 함수의 Lexical Envrionment를 기억하기 때문이다!

실제 사용 예시로 정리해보기 📝

1. 함수 내부의 변수 캡슐화

: 클로저 사용으로 변수에 직접 접근하지 못하게 하고(은닉화), 특정 메서드를 통해서만 접근하여 조작할 수 있도록 한다.

예시 1

function makeCounter() {
  let num = 0; // 은닉화
  // -> ❌ 갑자기 99로 증가하는 등 console.log의 출력값 변경 불가

  return function () {
    return num++; // 내부함수가 외부함수를 참조 (num 값)
  };
}

let counter = makeCounter();

console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2

예시 2

function createCounter() {
  let count = 0; // 외부에서 직접 접근 불가능

  return {
    increment() {
      count++;
      console.log(count);
    },

    decrement() {
      count--;
      console.log(count);
    },
  };
}

const counter2 = createCounter();
counter2.increment(); // 1
counter2.increment(); // 2
counter2.decrement(); // 1

2. 이벤트 핸들러에서 클로저 사용하기

: 이벤트 핸들러 같이 "비동기 작업"에서도 유용하게 처리할 수 있다.

function bindEvent(element, message) {
  element.addEventListener("click", () => {
    console.log(message); // message 기억
  });
}

const button = document.querySelector("button");
bindEvent(button, "버튼 클릭!");

3. 반복문과 클로저

: 반복문에서 변수의 스코프 문제를 해결하기 위해 클로저를 사용할 수 있다.

function createFunctions() {
  const examples = [];

  for (let i = 0; i < 3; i++) {
    examples.push(() => {
      console.log(i);
    });
  }

  return examples
}

const examFunc = createFunctions();
examFunc[0]() // 0
examFunc[1]() // 1
examFunc[2]() // 2

여기서 생긴 의문점!?!🤔

클로저가 계속 메모리를 참조한다면 이게 과연 좋은걸까?
부트캠프 자바스크립트 강의를 들었을 때 메모리 관련해서 공부하면서 "메모리 공간을 계속 차지하는 것은 비용 낭비이고 좋지 않다" 고 배운 기억이 문득 나버렸다..! 그래서 조금 더 찾아보니 역시나 주의점으로 메모리 누수 이야기가 있었다.


클로저 사용시 주의점 - 메모리 누수(Memory Leak) 📝

메모리누수란?

: 프로그램에서 더 이상 사용하지 않는 메모리를 해제하지 못해 메모리가 계속 점유, 사용되는 상황을 뜻한다.
자바스크립트에서는 가비지 컬렉터(Garbage Collector = GC)가 자동으로 쓰지 않는 메모리를 정리하는데, 이렇게 클로저처럼 계속 참조하여 유지되면 가비지 컬렉터는 메모리를 정리할 수 없어 누수가 발생하게 되는 것이다.

클로저는 함수와 함께 환경을 메모리에 저장하는데, 필요 없는 참조를 계속해서 유지해버리면 메모리 누수가 발생하여 비용 리스크가 발생할 수 있다.

관련 예시 코드

function outer() {
  const bigData = new Array(1000000).fill("data");

  return function inner() {
    console.log(bigData[0]);
  };
}

const leak = outer(); // bigData가 클로저로 인해 계속 메모리에 남음
  1. outer 함수 실행 -> largeData라는 큰 배열이 생성
  2. inner 함수 반환 -> 클로저가 largeData를 참조
  3. 반환된 inner 함수는 여전히 largeData를 참조하고 있기 때문에, 가비지 컬렉터가 largeData를 해제하지 못한다.

또 다시 생긴 의문점 🤔

그러면 가비지컬렉터처럼 이 누수를 막아줄 수 있는 방법은 없을까?
가비지컬렉터가 참조하고 있다고 판단해서 삭제하지 못한다면, 우리가 직접 작성해줄 수 없을까?!


메모리누수 해결방법 📝

  • 참조를 명시적으로 제거: 더 이상 사용하지 않는 변수나 객체에 null을 할당하여 참조를 제거하기
  • 불필요한 클로저 생성 방지: 반드시 필요한 경우에만 클로저를 사용하기
  • 이벤트 핸들러 관리: 이벤트 리스너를 등록할 때, 제거할 수 있도록 핸들러를 별도로 관리하기
  • 객체 크기 최소화: 클로저 내부에서 참조하는 데이터는 가능한 작은 크기로 유지하기
function addEventListenerWithClosure() {
  const largeObject = new Array(1000000).fill("data");
  const handler = function () {
    console.log(largeObject[0]);
  };

  const btn = document.getElementById("btn");
  btn.addEventListener("click", handler);

  // 🗑️ 필요하지 않으면 리스너 제거
  btn.removeEventListener("click", handler);
}

addEventListenerWithClosure();


참고자료

profile
왕쪼랩 탈출 목표자의 코딩 공부기록

0개의 댓글

관련 채용 정보