JS08-2 클로저

조예진·2022년 3월 12일
0

JavaScript 스터디

목록 보기
12/16
post-thumbnail

본 시리즈는 모던 자바스크립트 Deep Dive 책을 참고하여 작성하고 있습니다.

클로저

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

클로저는 자바스크립트 고유 개념이 아니고, 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.

function outerFunc() {
  const x = 1;

  function innerFunc() {
    console.log(x);
  }

  return innerFunc;
}

const inner = outerFunc();
inner();

위 코드와 같은 상황에서, outerFunc의 실행이 종료되었지만 inner 식별자에 할당된 innerFunc 함수를 통해서 outerFunc 함수의 지역 변수인 x에 접근할 수 있다. 이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우에 생명주기가 종료된 외부 함수의 변수에 접근할 수 있는 중첩 함수를 클로저라고 부른다.

클로저의 활용

클로저는 상태를 안전하게 변경하고 유지하기 위해 사용된다. 즉, 상태를 은닉하고 특정 함수를 통해서만 상태 변경을 허용하는 것이다. 이렇게 하면 상태가 의도치 않게 변경되는 것을 막을 수 있다. 자바스크립트에서는 private와 같은 접근 제어자를 제공하지 않는데, 이를 구현하기 위해 사용된다.

function counter(start) {
  let num = start;
  function getNumber() {
    return num;
  }
  function increase() {
    return num++;
  }
  function decrease() {
    return num--;
  }
  return [getNumber, increase, decrease];
}

const [getNumber, increase, decrease] = counter(3);

increase();
console.log(getNumber()); // 4

이렇게 하면 counter 함수가 가지고 있는 num 지역 변수는 직접 접근이 불가능하고, 중첩 함수를 통해서만 접근하거나 값을 변경할 수 있다.

렉시컬 스코프

렉시컬 스코프(정적 스코프)란, 함수를 어디에 정의했는지에 따라 상위 스코프를 결정하는 것이다. 이 때 스코프의 실체는 실행 컨텍스트의 렉시컬 환경이다. 렉시컬 환경은 ‘외부 렉시컬 환경에 대한 참조'를 통해 상위 렉시컬 환경과 단방향 링크드 리스트 형태로 연결되고, 이는 스코프 체인을 이룬다.

렉시컬 스코프를 다시 정의하면, ‘외부 렉시컬 환경에 대한 참조’에 저장되는 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 위치에 의해 결정되는 것이다.

함수 객체 내부 슬롯 [[Environment]]

렉시컬 스코프가 가능하려면 함수는 자신이 정의되어 있는 위치를 기억해야 한다. 함수가 정의된 위치와 호출되는 위치는 다를 수 있기 때문이다.

function bar() {
  console.log('hi');
}

function foo() {
  bar(); // 전역에서 선언된 함수 foo가 bar 안에서 호출되고 있다
}

foo();

위 경우, foo의 상위 스코프는 전역 스코프가 되어야 한다. 그런데 foo가 호출된 시점의 실행 중인 실행 컨텍스트는 bar 함수의 실행 컨텍스트이다. 이런 상황에서도 자신이 정의된 위치를 알고 있기 위해 [[Environment]] 내부 슬롯을 사용한다.

foo는 전역에 함수 선언문으로 정의되었기 때문에 전역 코드 평가 과정에서 함수 객체를 생성한다. 이 때, [[Environment]] 슬롯에 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조를 저장한다.

함수가 [[Environment]] 슬롯에 가지고 있는 렉시컬 환경 참조는 함수의 상위 스코프를 가리키고, 즉, 함수의 실행 컨텍스트가 생성되었을 때 ‘외부 렉시컬 환경에 대한 참조’에 저장될 값이다.

클로저와 렉시컬 환경

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

const innerFunc = outer();
innerFunc();

위 예제의 경우, 외부 함수인 outer의 호출이 끝난 후에도 innerFunc 식별자를 통해 내부 함수인 inner 함수를 호출할 수 있다. inner 함수는 outer의 생명주기가 끝났지만 여전히 outer의 변수에 접근할 수 있다. 이와 같은 중첩 함수를 클로저라고 한다. (일반적으로 내부 함수가 상위 스코프의 식별자를 참조하고, 내부 함수의 생명주기가 외부 함수의 생명주기보다 긴 경우를 클로저라고 부른다)

클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라고 부른다. 클로저는 함수가 자유 변수에 대해 닫혀있다는 의미이다. (자유 변수에 묶여 있는 함수)

자바스크립트의 모든 함수는 [[Environment]] 내부 슬롯에 자신의 상위 스코프, 즉 렉시컬 환경을 기억한다. 이는 상위 스코프, 즉 외부 함수의 실행 컨텍스트 여부와는 관련이 없다. 따라서 외부 함수가 소멸해도 [[Environment]] 슬롯에 기억하고 있는 렉시컬 환경에 접근해서 외부 함수가 가지고 있던 식별자에 접근하고 값을 변경할 수 있다.

객체는 아무도 자신을 참조하지 않으면 가비지 컬렉션의 대상이 된다. 반대로 누구든 자신을 참조하면 가비지 컬렉션의 대상이 되지 않아 메모리 상에 남아 있게 된다. 만약 상위 스코프의 어떤 식별자도 참조하지 않는다면 최적화를 위해 상위 스코프를 기억하지 않는다. 식별자를 참조하더라도 상위 스코프의 모든 식별자를 기억하지는 않고, 기억해야 하는 식별자만 기억한다.

const x = 1;

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

const innerFunc = outer();
innerFunc();
profile
https://oooooroblog.com 으로 이사갔어요

0개의 댓글