JavaScript

클로저 (Closure)

추운날_너를_기다리며 2024. 8. 16. 20:56

클로저란, 사전에서 뜻을 찾아보면 폐쇄라는 뜻을 가지고 있습니다.

함수가 선언될(생성될) 그 당시에 주변의 환경과 함께 갇히는 것을 말합니다.

즉, 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합입니다.

또 다른 말로, 함수가 속한 렉시컬 스코프를 기억하며, 함수가 렉시컬 스코프 밖에서 실행될 때도 이 스코프에 접근할 수 있게 해주는 기능입니다.

 

여기서 렉시컬 스코프란 함수가 선언이 되는 위치에 따라서 상위 스코프가 결정되는 스코프입니다.

 

결국, 내부함수는 외부함수의 지역변수에 접근할 수 있는데 외부함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수에 접근할 수 있는 것을 말합니다.

 

이제부터 다양한 예시를 통해 알아보겠습니다.

const x = 1;

function outerFunc() {
  const x = 10;
  function innerFunc() {
    // x는 어디서 참조할까요 ???
    // 함수가 선언된 렉시컬 환경!!!
    // 함수가 선언될 당시의 외부 변소 등의 정보!!
    console.log(x); // 10
  }

  innerFunc();
}

outerFunc();

위에서 보면 전역변수로 x = 1이라는 값이 들어가고 outerFunc();로 함수가 실행이됩니다. 

그럼 outerFunc()함수 제일 밑에 선언된 innerFunc()가 실행이 되는데 이때 innerFunc()가 선언되어 있는 곳의 바로 밖에 scope인 outerFunc에서 선언된 x = 10이 console에 찍히게 됩니다.

 

이제 좀 어려운 예시로 가겠습니다.

 

const x = 1;

// outerFunc내에 innerFunc가
// '호출'되고 있음에도 불구하고
function outerFunc() {
  const x = 10;
  innerFunc(); // 1
}

// innerFunc와, outerFunc는 서로
// 다른 scope를 가지고 있다!!!
function innerFunc() {
  console.log(x); // 1
}

outerFunc();

전역에 x = 1이 선언되어있고 outerFunc()에 의해서 함수가 호출이 됩니다.

그럼 outerFunc()에 의해서 innerFunc()가 호출이 되는데 이때 innerFunc()함수가 선언되는 위치의 상위 스코프인 전역에 선언된 x = 1에 의해서 1이 콘솔에 찍힙니다.

 

대충 이제 JS엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프가 결정된다는 것을 알 수있습니다.

 

또 다른 예로 들겠습니다.

function foo() {
    const x = 1;
    const y = 2;

    // 클로저의 예
    // 중첩 함수 bar는 외부 함수(foo)보다 더 오래 유지되며
    // 상위 스코프의 식별자(foo)를 참조한다.
    function bar() {
        debugger;
        console.log(x); // 1
    }
    return bar;
}

const bar = foo();
bar();

설명할 것도 없이 bar()함수의 상위 스코프인 x =1; 즉, 1이 콘솔로 찍힙니다.

 

그럼 이제 클로저는 어려운데 왜쓸까요? 클로저는 주로 상태를 안전하게 변경하고 유지하기 위해 사용합니다.

즉, 상태를 안전하게 은닉한다(특정 함수에게만 상태 변경을 허용한다)는 표현을 기억해야 합니다.

 

일단 클로저로 쓰이지 않아서 보안문제가 있는 예제부터 보겠습니다.

// 카운트 상태 변수
let num = 0;

// 카운트 상태 변경 함수
const increase = function () {
    // 카운트 상태를 1만큼 증가시킨다.
    return ++num;
};

console.log(increase());
// num = 100; // 치명적인 단점이 있어요.
console.log(increase());
console.log(increase());

여기서 만약 num = 100;을 저 중간에 넣게 되면 우리가 원래 원했던 호출 1 2 3 가 나와야 했던게 1 101 102 가 나오게 됩니다. 

이를 위해서 클로저 개념이 들어가야 하는 겁니다.

 

모든 것을 보완한 클로저를 적어 증가 감소 함수를 만들어 보겠습니다.

const counter = (function () {
    //카운트 상태 변수
    let num = 0;

    // 클로저인 메서드(increase, decrease)를 갖는 객체를 반환한다.
    // property는 public -> 은닉되지 않는다.
    return {
        increase() {
            return ++num;
        },
        decrease() {
            return num > 0 ? --num : 0;
        },
    };
})();

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

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

console.log(counter.increase()); // 1 첫번째 이부분이 실행이 되면 변수 num값이 increase 메서드 안에 또 선언 또는 할당이 없기 때문에 외부 let num = 0;의 num 값에서부터 시작을 합니다.

모든 함수들이 실행 됨에따라서 1 2 1 0이 찍히게 됩니다.

 

결론, 클로저는 데이터의 상태를 내가 원할 때 변경시키고 그 값을 유지하기 위해서 사용을 하는데, 이때 우리가 지켜야할 데이터는 데이터를 바꾸는 함수 즉, 실행시킨 위치가 아닌 함수 선언 위치 바로 외부 스코프에 선언을 시켜준 후 그 데이터를 변경하고 외부에서 실수로 바뀌지 않게 해주기 위한 기능이라고 보면 될 것 같습니다.

이걸 보면서 지금까지 나는 개념도 모르고 사용했구나 싶습니다.