JavaScript 클로저

Taemin Jang·2024년 6월 6일
0

Javascript

목록 보기
8/14

클로저는 자바스크립트 고유의 개념이 아니며, 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.

클로저를 이해하기 위해서는 핵심 내용인렉시컬 환경에 대해 알고 넘어가야 한다.

렉시컬 스코프

자바스크립트 엔진은 함수를 어디서 호출했는지(동적 스코프)가 아닌, 함수를 어디에 정의했는지에 따라 상위 스코프를 결정하는데 이를 렉시컬 스코프(정적 스코프)라 한다.

const x = 1;

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

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

foo(); // ?
bar(); // ?

여기서 함수 foo와 bar는 전역에서 정의되었기 때문에, 정의되는 시점에서 함수의 상위 스코프는 전역으로 결정되고 변하지 않는다.

따라서 함수 bar를 호출하면 변수 x를 bar 스코프 내에서 찾고 없으면 스코프 체인을 통해 상위 스코프인 전역 변수 x의 값을 참조해서 1이 찍힌다.

함수 foo 또한 bar를 호출하고 있는데 호출에 의해 상위 스코프 참조는 변경되지 않기 때문에 동일한 1을 출력된다.

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

함수는 정의된 환경과 호출되는 환경이 다를 수 있다.

호출과는 상관 없이 상위 스코프를 기억해야 하므로 상위 스코프의 참조를 함수의 내부 슬롯[[Environment]]에 저장하게 된다.

예를 들어, 전역에서 정의된 함수 선언문은 전역 코드가 평가되는 시점에 평가되어 함수 객체를 생성한다.

생성된 함수 객체 내부 슬롯 [[Environment]]에는 실행 컨텍스트의 렉시컬 환경인 전역 렉시컬 환경의 참조가 저장된다.

만약 함수 내부에서 정의된 함수 표현식은 외부 함수가 실행되는 시점에 평가되어 함수 객체를 생성한다.

외부 함수 렉시컬 환경의 참조가 생성된 함수 객체 내부 슬롯에 저장된다.

따라서 위 예제를 보면 foo와 bar 함수는 전역에서 함수 선언문으로 정의되었기 때문에 내부 슬롯 [[Environment]]에는 전역 렉시컬 환경의 참조가 저장된다.

클로저와 렉시컬 환경

위에서 설명한 렉시컬 스코프와 함수 객체의 내부 슬롯에 대한 개념을 이해했다면 클로저를 이해할 수 있다.

const x = 1;

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

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

outer 함수를 호출하면 중첩 함수인 inner를 반환하고 생명 주기를 마감한다.

이때 outer 함수의 지역 변수 x도 같이 생명 주기를 마감하고, 더이상 outer 함수의 지역 변수 x에는 접근할 수 없다.

하지만 innerFunc 함수를 실행하면 outer 함수의 지역 변수 x 값을 출력하게 된다.

이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.

이러한 중첩 함수를 클로저라고 부른다.

outer 함수의 생명 주기가 종료되면 해당 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거된다.

outer 함수의 실행 컨텍스트에서 제거는 되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.

outer 함수의 렉시컬 환경은 inner 함수의 [[Environment]] 내부 슬롯에 참조되고 있고 inner 함수도 전역 변수 innerFunc에 참조되고 있기 때문에 가비지 컬렉션의 대상이 되지 않아 메모리 공간에서 제거되지 않는다.

클로저의 조건은 다음과 같다.
1. 중첩 함수로 선언되었는가?
2. 상위 스코프의 식별자를 참조하고 있는가?
3. 중첩 함수가 외부 함수보다 더 오래 유지되는가? (오래 유지하려면 중첩 함수가 외부 함수의 반환값이어야 한다.)

위 3가지 조건에 하나라도 부합되지 않으면 클로저가 아니다.

또한 클로저는 상위 스코프 식별자가 x, y, z가 있고 그 중 x만 참조하고 있다면 x만 기억하고 그 외 식별자는 기억하지 못한다.

이렇게 클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라고 부른다.

클로저는 상위 스코프를 기억해야 하므로 불필요한 메모리 낭비라고 생각할 수도 있지만, 클로저가 참조하고 있지 않은 식별자는 기억하지 않으므로 불필요한 메모리 낭비가 아니다.

따라서 클로저는 필요하다면 적극적으로 활용하는 것이 좋다.

클로저 활용하기

주로 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.

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

const increase = (function () {
  let num = 0;
  
  // 클로저
  return function () {
    return ++num;
  };
}());

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

즉시 실행 함수가 실행되고 반환한 함수가 increase 변수에 할당된다.

increase 변수에 할당된 함수는 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저가 된다.

num 변수는 외부에서 직접 접근할 수 없는 private 변수이므로 안전하게 상태를 변경할 수 있다.

const counter = (function () {
  let num = 0;
  
  return {
    // num : 0 프로퍼티는 public 하므로 은닉되지 않는다.
    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

즉시 실행 함수가 반환하는 객체 리터럴은 코드 블록이 아니므로 별도의 스코프를 생성하지 않는다.

다음은 함수형 프로그래밍에서 클로저를 활용하는 예제다.

// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
function makeCounter(predicate) {
  let counter = 0;
  
  return function () {
    counter = predicate(counter);
    return counter;
  };
}

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

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

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

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동되지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

makeCounter 함수는 인자로 전달받은 함수를 합성하여 자신이 반환하는 값으로 함수의 동작을 변경할 수 있다.

이 때 주의할 점은 makeCounter 함수를 호출해 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖는다.

increaser와 decreaser 함수는 makeCounter 함수를 호출 할 때마다 함수 객체를 생성하고 반환한 후 소멸된다. 따라서 각각 생성된 함수 객체는 [[Environment]] 내부 슬롯에 makeCounter 함수의 렉시컬 환경을 기억하고 있기 때문에 counter 변수는 공유 되지 않는다.

만약 독립된 counter가 아닌 연동하여 증감이 가능한 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들어야 한다.

function counter = (function () {
  let count = 0;
  
  return function (predicate) {
    count = predicate(count);
    return count;
  };
}());

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

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

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

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

캡슐화와 정보 은닉

캡슐화는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다.

캡슐화는 주로 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉이라고 한다.

정보 은닉은 외부에 공개하지 않고 감추어 적절치 못한 접근으로 부터 객체의 상태가 변경되는 것을 보호하고, 객체 간의 결합도를 낮추는 효과가 있다.

profile
하루하루 공부한 내용 기록하기

0개의 댓글

관련 채용 정보