클로저는 무엇일까?

StarSeeker·2021년 9월 14일
0

이 포스팅은 모던자바스크립트 문서의 변수의 유효범위와 클로저 챕터를 보고 학습한 내용을 기억하기 위해 정리한 글입니다.

일단 클로저를 이해하기 위해 변수선언의 스코프와 중첩함수에 대해서 알아야합니다.

변수 선언

자바스크립트에서 변수는 var, let, const 로 선언하여 사용합니다.
가장 중요한 차이점은 var와는 달리 let,const는 블록 스코프를 가진다는 것입니다.
var는 함수 스코프, 전역 스코프를 가집니다.

{
  // 지역 변수를 선언하고 몇 가지 조작을 했지만 그 결과를 밖에서 볼 수 없습니다.

  let message = "안녕하세요."; // 블록 내에서만 변숫값을 얻을 수 있습니다.

  alert(message); // 안녕하세요.
}

alert(message); // ReferenceError: message is not defined

if, while, for 문에서의 블록도 마찬가지입니다.

if (true) {
  let phrase = "안녕하세요!";

  alert(phrase); // 안녕하세요!
}

alert(phrase); // ReferenceError: phrase is not defined

중첩함수

자바스크립트에서는 함수 안에서 함수를 정의해 변수에 담을 수 있습니다.

function sayHiBye(firstName, lastName) {

  // 헬퍼(helper) 중첩 함수
  function getFullName() {
    return firstName + " " + lastName;
  }

  alert( "Hello, " + getFullName() );
  alert( "Bye, " + getFullName() );

}

또한 함수 자체를 반환 할 수도 있습니다.

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

그런데 여기서 의문이 생길 수 있습니다. 밑의 counter 함수를 호출할때 그 안에 있던 count 변수는 어떤 값을 가질까? 독립적으로 값을 가질까? 아니면 하나의 값을 공유하게 될까?
이제 이 부분을 클로저를 학습하면서 이해할 수 있습니다.

렉시컬 환경

일단 렉시컬 환경에 대해서 알아야합니다.
자바스크립트에서는 함수, 코드블록, 스크립트 전체(전역)에 렉시컬환경 이라 불리우는 특별한 객체가 있습니다. 명세에서는 이 객체를 '내부 숨김 연관 객체(internal hidden associated object)' 라고 부릅니다.

이 렉시컬 환경은 두 가지로 나눠지는데,

  1. 환경 레코드(Environment Record) – 모든 지역 변수를 프로퍼티로 저장하고 있는 객체입니다. this 값과 같은 기타 정보도 여기에 저장됩니다.

  2. 외부 렉시컬 환경(Outer Lexical Environment) 에 대한 참조 – 외부 코드와 연관됨

이제 이 렉시컬 환경이 어떻게 생성되는지를 알아보겠습니다.

1. 변수

따라서 우리가 선언하는 지역변수들은 결국 환경 레코드라고 불리우는 객체의 프로퍼티라고 생각하면 됩니다.

이 코드를 보면 환경 레코드에 phrase라는 지역변수가 정의되고 있음을 알 수 있습니다. 또한 outer(외부 렉시컬 환경)이 null을 가르키는 것을 알 수 있습니다.

이는 이 코드가 전역 컨텍스트에서 실행되고 있음을 나타내며 따라서 외부 렉시컬 환경이 없는 것입니다.
또한 변수가 let,const로 선언되기 전uninitialized 라는 상태값을 가지는 것을 알 수 있습니다.

이는 곧 자바스크립트는 let,const 같은 선언 키워드를 만나기 전에도 변수를 인식하고 있지만 사용할 수는 없다는 것을 알 수 있습니다. 만약 let을 만나기전에 phrase변수를 사용하려한다면 initialize 하기전에 접근 할수 없다는 에러가 출력 될 것입니다.

참고: 렉시컬 환경은 명세서에만 존재하는 이론적인 개념이기 때문에 실제로 자바스크립트로 이러한 렉시컬 환경을 조회하거나 수정할 수는 없습니다. 이러한 명세를 구현한 자바스크립트 엔진이 여러방식을 통해 렉시컬 환경을 생성하고 최적화하고 있습니다.

2. 함수

함수의 경우 함수 선언문일 경우를 생각해봅니다.

함수는 변수와는 달리 선언되자마자 초기화가 되었습니다. uninitialized 상태를 가지지 않고 바로 say라는 함수이름과 같은 프로퍼티에 함수가 할당되었습니다.

그런데 자바스크립트에서는 함수를 함수표현식으로 변수에 담을 수도 있습니다. 이런 경우는, 위의 변수의 경우와 마찬가지로 let, const를 만나기 전까지는 uninitialized 상태입니다.

3. 함수의 실행

함수를 실행 시키면 새로운 렉시컬 환경이 만들어 집니다.

위의 say함수를 실행 시켰을때 name이라는 지역변수에 "john"이라는 값이 파라미터로 넘어왔습니다.
이때 렉시컬환경의 환경 레코드에 name프로퍼티에 "john"이라는 값이 저장되었고 이때 외부 렉시컬 환경은 전역 렉시컬 환경을 가리키고 있는 것을 알 수 있습니다.

이런식으로 함수를 실행시킬 때 (새로운 렉시컬 환경이 만들어 질 때)는 그 함수가 정의된 렉시컬 환경이 외부 렉시컬 환경이 됩니다.

이때 name과 phrase 변수의 값은 어떻게 탐색 할까요?

name의 경우 자신의 지역 렉시컬 환경에 값이 있기때문에 그대로 가져옵니다. 하지만 phrase의 경우에는 자신의 렉시컬 환경에 없기 때문에 외부에서 변수값을 찾습니다.
이는 변수값을 찾지 못한다면 전역 렉시컬 환경까지 계속 탐색작업이 진행 됩니다.

4. 중첩함수 일때의 렉시컬 환경

앞서본 중첩함수의 예시를 다시 살펴봅니다.

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

이때 렉시컬환경은 makeCounter 함수의 렉시컬 환경이 생성됩니다.

그리고 makeCounter의 렉시컬 환경의 외부 렉시컬환경은 전역 렉시컬 환경입니다.
여기서 반환하는 함수에 주목해봅니다.
함수는 반환했지만 이 함수는 실행한 함수는 아닙니다. 함수 값만 반환 하고 있죠.

이때 중요한 지점이 있습니다. 함수는 자신이 생성된 렉시컬 환경을 기억합니다.
함수의 [[Environment]] 라는 숨김 프로퍼티에 자신이 생성된 렉시컬 환경에 대한 참조가 저장됩니다.

따라서 count++;을 반환하는 함수는 자신이 생성된 makeCounter의 렉시컬 환경을 기억하고 있습니다.

따라서 앞으로 counter 변수를 함수로써 실행하게 되면 실행할 때마다
렉시컬 환경이 생성 되고 이 렉시컬 환경의 외부 렉시컬 환경은 [[Environment]]가 참조하고 있는 렉시컬 환경이 됩니다.(위에서는 count:0 을 가지고 있는 렉시컬 환경)

만약 counter 함수를 실행시키게 되면,
자신 안에 있는 count 변수를 자신의 렉시컬환경에서 찾고 위의 변수 챕터에서 본것 처럼 전역 렉시컬 환경까지 count 변수를 찾습니다.

바로 다음 외부렉시컬 환경에서 count 변수를 찾았습니다!!

함수 호출이 끝나게 되면 count는 1로 수정된 것을 알 수가 있습니다.
여기까지 오면, 처음의 궁금증을 풀수 있습니다.
결국 counter가 여러번 호출되면 매번 다른 렉시컬 환경을 가지더라도 같은 렉시컬 환경을 외부 렉시컬환경으로 바라보게됩니다.
따라서 그 안에있는 count 변수는 어떤 count 함수를 호출하더라도 같은 값을 공유하게 됩니다.
count 함수가 호출되면 count 값이 계속 1씩 늘어나는 이유입니다.

클로저는 이렇듯 외부 변수에 접근 할 수 있는 함수를 이야기 합니다. 함수를 이용해서 특정한 변수값을 수정하고 제어할 수 있고 이는 외부에서 접근할 수 없는 변수이기 때문에 오직 클로저를 통해서만 변경 할 수 있습니다. 이런 점이 클로저의 특징이기도 합니다.

profile
춤추듯 개발하고 싶은 사람

0개의 댓글