자바스크립트 - 클로저

하머·2022년 10월 17일
0
post-custom-banner

  • 클로저는 함수와 함수가 선언된 어휘적 환경(Lexical environment)의 조합이다. (MDN)
  • 어떤 함수 A에서 선언한 변수 a를 참조하는 내부 함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상 (코어 자바스크립트)

클로저의 동작원리


1. 함수가 생성될 때 함수는 Lexical Environment를 생성한다.

Lexical Environment는 식별자와 참조 혹은 값을 기록하는 LexicalEnviormentRecord와 외부 Lexical Enviornment를 참조하는 포인터인 OuterEnvironmentReference를 갖고 있다.

// 실제로 이렇게 동작하진 않지만 개념적으로 이러하다
function foo() {
 const a = 1;
 const b = 2;
 const c = 3;
 function bar() {}

 // 2. 실행 컨텍스트 동작

 // ...
 }

 foo(); // 1. 함수 호출
 
 // 실행 컨텍스트의 렉시컬 환경

 {
   environmentRecord: {
     a: 1,
     b: 2,
     c: 3,
     bar: <Function>
   },
   outer: foo.[[Environment]]
 }

2. 실행 컨텍스트 과정 중 콜스택 최상단 렉시컬 환경 Record에 변수가 없다면 outer의 environment를 참조해 outer의 Record를 찾아본다.

function doSomthing(x) {
  const x = 10;
  function sum(y) {
    return x + y;
  };
  
  return sum;
}
const add = doSomthing(2);
console.log(add(7));

실행 컨텍스트 과정을 쓰자면

  1. 전역 컨텍스트가 콜 스택에 푸쉬
  2. doSomthing 렉시컬 환경의 Record에 변수 x와 함수 sum이 들어간 뒤 doSomthing 컨텍스트 콜 스택에 푸쉬, 이 때 함수 sum의 렉시컬 환경의 outer가 doSomthing으로 기록된다.
  3. doSomthing 컨텍스트에서 x = 10을 할당
  4. 콜스택에서 doSomthing 컨텍스트를 pop
  5. 전역 컨텍스트에서 add = sum을 할당
  6. console.log 함수 호출
  7. add의 컨텍스트 콜 스택에 푸쉬
  8. add의 리턴 과정 중 x가 Record에 없으므로 outer로 기록되어있는 doSomthing의 렉시컬 환경 Record에서 변수를 찾는다.

+) ReferenceError가 일어나는 과정 또한 record => outer => record => outer를 반복해 실행 과정 중 없는 변수를 찾다가 마지막 outer인 전역(Global) 렉시컬 환경의 outer는 없으므로 ReferenceError가 나오게 된다.


실제로 어떻게 쓰지?

  • 클로저의 특성인 변수의 은닉을 통해 클로저 함수에서만 접근 가능하고 외부에서 접근하지 못하는 변수를 만들어 낼 수 있다.

1. VanillaJS

function Counter() {
  // 카운트를 유지하기 위한 자유 변수
  var counter = 0;

  // 클로저
  this.increase = function () {
    return ++counter;
  };

  // 클로저
  this.decrease = function () {
    return --counter;
  };
}

const counter = new Counter();

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

이처럼 private이 존재하지 않는 바닐라 js에서 클로저 함수를 만들어 사용해 private 키워드를 흉내낼 수 있다. (다만 ES2019에서 해쉬 # prefix를 추가해 private class 필드를 선언할 수 있다.MDN - Private class fields)

2. React

// 실제로 이렇게 동작하진 않지만 개념적으로 이러하다
import React from "react";

let state = [];
let setters = [];
let cursor = 0;
let firstrun = true;

const createSetter = (cursor) => {
  return (newValue) => {
    state[cursor] = newValue;
  };
};

const customUseState = (initialValue) => {
  if (firstrun) {
    state.push(initialValue);
    setters.push(createSetter(cursor));
    firstrun = false;
  }

  const resState = state[cursor];
  const resSetter = setters[cursor];
  cursor++;
  return [resState, resSetter];
};

export default function App() {
  cursor = 0;
  const [counter, setCounter] = customUseState(0);

  return (
    <>
      <div>{counter}</div>
      <button onClick={() => setCounter(counter + 1)}>+</button>
      <button onClick={() => setCounter(counter - 1)}>-</button>
    </>
  );
}

이처럼 React의 useState를 통해 생성한 상태를 접근하고 유지하기 위해 useState 바깥에 state를 저장하는데

이 state들은 배열 형식으로 저장되며 상태가 업데이트 되었을 때 컴포넌트 바깥의 변수들이기 때문에 업데이트 한 후에도 이 변수들에 접근이 가능하다.

즉 counter와 setCounter는 컴포넌트 바깥의 private한 state 배열을 참조 및 수정할 수 있는 클로저 함수인 셈이다.


참고자료

post-custom-banner

0개의 댓글