들어가며..

자바스크립트는 함수 지향 언어입니다. 이런 특징은 개발자에게 많은 자유도를 줍니다. 함수를 동적으로 생성할 수 있고, 생성한 함수를 다른 함수에 인수로 넘길 수 있으며, 생성된 곳이 아닌 곳에서 함수를 호출할 수도 있기 때문입니다.

함수 내부에서 함수 외부에 있는 변수에 접근할 수 있다는 사실은 앞서 학습해서 알고 계실 겁니다.

그런데 함수가 생성된 이후에 외부 변수가 변경되면 어떤 일이 발생할까요? 함수는 새로운 값을 가져올까요? 아니면 생성 시점 이전의 값을 가져올까요?

매개변수를 통해 함수를 넘기고 이 함수를 저 멀리 떨어진 코드에서 호출할 땐 어떤 일이 발생할까요? 함수는 호출되는 곳을 기준으로 외부 변수에 접근할까요?

이젠 이런 간단한 시나리오부터 시작해 좀 더 복잡한 시나리오를 다룰 수 있도록 지식을 확장해 봅시다.

클로저

‘클로저(closure)' 는 개발자라면 알고 있어야 할 프로그래밍 용어입니다.

클로저외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미합니다. 몇 몇 언어에선 클로저를 구현하는 게 불가능하거나 특수한 방식으로 함수를 작성해야 클로저를 만들 수 있습니다. 하지만 자바스크립트에선 모든 함수가 자연스럽게 클로저가 됩니다. 예외가 하나 있긴 한데 자세한 내용은 ‘new Function’ 문법에서 다루도록 하겠습니다.

요점을 정리해 봅시다. 자바스크립트의 함수는 숨김 프로퍼티인 [[Environment]]를 이용해 자신이 어디서 만들어졌는지를 기억합니다. 함수 내부의 코드는 [[Environment]]를 사용해 외부 변수에 접근합니다.

프런트엔드 개발자 채용 인터뷰에서 “클로저가 무엇입니까?“라는 질문을 받으면, 클로저의 정의를 말하고 자바스크립트에서 왜 모든 함수가 클로저인지에 관해 설명하면 될 것 같습니다. 이때 [[Environment]] 프로퍼티와 렉시컬 환경이 어떤 방식으로 동작하는지에 대한 설명을 덧붙이면 좋습니다.

변수의 유효범위와 클로저

함수 내부에서 함수 외부에 있는 변수에 접근할 수 있다는 사실은 맞다.

그런데 함수가 생성된 이후에 외부 변수가 변경되면 어떤 일이 발생할까?

  1. 새로운 함수를 가져온다
  2. 생성 시점 이전의 값을 가져온다.

또한 매개변수를 통해 함수를 넘기고 이 함수를 저 멀리 떨어진 코드에서 호출할 땐 어떤 일이 발생할까? 함수는 호출 되는 곳을 기준으로 외부 변수에 접근할까?

중첩함수

function makeCounter() {
  let count = 0;

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

let counter = makeCounter();

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

그런데 makeCounter를 살펴보다 보면 “counter를 여러 개 만들었을 때, 이 함수들은 서로 독립적일까? 함수와 중첩 함수 내 count 변수엔 어떤 값이 할당될까?” 같은 의문이 들기 마련입니다.

렉시컬 환경

1단계: 변수

실행중인 함수, 코드블록, 스크립트 전체를 갖는다.

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

  1. 외부 렉시컬 환경(Outer Lexical Environment)에 대한 참조
    • 외부 코드와 연관되어있음

‘변수’는 특수 내부 객체인 환경 레코드의 프로퍼티일 뿐이다.

변수를 가져오거나 변경 = 환경 레코드의 프로퍼티를 가져오가나 변경

  • 렉시컬 환경이 하나만 존재

👀 한 줄 한 줄 실행될 때마다 어떻게 변화하는지 살펴보자.

(네모 상자: 환경 레코드 / 붉은 화살표: 외부 참조)

  1. 스크립트가 시작되면 선언한 변수 전체가 렉시컬 환경에 올라간다. (pre-populated)
    • 이때 변수의 상태는 특수 내부 상태(special internal state)인 ‘uninitialized’가 됩니다. 자바스크립트 엔진은 ‘uninitialized’ 상태의 변수를 인지하긴 하지만, let을 만나기 전까진 이 변수를 참조할 수 없습니다.
  2. let phrase가 선언되었지만 프로퍼티 값은 undefined이다.
  3. 값 할당
  4. 값 변경

현재까지 배운 내용 요약 💁‍♀️

  1. 변수는 특수 내부 객체인 환경 레코드의 프로퍼티이다. 환경 레코드는 현재 실행 중인 함수와 코드 블록, 스크립트와 연관되어 있다.
  2. 변수를 변경하면 환경 레코드의 프로퍼티가 변경된다.

렉시컬 환경은 JS가 어떻게 동작하는지 설명하는 데 쓰이는 이론상의 객체이다. 따라서 코드를 사용해 직접 렉시컬 환경을 얻거나 조작하는 것은 불가능하다.

2단계: 함수 선언문

함수 선언문으로 선언한 함수는 일반 변수와 달리 바로 초기화된다는 점에서 차이가 있다.

아래 스크립트에 함수를 추가했을 때 전역 렉시컬 환경 초기 상태가 어떻게 변하는지 보여준다.

let say = function(name)..

이런 동작 방식은 함수 선언문으로 정의한 함수에만 적용된다. 위와 같이 함수를 변수에 할당한 함수 표현식은 해당하지 않는다.

3단계: 내부와 외부 렉시컬 환경

함수를 호출해 실행하면 새로운 렉시컬 환경이 자동으로 만들어진다. 이 렉시컬 환경엔 함수 호출 시 넘겨받은 매개변수와 함수의 지역 변수가 저장된다.

함수가 호출 중인 동안 👀

  1. 호출 중인 함수를 위한 내부 렉시컬 환경
  2. 내부 렉시컬 환경이 가리키는 외부 렉시컬 환경

두개를 갖게 된다.

  • 내부 렉시컬 환경 - name : “Jone” / 현재 실행 중인 함수인 say에 상응한다.

  • 외부 렉시컬 환경 - say function, phrase : “Hello”

코드에서 변수에 접근할 땐, 먼저 내부 렉시컬 환경을 검색 범위로 잡는다 내부 렉시컬 환경에서 원하는 변수를 찾지 못하면 검색 범위를 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경으로 확장한다. 이 과정은 검색 범위가 전역 렉시컬 환경으로 확장될 때까지 반복된다.*

내부 렉시컬 환경에 name을 찾았지만 phrase를 찾지 못해 외부 렉시컬 환경으로 확장해 검색한다.

4단계 : 반환 함수

function makeCounter() {
  let count = 0;

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

let counter = makeCounter();

makeCounter()를 호출하면 호출할 때 마다 새로운 렉시컬 환경 객체가 만들어진다. 그리고 이 렉시컬 환경 개체 엔 makeCounter를 실행하는데 필요한 변수들도 저장된다.

여기서 중첩함수(반환 함수)가 만들어지면 어떻게 될까?

중요한 사실 하나 ❗️

모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다. 🤔

함수는 [[Environment]]라 불리는 숨김 프로퍼티를 갖는데, 여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.

따.라.서.

counter[[Environment]]엔 { counter: 0 }이 있는 렉시컬 환경에 대한 참조가 저장된다.

  1. 함수가 생성될 때 딱 한 번 그 값이 세팅된다.
  2. 이 값은 영원히 바뀌지 않는다.

실행 흐름이 중첩 함수의 본문으로 넘어오면 count 변수가 필요한데, 먼저 자체 렉시컬 환경에서 변수를 찾는다. 익명 중첩 함수엔 지역 변수가 없기 때문에 이 렉시컬 환경은 비어있다. 이제 counter()의 렉시컬 환경이 참조하는 외부 렉시컬 환경에서 count를 찾자.

count()를 여러 번 호출하면 count 변수가 2, 3으로 증가하는 이유이다.

가비지 컬렉션

자바스크립트에서 모든 객체는 도달 가능한 상태일 때만 메모리에 유지되고 나머지는 삭제된다.

그런데 호출이 끝나도 여전히 도달 가능한 함수가 있을 수 있다. 이때는 중첩함수의 [[Environment]] 프로퍼티에 외부 함수에 대한 렉시컬 환경에 대한 정보가 저장된다

이론상으로 그렇겠지만 사실 JS엔진이 최적화를 시켜주기 때문에 신경쓰지 않아도 된다.

하지만 종종 디버깅 이슈가 존재한다.

클로저의 활용

내부에서 외부 데이터를 사용하고자 할 때 !!

  1. 함수를 분리
  2. bind 사용 ⇒ 하지만 여러가지 제약 사항 발생
  3. 고차함수를 사용하여 함수를 인자로 받거나 리턴한다.

캡슐화와 정보은닉 !!

클로저를 활용하면 public과 private한 값을 분리할 수 있다.

상태를 안전하게 변경하고 유지하기 위해 사용한다. 다시말해 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.

let num = 0;

const increase = function () {
	return ++num;
};

console.log(increase());
console.log(increase());
console.log(increase());
  • 카운트 상태는 increase 함수가 호출되기 전까지 변경되지 않고 유지되어야 한다.
  • 이를 위해 카운트 상태는 increase 함수만이 변경할 수 있어야 한다.

increase 함수만이 num 변수를 참조하고 변경할 수 있게 하는 것이 바람직하다.

const increase = function () {
	let num = 0;

	return ++num;
}

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

하지만 이전의 상태값을 유지하지 못한다.

const increase = (function() {
    let num = 0;

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

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

Reference