본문 바로가기

개발자 강화/프론트엔드

[매일메일] 클로저란? (FE.241212)

클로저

외부 함수의 변수를 내부 함수가 계속해서 참조할 수 있도록 하는 매커니즘

함수가 생성될 때 렉시컬 스코프(lexical scope)를 기억하며, 함수가 실행될 때도 그 스코프에 계속 접근할 수 있음

 

자바스크립트의 함수가 일급 객체라는 특성 + 렉시컬 스코프의 조합으로 만들어짐

 

function outerFunction(outerVariable) {
  return function innerFunction(innerVariable) {
    console.log('Outer Variable: ' + outerVariable);
    console.log('Inner Variable: ' + innerVariable);
  };
}

const newFunction = outerFunction('outside');
newFunction('inside');

 

1. outerFunction('outside')가 실행되면, outerVariable에는 'outside'가 저장됨

2. outerFunction 내부에서 innerFunction을 반환함

3. 반환된 innerFunction을 newFunction에 저장함

4. newFunction('inside')를 호출하면 innerFunction이 실행됨

5. innerFunction 내부에서 outerVariable에 접근함. 이미 outerFunction 실행이 끝났지만 여전히 접근할 수 있음.

 

내부 함수(innerFunction)은 자신이 선언된 환경(outerFunction의 실행환경)을 기억하고, 그 스코프에 접근할 수 있음

-> 클로저는 함수가 생성될 때 환경을 기억하고 활용하는 기능을 의미함


클로저의 활용

변수와 함수의 접근 범위를 제어하고 특정 데이터와 상태를 유지하기 위해 자주 활용됨

 

 

1. 데이터 은닉(Encapsulation):외부 접근을 막고 데이터의 무결성을 유지함

클로저는 외부에서 접근할 수 없는 비공개(private) 변수와 함수를 만들 수 있음

특정 함수 내부에서만 접근 가능한 변수를 생성하고, 이를 조작할 수 있는 함수만 외부로 노출해 관리

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

  return {
    increment: function () {
      count++;
      console.log(`Count: ${count}`);
    },
    decrement: function () {
      count--;
      console.log(`Count: ${count}`);
    },
    getCount: function () {
      return count;
    }
  };
}

const myCounter = counter();
myCounter.increment(); // Count: 1
myCounter.increment(); // Count: 2
myCounter.decrement(); // Count: 1

console.log(myCounter.getCount()); // 1
console.log(myCounter.count); // undefined (외부에서 접근 불가능)

count 변수는 외부에서 직접 변경할 수 없기 떄문에 잘못된 값이 할당되는 것을 방지할 수 있음

getCount, increment, decrement 메서드를 통해서만 count 값을 조작할 수 있음

 

2. 비동기 작업: 이전의 실행 컨택스트를 유지해야 할 때

비동기 작업을 수행할 때 클로저로 함수 실행 시점의 변수 상태를 유지할 수 있음

function createLogger(name) {
  return function() {
    console.log(`Logger: ${name}`);
  };
}

const logger = createLogger('MyApp');
setTimeout(logger, 1000); // 1초 후에 'Logger: MyApp' 출력

 

1. createLogger('MyApp')을 호출하면, 내부 함수가 반환됨.

2. 내부 함수가 setTimeout(logger, 1000);에 전달되면서 1초 후에 실행됨

3. 실행될 때 name 변수의 값 'MyApp'을 유지하고 있기 때문에 'Logger: MyApp'이 정상적으로 출력됨

 

만약 name 변수를 setTimeout 내부에서 선언했다면, 1초 후 실행될 때 name을 찾을 수 없음

클로저는 외부 함수의 변수를 유지한 채 실행할 수 있음

 

3. 모듈 패턴 구현: 필요한 함수와 데이터만 외부로 노출해 모듈 패턴을 쉽게 구현

모듈 패턴: 하나의 객체를 생성하고, 내부 변수와 메서드를 캡슐화하면서 필요한 것만 외부로 노출하는 방식

(데이터 은닉과 다른 점이 무엇인지 개인적으로 의문..??)

const UserModule = (function () {
  let users = []; // 외부에서 직접 접근 불가능

  return {
    addUser: function (name) {
      users.push(name);
      console.log(`${name} added`);
    },
    getUsers: function () {
      return users.slice(); // 배열 복사본 반환 (직접 조작 방지)
    }
  };
})();

UserModule.addUser('Alice');
UserModule.addUser('Bob');

console.log(UserModule.getUsers()); // ['Alice', 'Bob']
console.log(UserModule.users); // undefined (직접 접근 불가능)

내부 변수(users)는 외부에서 직접 수정할 수 없고, 지정된 함수(addUser, getUsers)를 통해 조작 가능

유지보수가 쉬워지고, 코드 응집력이 높아짐

 


클로저 사용 시 주의할 점

1. 메모리 누수(Memory Leak)

클로저는 외부 변수에 대한 참조를 유지하므로, 불필요한 메모리를 차지할 수 있음

필요하지 않은 클로저는 null 할당을 통해 제거하는 것이 좋음

function outer() {
  let data = new Array(1000000).fill('data'); // 큰 데이터
  return function inner() {
    console.log(data[0]); // 클로저로 인해 'data'가 유지됨
  };
}

let myFunc = outer();
myFunc(); // 'data' 출력

myFunc = null; // 클로저 해제 (GC가 수거 가능)

 

2. 예기치 않은 값 유지

클로저는 변수를 참조(reference) 하므로, 반복문에서 예기치 않은 결과 초래할 수 있음

function createFunctions() {
  let funcs = [];

  for (var i = 0; i < 3; i++) {
    funcs.push(function () {
      console.log(i);
    });
  }

  return funcs;
}

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

var가 함수 스코프를 가짐. var i는 하나의 공유된 변수이므로, 반복문이 끝난 후 i의 최종 값이 모든 함수에서 사용됨.


아니 이거 솔직히 구라같은데;;이생각 하면서 실제로 테스트해봤는데

진짜 var로 하면 결과 다 3으로 뜸... 개신기하다...

 

2.1. 예기치 않은 값 해결 방법

1. let을 사용해 블록 스코프 유지

function createFunctions() {
  let funcs = [];

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

  return funcs;
}

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

for (let i=0; i<3; i++)

 

let은 블록 스코프를 가지므로, for 루프의 반복마다 새로운 i 변수가 생성됨

funcs[0]은 i=0인 상태에서 생성된 함수이고, funcs[1]은 i=1, funcs[2]은 i=2인 상태에서 생성된 함수

결과적으로 funcs[0](), funcs[1](), funcs[2]()가 각각 다른 i 값을 올바르게 출력

 

2. 즉시 실행 함수(IIFE, Immediately Invoked Function Expression)

각 반복에서 새로운 스코프 생성

function createFunctions() {
  let funcs = [];

  for (var i = 0; i < 3; i++) {
    (function (index) {
      funcs.push(function () {
        console.log(index);
      });
    })(i);
  }

  return funcs;
}

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

 

function(index) { ... }(i) <- 즉시 실행 함수(IIFE)

i 값을 즉시 실행 함수의 매개변수 index로 전달하여, 새로운 스코프에서 index를 고정시키고 클로저를 생성함

funcs[0], funcs[1], funcs[2]는 각각 다른 index 값을 기억하는 함수가 됨

 

3. bind()를 사용해 클로저 생성

bind()로 함수 내부에서 i 값을 고정

function createFunctions() {
  let funcs = [];

  for (var i = 0; i < 3; i++) {
    funcs.push(
      function () {
        console.log(this);
      }.bind(i)
    );
  }

  return funcs;
}

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

.bind(i)를 사용하면, 함수의 this가 i 값으로 고정됨

funcs[0], funcs[1], funcs[2]가 각각 올바른 i 값을 참조함

 


출처

[1] 매일메일. 241212. 클로저에 대해서 설명해주세요. 2번. https://maeil-mail.kr 

[2] gpt에게 클로저에 대해 묻다.