클로저(closure)

Moen·2022년 9월 30일
0
post-thumbnail
post-custom-banner

mdn에서 정의하는 클로저는 함수와 그 함수의 렉시컬 환경의 조합이라고 정의하고 또는 외부 변수를 기억하고 이 외부함수에 접근할 수 있는 함수입니다.

여기서 렉시컬 환경와 정적 스코프(렉시컬 스코프)의 개념을 알고 있어야 클로저를 이해할 수 있습니다.

렉시컬 스코프(정적 스코프)


프로그래밍 언어에서는 함수 정의 위치, 함수 호출 위치에 따라서 상위 스코프를 결정하는 정적 스코프, 동적 스코프로 정의합니다.

  • 정적 스코프: 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정합니다.
  • 동적 스코프: 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정합니다.
const exVar = 1;

function foo() {
  const exVar = 100;

  bar();
}

function bar() {
  console.log(exVar);
}

foo(); // 정적 스코프: 1, 동적 스코프: 100

// result: 1

위 예제를 통해서 JavaScript는 정적 스코프로 프로그램이 정의 되어있으며 호출 위치는 상위스코프의 결정에 어떠한 영향도 주지 않으며 함수를 정의한 위치를 통해서만 스코를 결정합니다. 즉 한번 결정된 스코프를 동적으로 변화하지 않고 정의한 위치만을 상위 스코프로 인지합니다.

내부 슬로 [[ Environment ]]


내부 슬롯 [[ Environment ]]는 자바스크립트 엔진 구현 알고리즘을 설명하기 ECMAScript 사양에서 사용하는 의사 프로퍼티입니다. 내부슬롯 [[ Environment ]]를 알기 위해서는 실행 컨텍스트의 개념인 외부 렉시컬 환경의 참조, 렉시컬 환경의 참조에 대해서 알고 있어야 합니다.

  • 렉시컬 환경: 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프의 참조값을 기록하는 자료구조입니다. 즉, 렉시컬 환경은 스코프의 실체입니다. (식별자와 식별자에 바인딩된 값은 객체의 프로퍼티처럼 자료구조에 저장됩니다.)

  • 외부 렉시컬 환경의 참조: 현재 평가중인 소스코드를 포함하는 외부 소스코드의 렉시컬 환경(상위 스코프)의 참조값입니다. 외부 렉시컬 환경에 대한 참조를 통해서 스코프 체인이 구현됩니다.

함수가 평가되어 함수 객체가 되는 시점에 내부슬롯[[ Environment ]]도 함수를 정의한 상위 스코프를 기억합니다. 함수가 평가되어 함수 객체가 만들어지는 시점은 함수를 정의한 상위 스코프가 평가 또는 실행되는 시점입니다. 편의상 함수 호이스팅이 발생하지 않는 함수 표현식을 통해서 설명하겠습니다.

var x = 1;

const foo = function () {
	console.log(x); // 1
};

foo() // 실행 컨텍스트가 생성되는 시점은 함수가 호출되는 시점가 동일입니다.

간단한 예제를 통해서 외부 렉시컬 환경에 대한 참조(상위 스코프)와 내부 슬롯[[Environment]]의 관계에 대해서 설명하겠습니다.

  1. foo함수는 전역 객체가 실행되는 시점에 함수를 평가하여 함수 객체가 만들어집니다.
  2. 함수 객체가 만들어지면서 내부 슬롯[[Environment]]도 함께 만들어집니다.
    • 내부 슬롯 [[Environment]]에는 함수가 평가되는 상위 스코프의 참조값이 저장됩니다.
  3. foo함수가 호출되면 foo함수의 실행 컨텍스트가 생성되고 렉시컬 환경이 생성됩니다.
    • 렉시컬 환경이 생성되면서 식별자와 식별자에 바인딩된 값을 저장합니다.
  4. 외부 렉시컬 환경에 대한 참조의 참조값은 내부슬롯[[Environment]]에 저장된 상위 스코프의 참조값이 외부 렉시컬 환경에 대한 참조에 저장됩니다.
  5. foo함수가 호출되고 console.log(x)함수가 실행 됩니다.(console은 전역 객체의 프로퍼티로 실행 컨텍스를 만들지만 생략하겠습니다.)
  6. Console.log의 인수 x는 외부 렉시컬 환경에 대한 참조로 만들어진 스코프 체인을 통해서 상위 스코프에서 식별자를 검색하고 먄약 상위 스코프에 식별자가 없다면 그 위의 상위 스코프에서 식별자를 검색하여 결과를 반환합니다.(전체 스코프에서 식별자가 전재하지 않으면 undefined를 반환합니다.)
    • console 스코프 => foo 함수 스코프 => 전역 스코프로 단반향 스코프 체인이 형성됩니다.

함수가 호출되어 실행 컨텍스트를 생성하면 외부 렉시컬 환경에 대한 참조는 내부슬롯[[Environment]]에 저장된 상위 스코프를 통해서 상위 스코프를 결정하게 됩니다.

다시 mdn에서 정의한 클로저를 보면 함수와 그 함수의 렉시컬 환경의 조합입니다. mdn의 정의에서는 자바스크립트의 모든 함수는 내부슬롯 [[Environment]]가 존재하기 때문에 모든 함수는 클로저입니다.

브라우저 최적화


위의 설명처럼 자바스크립트의 모든 함수는 클로저입니다. 하지만 사용하지 않는 식별자를 메모리에 저장하고 있다면 최적화 부분에서 매우 좋지 않는 포퍼먼스를 보여줍니다. 이런 부분을 해결하기 위해서는 모든 브라우저 자체에서 최적화를 진행합니다.

브라우저 성능 관련 고려 사항

  • 상위 스코프를의 식별자를 식별하지 않는 경우에는 브라우저가 최적화를 통해서 상위 스코프의 식별자를 메모리에서 제거합니다.
const x = 1;

function outer() {
	const y = 2;
	
	function inner() {
		const z = 3;	
		console.log(z); // 외부 스코프를 참조하지 않고 내부 함수에서만 변수를 식별합니다. 
	}
	return inner;
}

const result = outer();

result();
  • 상위 스코프의 식별자를 식별하지만 상위 스코프보다 생명 주기가 짧을 경우 브라우저는 내부 함수를 클로저라고 생각하지 않는다.
const x = 1;

function outer() {
	const y = 2;
	
	function inner() {
		const z = 3;	
		console.log(x, y, z); // 외부 식별자를 참조하지만 외부 함수보다 내부 함수의 생명주기가 짧습니다. 
	}
	inner();
}

outer();
  • 상위 스코프의 식별자를 식별하면서 외부 함수보다 생명 주기가 긴 내부 함수는 브라우저에서 클로저라고 생각합니다.

    (만약 상위 스코프에 여러개의 식별자가 있지만 내부 함수에서 하나의 식별자만을 참조한다면 내부 함수에서 식별하는 식별자를 제외하고 다른 식별자는 브라우저 최저고하를 통해서 메모리에 저장하지 않습니다.)

const x = 1;

function outer() {
	const y = 2;
	
	function inner() {
		const z = 3;	
		console.log(x, y, z); // 외부 식별자를 참조하며 내부 함수가 외부 함수보다 생명주기가 더 깁니다. 
	}
	return inner;
}

const result = outer();

result();
  • 클로저는 상위 스코프의 식별자를 참조하면서 외부 함수보다 내부 함수의 생명 주기가 더 오래 유지되는 경우를 클로저라고 지칭합니다.

  • 클로저를 통해서 은닉화 한 변수를 자유 변수라고 지칭합니다.

캡슐화


클로저의 활용

자바스크립트는 객체 지향 언어이지만 다른 객체 지향 언어와 다르게 public, private, protected를 지원하지 않고 public만을 지원한다. 그말은 자바스크립트의 모든 객체는 어디서나 접근하여 객체의 프로퍼티에 접근할 수 있다.

pubilc만을 지원하는 자바스크립트는 어디서나 객체의 프로퍼티에 접근할 수 있어서 동적으로 프로퍼티를 변경할 수 있다

  • 객체에 접근하여 동적으로 프로퍼티 변경을 할 수 있고 어디서나 접근이 가능해서 보완성 및 신뢰성이 떨어진다.
  • 문제점을 해결하기 위해 캡슐화를 통한 객체의 특정 프로퍼티나 메서드를 감출 목적으로 정보 은닉을 할 수 있다.

정보 은닉을 위해서는 클로저를 사용할 수 있습니다.

function Company(name, personnel) {
  this.name = name;
  let _personnel = personnel;

	this.getPersonnel = function () {
		return _personnel
	};

  this.getInfo = function () {
    return `회사명 : ${this.name}, 인원 수 : ${_personnel} 입니다.`;
  };
}

const company = new Company('코카콜라', 300);

console.log(company.name); // 코카콜라
console.log(company._personnel); // undefined
console.log(company.getPersonnel()); // 300
console.log(company.getInfo()); // 회사명 : 코카콜라, 인원 수 : 300 입니다.

생성자 함수도 함수 평가 후 함수 객체를 반환하기 때문에 자신의 프로퍼티를 가질 수 있습니다. 이러한 성질을 이용하여 private 변수를 만들 수 있으며 인스턴스에서는 _personnel 변수에 접근 할 수 없으며 getPersonnel 인스턴스 메서드를 통해서만 접근 할 수 있는 private 변수입니다. 그렇지만 메서드들은 인스턴스의 메서드이기 때문에 new 연산자와 함께 인스턴스를 생성하는 경우에는 중복된 메서드를 가지고 있는 문제점이 있습니다.

function Company(name, personnel) {
  this.name = name;
  let _personnel = personnel;
}

Company.prototype.getPersonnel = function () {
  return _personnel;
};

Company.prototype.getInfo = function () {
  return `회사명 : ${this.name}, 인원 수 : ${_personnel} 입니다.`;
  // _personnel을 참조할 수 없습니다.
};

const company = new Company('코카콜라', 300);

console.log(company.getPersonnel()); // ReferenceError: _personnel is not defined 
console.log(company.getInfo()); // ReferenceError: _personnel is not defined

위의 예시에서 Company.prototype가 정의되는 스코프인 전역 스코프가 상위 스코프로 내부 슬롯[[ Environment ]]에는 전역 스코프가 저장 되기 때문에 _personnel를 참조 할 수 없어서 ReferenceError가 발생합니다.

const Company = (function Company() {
  let _personnel = 0;

  function Company(name, personnel) {
    this.name = name;
    _personnel = personnel;
  }

  Company.prototype.getPersonnel = function () {
    return _personnel;
  };

  Company.prototype.getInfo = function () {
    return `회사명 : ${this.name}, 인원 수 : ${_personnel} 입니다.`;
  };

  return Company;
})();

const company1 = new Company('코카콜라', 300);

console.log(company1); // Company { name: '코카콜라' }
console.log(company1.getInfo()); // 회사명 : 코카콜라, 인원 수 : 300 입니다.

const company2 = new Company('팹시', 250);

console.log(company2); // Company { name: '팹시' }
console.log(company2.getInfo()); // 회사명 : 팹시, 인원 수 : 250 입니다.

console.log(company1.getInfo()); // 회사명 : 코카콜라, 인원 수 : 250 입니다.    

즉시 실행 함수를 통해서 변수 _personnel를 private 변수로 만들고 Company.prototype의 메서드로 만들어서 동일한 메서드는 중복 생성 되지 않게 만들었습니다. 하지만 예시의 마지막 줄에서 company1.getInfo( )의 반환값이 재대로 적용되지 않은 모습입니다.

Company.prototype.getInfo 메서드는 단 한번만 실행되는 클로저이기 때문에 발생하는 현상입니다. 이 처럼 자바스크립트는 캡슐화를 통한 정보 은닉이 완전하게 지원하지 않는 상태입니다.

참고 문서


클로저 - MDN

클로저 - poiemaweb

변수의 유효범위와 클로저 - 모던 JavaScript 튜토리얼

profile
게시글에 잘못된 부분이 있으면 댓글로 알려주시면 빠르게 수정 및 수용도 하겠습니다. 🥲
post-custom-banner

0개의 댓글