[Javascript Deepdive] 클로저(Closure)

Gyuhan Park·2023년 6월 29일
0

javascript deepdive

목록 보기
4/11

클로저란 상위 스코프의 식별자를 참조 하며, 외부 함수보다 더 오래 유지 되는 중첩 함수 를 말한다. 자바스크립트는 렉시컬 스코프를 따르기 때문에 함수가 어디서 정의됐는지에 따라 상위 스코프를 결정한다. 함수 정의가 평가되어 함수 객체를 생성하는 시점인 실행 중인 실행 컨텍스트의 렉시컬 환경 을 상위 스코프로 참조한다. 이에 함수는 함수 객체의 내부 슬롯인 [[Environment]] 에 상위 스코프 참조를 저장한다. 따라서 함수는 언제나 상위 스코프의 식별자를 참조 할 수 있으며, 식별자에 바인딩된 값을 변경 할 수 있다.

클로저를 반환하는 외부 함수는 콜스택에서 제거되지만 클로저의 [[Environment]] 에서 참조하고 있기 때문에 렉시컬 환경은 유지된다.
또한 클로저를 활용하면 캡슐화와 정보은닉 효과를 얻을 수 있다.

📘 함수 렉시컬 환경

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

중첩함수 innerFunc의 상위 스코프 : outerFunc
innerFunc에서 outerFunc의 변수 x에 접근 가능 → 따라서 x값은 10

const x = 1;

function outerFunc() {
  const x = 10;
  function innerFunc() {
    console.log(x); // 10
  }
  
  innerFunc();
}

innerFunc() 이 outerFunc() 내부에서 정의되지 않는다면 innerFunc() 은 outerFunc() 에 정의된 x 변수에 접근할 수 없다.

  • 어디서 정의됐는지에 따라 스코프가 결정되는 렉시컬 스코프를 따르기 때문
    • 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 위치(환경)에 의해 결정
const x = 1;

function outerFunc() {
  const x = 10;
  innerFunc();
}

function innerFunc() {
  consol.log(x); // 1
}

📘 [[Environment]]

렉시컬 스코프를 따르기 위해선 자신이 정의된 환경인 상위 스코프를 기억 해야함
함수 정의가 평가되어 함수 객체를 생성할 때, 함수 객체의 내부 슬롯인 [[Environment]]상위 스코프의 참조 저장
함수 정의가 평가되어 함수 객체를 생성하는 시점인 실행 중인 실행 컨텍스트의 렉시컬 환경 참조

✅ 예시

foo 함수와 bar 함수가 전역에서 함수 선언문으로 정의
전역 코드가 평가되는 시점에 함수 객체를 생성하고 window의 프로퍼티로 생성
함수 정의가 평가되는 시점인 전역 코드를 평가할 때, [[Environment]] 슬롯에 전역 렉시컬 환경 참조값 저장
foo 함수가 호출될 때 함수 코드 평가 → 외부 렉시컬 환경에 대한 참조값을 [[Environment]] 에서 가져옴

const x = 1;

function foo() {
  const x = 10;
  bar();
}

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

foo();
bar();

📘 클로저(Closure)

💡 중첩 함수가 상위 스코프의 식별자를 참조 하며, 중첩 함수가 외부 함수보다 더 오래 유지 되는 중첩 함수

상위 스코프의 식별자 중 클로저가 참조하고 있는 식별자 → 자유변수
클로저는 자유 변수만 기억
클로저(closure)란 함수가 “자유 변수에 대해 닫혀있음” 또는 “자유 변수에 묶여 있는 함수”
함수는 언제나 자신이 기억하는 상위 스코프의 식별자를 참조 할 수 있으며, 식별자에 바인딩된 값을 변경 가능

✅ 예시

outer 함수를 호출하면 중첩함수 inner를 반환하고 생명주기 마감
inner 함수는 자신이 평가될 때 결정된 상위 스코프를 [[Environment]] 내부 슬롯에 저장
저장된 상위 스코프는 inner함수가 존재하는 한 유지

const x = 1;

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

const innerFunc = outer();
innerFunc(); // 10

outer 함수는 inner 함수를 반환하면서 생명주기 종료 → outer 함수의 실행 컨텍스트 제거
outer 함수의 실행 컨텍스트는 콜스택에서 제거되지만 렉시컬 환경은 유지
outer 함수의 렉시컬 환경 → inner 함수의 [[Environment]] 내부 슬롯에 의해 참조
inner 함수의 렉시컬 환경 → innerFunc에 의해 참조

🚨 클로저지만 클로저가 아닌 것

  • 중첩 함수가 외부 함수보다 더 오래 유지되지만 상위 스코프의 어떤 식별자도 참조하지 않는 경우
    • 이때, 대부분의 모던 브라우저는 최적화를 통해 상위 스코프 기억 ❌
  • 중첩 함수가 상위 스코프의 식별자를 참조하지만 중첩 함수를 반환하지 않는 경우
    • 외부 함수보다 중첩 함수의 생명주기가 짧음

📘 클로저의 활용

  1. 상태를 안전하게 변경하고 유지하기 위해 사용
  2. 특정 함수에게만 상태 변경을 허용하기 위해 사용

✅ 문제 코드

올바르게 동작하려면 다음 두 조건을 만족해야 함
1. num은 increase 함수가 호출되기 전까지 변경되지 않고 유지되어야 함
2. increase 함수만이 num을 변경할 수 있어야 함

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

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

✅ 클로저 활용

이전 상태를 유지하며 increase 함수만이 num을 변경할 수 있도록 하기
즉시 실행함수 를 이용하여 num을 직접 참조할 수 없고, 함수는 호출된 이후 바로 소멸시킴
클로저인 increase만 num의 상태를 변경 가능

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

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

✅ 클로저 활용(2)

const counter = (function() {
	let num = 0;
	
	return {
		increase() {
			return ++num;
		},
		decrease() {
			return num > 0 ? --num : 0;
		}
	};
}());

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

console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0

✅ 클로저 활용(3) - 함수형 프로그래밍

인수로 전달되는 보조 함수(aux)에 상태 변경 위임
makeCounter가 반환하는 함수는 makeCounter 함수를 상위 스코프로 가지는 클로저
makeCounter 함수를 호출해 반환되는 함수는 자신만의 독립된 렉시컬 환경을 가짐
→ 호출할 때마다 새로운 makeCounter 함수 실행 컨텍스트 생성

function makeCounter(aux) {
  let counter = 0;
  
  return function() {
    counter = aux(counter);
    return counter;
  };
}

function increase(n) {
  return ++n;
}

function decrease(n) {
  return --n;
}

const increaser = makeCounter(increase);
console.log(increaser()); // 1

const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1

counter를 공유하려면 렉시컬 환경을 공유하는 클로저를 만들어야 함
makeCounter를 여러번 호출하면 안됨

const counter = (function(){
	let counter = 0;
	return function (aux) {
		counter = aux(counter);
		return counter;
	};
}());
...

console.log(counter(increase)); // 1
console.log(counter(decrease)); // 0

📘 캡슐화와 정보은닉

자바스크립트는 private, public 등 접근 제한자가 존재하지 않는다.(_ : protected, # : private 으로 최근에 생기긴 함)
객체의 모든 프로퍼티와 메서드는 기본적으로 public이다.

따라서 아래 코드처럼 클로저를 통해 정보를 private하게 만들 수 있다

const Person = (function() {
  let _age = 0;
  
  function Person(name, age) {
    this.name = name;
    _age = age;
  }
  
  Person.prototype.sayHi = function() {
    console.log(`${this.name}, I'm ${_age}`);
  };
  
  return Person;
}());

const me = new Person('Lee', 20);
me.sayHi(); // Lee, I'm 20
console.log(me.name); // Lee
console.log(me._age); // undefined

생성자 함수 Person 을 반환하면서 _age는 접근할 수 없다.

하지만 인스턴스를 여러 개 생성하는 경우 프로토타입 메서드의 상위 스코프는 즉시실행함수 를 가리키므로 같은 _age 변수를 가리킨다. 따라서 인스턴스를 생성할 때 동일한 _age의 값이 변한다.

const me = new Person('Lee', 20);
const you = new Person('Kim', 30);
me.sayHi(); // Lee, I'm 30

이와 같이 자바스크립트는 정보 은닉을 완전하게 지원하지 않는다.

모던자바스크립트 deepdive

profile
단단한 프론트엔드 개발자가 되고 싶은

0개의 댓글