[Javascript] 클로저(Closure)

박세화·2024년 1월 9일
0

Javascript

목록 보기
7/12

📚 이전 글 자바스크립트 호출 스택(Call Stack)의 동작 원리-실행 컨텍스트에서 이어집니다.

📌 클로저(Closure)란?

중첩 함수가 이미 생명주기를 마감한 외부 함수의 변수에 여전히 접근할 수 있을 때, 이때 그 중첩 함수를 클로저 함수라고 한다.

const global = 1;

function outer() {
  const x = 10;
  const inner = function () {
    console.log(x);
  };

  return inner;
}

const rec = outer();
rec();  //10

위 예시에서 outer 함수는 변수 rec 에 할당되는 순간 inner 함수를 리턴하며 실행이 종료된다.
따라서 outer 내부 변수 const x = 10 또한 효력이 없어진다.

하지만 콘솔 결과값을 보면 10이다. 이것은 inner 함수가 실행될 때 이미 종료된 상태였던 outer의 변수 x를 참조했다는 뜻이다. 어떻게 가능할까?

  • const rec = outer() 에서 outer 함수가 호출되며 함수 실행 컨텍스트가 생성된다.
    실행 컨텍스트는 자신의 함수 렉시컬 환경에 바인딩되어있기 때문에, outer 의 함수 렉시컬 환경도 생성된다.

  • const rec = outer() 에 의해 outer가 실행되었으나, 변수에 할당되는 순간 inner 함수를 리턴하고 종료된다고 했다. 따라서 실행 컨텍스트에서 pop 되어 사라진다.

⭐️ 하지만 outer의 함수 렉시컬 환경은 사라지지 않는다.(그림)

정적 스코핑 의 특성으로 인해, 자바스크립트의 함수는 태어나면서(선언되는 시점에) 자신의 상위 스코프를 결정하여 참조할 수 있게 된다고 했다.
inner 함수는 태어나면서 자신의 외부 렉시컬 환경인 outer 의 렉시컬 환경을 참조하게 된다. outer의 실행 컨텍스트는 사라졌지만, 그의 렉시컬 환경은 여전히 존재하기 때문에 참조가 가능하다.

➡️ 그래서 inner 함수 내부에서 console.log(x) 가 실행될 때, 변수 x의 참조값을 outer 의 내부에서 찾아 출력할 수 있는 것이다.
그리고 이때의 outer 함수 내 변수 x 와 같은 변수를 자유변수라 한다.

💡 이렇게 중첩 함수가 이미 생명주기를 마감한 외부 함수의 변수에 여전히 접근할 수 있을 때, 이때 그 중첩 함수를 클로저 함수라고 한다.


📌 어떨 때 쓰면 좋을까?

  1. 상태 유지
  2. 전역 변수 사용 억제
  3. 특정 정보를 은닉해야 할 때

📌 예시코드 4가지

1️⃣

const makeCalculator = (function () {
    let number = 0;
    return function (operator, n) {
        number = operator(number, n);
        return number;
    };
})();

function increase(a, b) {
    return a + b;
}

function multiply(a, b) {
    return a * b;
}

function decrease(a, b) {
    return a - b;
}

function divide(a, b) {
    return a / b;
}

console.log(makeCalculator(increase, 2)); // 2
console.log(makeCalculator(multiply, 6)); // 12
console.log(makeCalculator(divide, 4)); // 3
console.log(makeCalculator(decrease, 7)); // -4

✅ 즉시 실행 함수
한 번의 실행 이후 없어지는 함수

let isAdult;
(function init(age) {
    let currentAge = age;
    if (age >= 20) {
        isAdult = true;
    } else {
        isAdult = false;
    }
})(20);
console.log(isAdult); //  true
console.log(currentAge); //  Uncaught ReferenceError: currentAge is not defined

init 함수는 선언과 동시에 실행되는 즉시 실행 함수이다.

  • 실행될 때 매개변수 20을 통해 currentAge 는 20으로 업데이트된다.
  • 그리고 age 가 20 이상이기 때문에, 조건문에 따라 전역변수 isAdult 가 true로 할당된다.
  • 그리고 init 의 실행이 종료되며, 함수의 지역변수인 currentAge도 사라진다.
    ⭐️ 따라서 콘솔에 currentAge를 출력하려 하면 ReferenceError를 보게 된다. 이미 currentAge는 사라졌기 때문이다.
  • makeCalculator 함수는 즉시실행함수로서, 코드가 실행되자마자 함수 내부가 실행되며 사라진다. 하지만 사라지는 동시에 이런 객체를 반환한다. 편의상 리턴객체 라고 부르겠다.
function (operator, n) {
   number = operator(number, n);
   return number;
};
  • makeCalculator(increase, 2) 은 위의 리턴객체에 각각increase와 2를 대입한 것과 같아진다.

  • 하지만 리턴객체 내부를 실행하려고 보니 number 이라는 변수가 필요하다.
    이 리턴객체는 익명 함수의 클로저이기 때문에, 사라져버린 익명 함수 내부의 변수에 접근이 가능하다.

  • 따라서 첫번째 콘솔로그에서

function (increase, 2) {
   number = increase(0, 2);
   return number;  //2
};

해당 동작이 실행되어 number 값이 2로 업데이트된다.

👀 만약 makeCalculator 를 함수 표현식으로 바꾸면 결과가 똑같을까?

const makeCalculator = () => {
    let number = 0;
    return function (operator, n) {
        number = operator(number, n);
        return number;
    };
};

function increase(a, b) {
    return a + b;
}

function multiply(a, b) {
    return a * b;
}

function decrease(a, b) {
    return a - b;
}

function divide(a, b) {
    return a / b;
}

console.log(makeCalculator()(increase, 2)); // 2
console.log(makeCalculator()(multiply, 6)); // 0
console.log(makeCalculator()(divide, 4)); // 0
console.log(makeCalculator()(decrease, 7)); // -7
  • 함수 표현식으로 함수를 선언하면, 위의 즉시 실행 함수로 선언했을 때와 다르게 함수를 호출할 때마다 새로 실행된다.
  • 현재 makeCalculator 함수 내부에는 let number = 0 변수가 존재한다. 함수가 콘솔로그마다 새로이 실행된다는 건, 이 let number = 0도 새로이 실행된다는 뜻이다.
  • 따라서 number 값이 계속 0으로 초기화되기 때문에 위의 즉시 실행 함수 예시와 값이 다르게 나온다

2️⃣

const counter = () => {
    let value = 0;
    return {
        add: (n) => (value += n),
        getValue: () => {
            console.log(value);
        },
    };
};
let c = counter();
c.add(5);
c.add(9);

c.getValue(); // 14
  • counter 함수는 변수 c에 할당될 때 addgetValue 라는 두 가지 함수 객체를 리턴하면서 실행이 종료된다.

  • add 를 두 번 호출했는데 그 두 번 각각의 매개변수끼리 더해진 값이 getValue 를 통해 얻어지므로, 뭔가 counter 내부에 값을 저장해놓을 만한 것이 필요할 것이다.
    ➡️ 그것이 let value = 0;

  • 초기 value 값을 0으로 해놓은 뒤, 첫 번째 add를 호출하고 매개변수에 5를 대입한다. 이 5를 저장시켜야하므로, value 값에 5를 더해 값을 업데이트한다.
    ⭐️ add: (n) => (value += n)counter의 클로저 함수이기에 let value = 0; 를 참조할 수 있는 것이다

  • add를 한 번 더 호출하고 이번엔 매개변수에 9를 담는다. 좀전에 value 값을 5로 업데이트 해놨고, 이번에도 add 함수를 통해 value 값을 14로 업데이트한다.

  • getValue() 는 최종적인 value 값을 콘솔에 찍어낸다.


3️⃣

const multiply=(x1, x2)=> {
    if (x2) {
      return x1 * x2
    }
    return (n) => {
      return x1 * n
    }
  }

multiply(2, 4); // 8
multiply(3, 5); // 15

const double = multiply(2);
double(2); // 4
double(8); // 16

const hexa = multiply(6);
hexa(6); // 36
hexa(10); // 60
  • multiply 라는 곱셈 함수가 존재하고, 매개변수가 두 개일 때와 한 개일 때가 나뉜다. 따라서 내부에 if문으로 케이스를 나누고, 두 번째 매개변수가 존재한다면 두 변수의 곱을 반환한다.

  • 하지만 두 번째 매개변수가 없다면 클로저 함수를 반환한다.

  • double 이라는 변수에 리턴 함수를 할당하며 multiply 함수의 실행은 종료된다.
    ⭐️ 이때 multiply에 2라는 매개변수가 할당된 것을 보자. 클로저 함수는 상위 함수 스코프의 변수 뿐만 아니라 매개변수 또한 참조할 수 있다.
    따라서 return (n) => { return x1 * n } 이 클로저 함수의 x1multiply의 매개변수 2가 된다.

  • double(2) 는 2 * 2인 4가 된다.


4️⃣

// 보조 함수
function increase(n) {
    return ++n;
}

// 보조 함수
function decrease(n) {
    return --n;
}

// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환한다.

function makeCounter(f) {
    let initial = 0;
    return () => {
        initial = f(initial);
        return initial;
    };
}

const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
  • makeCounter 함수에 보조 함수 increasedecrease를 매개변수로 넣을 수 있다.

  • increaser 를 먼저 보자. makeCounterincreaser에 할당되며 함수를 리턴하고 종료된다. 이때 increaser 를 실행했을 때 어떠한 값이 업데이트되어 저장되어야 한다.

  • 클로저가 참조할 수 있는 변수 선언 라인 let initial = 0 으로 초깃값을 설정한다.

  • 그리고 리턴 함수( = 클로저 함수) 내부에서 increase(n) 함수의 사용을 통해 initial 값을 업데이트 한 후에 리턴한다.

  • 따라서 첫 번째 console.log(increaser()) 에서는 1로 업데이트된 initial 값이 출력이 되며, 두 번째 console.log(increaser()) 에서는 이미 initial 값이 1로 변해있는 상태에서 실행되기 때문에 2로 업데이트되어 출력된다.

  • decreaser 는 increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.



출처
클로저
React Hooks - vanilla JavaScript로 구현하기
실행 컨텍스트
[10분 테코톡] 🍧 엘라의 Scope & Closure
[JS]클로져(closure)와 클로져의 사용 예제
JavaScript - 즉시실행함수(IIFE)

0개의 댓글