본문 바로가기

TIL

[TIL-182] 모던 자바스크립트 24장 클로저

24장 클로저

 : 함수와 함수가 선언된 렉시컬 환경의 조합

함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 특성.

 

24.1 렉시컬 스코프

실행 컨텍스트 관점에서 렉시컬 스코프(정적 스코프) : 함수를 정의한 위치에 따라 상위 스코프(외부 렉시컬 환경에 대한 참조를 통해 연결)를 결정하는 것. 호출 위치가 아니라 정의 위치에 의해 정적으로 결정되며 변하지 않는다.

(렉시컬 스코프가 그 자체로 특정한 스코프인 것은 아니고, 스코프를 결정하는 방식(동적/정적) 중 하나.)

 

 

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

 : 상위 스코프(자신이 정의된 환경)의 참조를 저장하는 슬롯

함수 객체를 생성할 때 [[Environment]]에 저장하므로, 상위 스코프의 참조는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경을 가리키게 됨. 함수 정의 평가 시점의 "실행 중 실행 컨텍스트"는 함수가 정의된 상위 함수(또는 전역)의 실행 컨텍스트이기 때문.

(함수의 [[Environment]]는 환경, 말 그대로 함수가 정의된 (실행 중인 실행 컨텍스트의 렉시컬) "환경"을 기록해둔 것이고, 그 환경은 상위 스코프를 의미한다고 생각하면 쉬울 듯)

[그림 24-1]

 

 

24.3 클로저와 렉시컬 환경

클로저 : 외부 함수보다 오래 지속되어, 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있는 중첩 함수

[예제 24-5]

클로저의 정의에서 "함수가 선언된 렉시컬 환경"이란 상위 스코프. 중첩 함수가 이미 생명 주기 끝난 외부 함수의 렉시컬 환경상위 스코프로 참조함으로써 소멸되지 않음.

모든 함수가 상위 스코프를 참조하고 있기 때문에 이론적으로 클로저지만, 일반적으로 (1)외부 함수보다 더 오래 생존하는 중첩 함수이며 (2)상위 스코프의 식별자를 참조하고 있어야 위와 같은 현상이 일어나 클로저라 부른다.

상위 스코프의 식별자를 참조하지 않으면 브라우저가 최적화를 통해 더이상 상위 스코프를 기억하지 않는다. 또한 클로저가 참조하고 있지 않은 상위 스코프의 식별자도 기억하지 않는다. 클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라 한다. 클로저는 자유 변수에 묶여있는 함수이다.

 

 

24.4 클로저의 활용

클로저의 필요성

클로저의 사용 목적 : 상태를 안전하게 변경하고 유지

[예제 24-9] 카운트 상태가 전역 변수라서 increase 함수가 호출되기 전에 다른 방식으로 접근, 변경될 수 있다.

[예제 24-10] 그렇다고 카운트 상태를 increase 함수 안에서 선언하면 increase가 실행될때마다 선언, 초기화되어 이전 상태를 유지하지 못한다.

 

클로저로 문제 해결

[예제 24-11] 이럴 때 클로저를 사용하면, 카운트 상태가 자유 변수로서 클로저만이 참조할 수 있게 보호된다.

즉시 실행함수를 이용하여, increase 함수가 평가되는 최초에 카운트 상태가 생성된 후 반환되는 함수에 의해 참조됨으로써 즉시 실행 함수의 생명 주기가 종료되어도 살아남는다.

 

생성자 함수에 클로저 활용

[예제 24-13] 생성자 함수의 메서드도 클로저가 될 수 있다.

생성자 함수에 즉시 실행 함수를 할당하여 그 안에서 자유 변수를 선언하고, 자유 변수를 참조하는 메서드를 가진 새로운 함수를 반환한다. 이 메서드들은 자신이 속한 함수가 평가되어 함수 객체가 될 때 실행 중인 실행 컨텍스트인 즉시 실행 함수를 상위 스코프로 기억하는 클로저가 된다. 프로토타입 메서드라도 이 생성자 함수로 생성한 인스턴스는 클로저 메서드들을 상속받아 자유 변수를 참조할 수 있다.

 

고차 함수에 클로저 활용

[예제 24-14] 인수로 전달 받은 보조 함수에 따라 다른 동작을 실행하는 함수 반환하는 고차 함수.

예제가 어려운데, 고차 함수가 할당되어 실행되는 부분부터 맥락을 살펴보면 이해가 쉽다. 새로운 변수에 이 고차 함수를 할당하면 자유 변수를 가진, 특정 동작(인수로 보낸 보조함수)을 수행하는 클로저가 생성된다.

[그림 24-9] 이 고차 함수를 실행할 때마다 새로운 실행 컨텍스트 안에 새로운 렉시컬 환경이 형성되기 때문에 그 결과 생성된 클로저 함수는 독립적인 렉시컬 환경을 갖는다. [그림 24-10] 따라서 여러 클로저 함수를 생성해도 서로 자유 변수(상태)를 공유하지 않는다.

[예제 24-15] 즉시 실행 함수 안에 자유 변수를 만들어놓고 클로저가 될 함수를 반환하면, 이 고차 함수에 매번 다른 보조함수를 넣어 서로 다른 동작을 수행하도록 하여 자유 변수를 공유할 수 있다. 앞의 예제와 차이점은, 고차 함수 하나에 서로 다른 보조 함수를 인수로 전달하여 새로운 클로저 함수를 여러 개 생성하는 게 아니라, 고차 함수인 하나의 클로저 함수를 생성하여 그때그때 보조 함수를 다르게 전달하여 다른 동작을 수행한다는 점이다. 

 

 

24.5 캡슐화와 정보 은닉

캡슐화 : 객체의 프로퍼티와 메서드를 하나로 묶는 것.

정보 은닉 : 캡슐화를 특정 프로퍼티나 메서드를 감출 목적으로 사용하는 것.

  • 장점
    1. 외부의 적절치 못한 접근으로부터 상태 변경 방지.
    2. 객체 간의 상호 의존성(결합도) 낮춤.
  • 방법
    • 다른 객체지향 프로그래밍 언어들의 클래스에서는 프로퍼티와 메서드에 대해 접근 제한자(public, private, protected)를 선언하여 공개 범위 한정.
    • 자바스크립트에는 접근 제한자가 없음.
      • [예제 24-18] 앞에서의 방식으로 생성자 함수를 즉시 실행 함수에서 반환하여 클로저를 만듦.
      • → 프로토타입 메서드의 경우 즉시 실행 함수가 호출될 때 단 한 번 생성(생성될 때 렉시컬 환경 형성)되는 클로저이므로 모든 인스턴스가 공유하여 자유 변수의 상태가 유지되지 않음.
      • 즉, 완전한 정보 은닉을 지원하지 않음.
    • 새로운 표준 사양에서 클래스에 private 필드 정의 가능해짐.

 

 

24.6 자주 발생하는 실수

[예제 24-20] for 문의 i가 공유되는 문제

for 문의 변수 선언문에 var 키워드로 선언한 i 변수는 전역 변수이다. 따라서 function () { return i; } 함수를 배열의 요소로 세 번 추가하면 각각 함수 객체 생성 당시의 i 변수 값을 기억해서 반환하는 것이 아니라, 최종적으로 호출될 시점의 i 변수 값을 반환한다. 결과적으로 세 개의 함수 객체는 반환값이 동일하다.

[예제 24-21] 클로저를 사용하여 해결

배열의 요소로 추가될 함수를 즉시 실행 함수로 감싸고, for 문의 해당 루프 시점의 i 변수 값을 인수로 전달하여 중첩함수가 이 매개변수를 참조할 수 있게 한다. 이로써 매개변수가 자유 변수, 중첩 함수가 클로저가 되어 i 변수의 변화에 관계 없이 고정된 반환값을 갖는다.

[예제 24-22] let 키워드를 사용하여 해결

for 문의 변수 선언문에 let 키워드로 i 변수를 선언하면 블록 레벨 스코프이므로 for 문의 매 루프에 독립적인 렉시컬 환경을 생성하여 i 변수가 공유되지 않는다. i가 0일 때와 i가 1일 때 코드 블록이 다르기 때문에 거기서 각각 정의한 (i를 반환하는) 함수는 서로 다른 상위 스코프를 참조한다.

let이나 const 키워드를 사용하는 반복문은 블록 레벨 스코프이므로, 코드 블록을 반복 실행할 때마다 새로운 렉시컬 환경을 생성하여 그 당시의 상태를 사진 찍듯 저장한다. 물론 코드 블록 내부에서 함수를 정의하여 자유 변수를 참조할 때에 한해서다.