[JS] 클로저란?

chaevivi·2023년 7월 13일
0
post-thumbnail

클로저란?

클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다. (MDN 정의)

클로저는 자바스크립트의 고유 개념이 아니라 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 특성이다. 위의 정의에서 "함수가 선언된 렉시컬 환경"은 어떤 것을 의미할까?



1. 클로저와 렉시컬 환경

  • 렉시컬 스코프(정적 스코프) : 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정

const x = 1;

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

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

foo();    // 1
bar();    // 1

위의 코드를 살펴보면,

  • foo 함수와 bar 함수는 모두 전역에서 정의된 전역 함수
    - 함수의 상위 스코프는 함수를 어디서 정의했느냐에 따라 결정되므로 두 함수의 상위 스코프는 전역
  • 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정 -> 렉시컬 스코프

const x = 1;

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

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

위의 코드를 살펴보면,

  • outer 함수를 호출(③)하면 outer 함수는 중첩 함수 inner를 반환하고 생명 주기 마감. 즉, outer 함수 실행이 종료되면 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거
  • 이때 outer 함수의 지역 변수 x와 변수 값 10을 저장하고 있던 outer 실행 컨텍스트가 제거되었으므로 x 또한 생명 주기 마감 -> 지역 변수 x는 더이상 유효하지 않음
  • 하지만 실행 결과는 outer 함수의 지역 변수 x의 값인 10
  • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수 참조 가능. 이러한 중첩 함수가 클로저
    • outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니기 때문에 변수 참조가 가능
  • 자유 변수 : outer 함수의 지역 변수 x처럼 클로저에 의해 참조되는 상위 스코프의 변수

MDN에서 정의한 클로저 개념의 "함수가 선언된 렉시컬 환경"이란 함수가 정의된 위치의 스코프, 즉 상위 스코프를 의미하는 실행 컨텍스트의 렉시컬 환경을 말한다.



2. 클로저의 활용

  • 클로저는 상태(state)를 안전하게 변경하고 유지하기 위해 사용
  • 다시 말해, 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용

const counter = (function () {
  // 카운트 상태 변수
  let num = 0;
  
  return {
    // num: 0,    // 프로퍼티는 pulbic하므로 은닉 불가
    increase() {
      return ++num;
    },
    decrease() {
      return --num;
    }
  };
}());

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

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

위의 코드를 살펴보면,

  • 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터 코드
  • counter 변수는 클로저인 메서드를 갖는 객체를 반환
    • 객체 리터럴은 스코프를 만들지 않기 때문에 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경
  • 즉시 실행 함수는 한 번만 실행되므로 num 변수가 재차 초기화되지 않아 카운터 제대로 구현 가능
  • num 변수는 함수 안에 선언된 지역 변수이기 때문에 외부에서 직접 접근할 수 없는 private 변수

// 함수를 반환하는 고차 함수
const counter = (function () {
  let counter = 0;
  
  // 클로저 반환
  return function(aux) {
    // 인수로 전달받은 보조 함수에 상태 변경 위임
    counter = aux(counter);
    return counter;
  };
}());

// 보조 함수
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

위의 코드를 살펴보면,

  • 앞선 코드는 메서드를 반환했지만 위의 코드는 함수를 인수로 전달받고 함수를 반환(고차 함수)
  • 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저 반환


3. 캡슐화와 정보 은닉

  • 캡슐화(encapsulation) : 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것
  • 정보 은닉 : 외부에 공개할 필요가 없는 구현의 일부를 외부에 공개되지 않도록 감추어 적절치 못한 접근으로부터 객체의 상태가 변경되는 것을 방지해 정보 보호, 객체 간의 의존성, 즉 결합도를 낮춤
  • 자바스크립트는 public, private, protected 같은 접근 제한자를 제공하지 않지만 비슷하게 구현 가능 -> 하지만 정보 은닉을 완전하게 지원 X

const Person = (function () {
  let _age = 0;    // private
  
  // 생성자 함수
  function Person(name, age) {
    this.name = name;    // public
    _age = age;
  }
  
  // 프로토타입 메서드
  Person.prototype.sayHi = function () {
    console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
  };
  
  // 생성자 함수 반환
  return Person;
}());

const me = new Person('Lee', 20);
me.sayHi();    // Hi! My name is Lee. I am 20.
console.log(me.name);    // Lee
console.log(me._age);    // undefined

const you = new Person('Kim', 30);
you.sayHi();    // Hi! My name is Kim. I am 30.

me.sayHi();    // Hi! My name is Lee. I am 30.

위의 코드를 살펴보면,

  • 즉시 실행 함수가 반환하는 Person 생성자 함수와 생성자 함수의 인스턴스가 상속받아 호출할 Person.prototype.sayHi 메서드는 즉시 실행 함수가 종료된 이후 호출
  • Person 생성자 함수와 sayHi 메서드는 이미 종료되어 소멸한 즉시 실행 함수의 지역 변수 _age를 참조할 수 있는 클로저
  • 이러한 패턴을 사용해 정보 은닉 구현 가능
  • 하지만 Person 생성자 함수가 여러 개의 인스턴스를 생성할 경우 _age 변수의 상태가 유지되지 않음
    • 이유는 Person.prototype.sayHi 메서드가 단 한 번 생성되는 클로저이기 때문
    • Person.prototype.sayHi 메서드의 상위 스코프는 어떤 인스턴스로 호출하더라도 하나의 상위 스코프를 사용하기 때문



출처 : 모던 자바스크립트 Deep Dive

profile
직접 만드는 게 좋은 프론트엔드 개발자

0개의 댓글