오늘은 클로져에 대해 살펴보려고 한다.
우리가 태어난 고향을 잊지 못하는 것처럼, 클로져도 태어난 환경의 상위 스코프를 기억하는 특성을 이용한 함수이다.
바로 클로져의 본격적인 개념을 살펴보기 앞서, 이전 글에서도 설명했던 실행 컨텍스트 글을 떠올려보자.
지난 글에서 자바스크립트의 동작원리를 이해하기 위한 실행 컨텍스트에 대해 알아보았다. 실행 컨텍스트가 생겨나면서 렉시컬 환경이 외부 환경에 대한 참조 값을 가진다는 것을 배웠다.
이 외부 환경에 대한 참조 값으로, 함수의 경우에는 함수가 선언된 장소의 상위 스코프가 그 참조값이 된다. 이러한 특성을 '렉시컬 스코프'라고 하며, 클로져는 이 렉시컬 스코프를 이용한 스킬이라고 볼 수 있다.
우리가 유의해야할 점은 함수가 호출되는 시점이 아니라 선언되는 시점의 외부환경을 참조한다는 것이다.
클로져는 자신을 포함하고 있는 외부 함수보다 내부 함수가 더 오래 유지되는 경우,
외부 함수 밖에서 내부 함수가 호출되더라도 자신이 생성된 환경을 기억하는 습성을 활용하여 외부 함수의 지역 변수에 접근할 수 있는 함수를 말한다.
(클로져를 넓게 보면 내부 함수가 외부 환경을 기억하여 외부 함수에 접근하는 현상 자체를 말하기도 하지만, 이 글에서는 실무적으로 그러한 현상을 활용하여 사용되는 함수들을 일컫는다.)
function outer() {
var name = `kai`;
function inner() {
console.log(name);
}
inner();
}
outer();
// console> kai
위 코드를 보면 함수 inner는 name이라는 변수가 할당되지 않았다. 하지만 선언 당시의 렉시컬 환경이 함수 outer를 외부 참조로 가리켜, name 변수에 할당된 값을 가져와 console.log를 성공적으로 실행한 것을 볼 수 있다.
이때 외부함수의 변수인 name은 '자유 변수(free variables)'라고 불린다.
본격적으로 클로져의 활용 유형들을 예시를 통해 살펴보자
function CreateCounter() {
var counter = 0;
this.increase = function () {
return ++counter;
};
this.decrease = function () {
return --counter;
};
}
const countNum = new CreateCounter();
console.log(countNum.increase()); // 1
console.log(countNum.decrease()); // 0
변수 counter의 상태를 기억하여, increase 이후에 decrease를 실행하면 증가된 1에서 1만큼 감소된 0이 나오는 것을 확인할 수 있다. increase()와 decrease()가 하나의 상태(counter)를 공유하고 있음을 알 수 있다.
문득 리액트의 useState가 떠올랐는가?
그렇다면 제대로 떠올린게 맞다.
useState Hook API는 내부적으로 클로져를 활용하여 구현되었다.
(추후 게시물을 통해 클로져를 활용한 useState 직접 구현을 시도하는 과정을 소개하도록 하겠다. )
자바스크립트는 프로토타입 기반의 객체 지향 언어라 클래스 기반 객체지향 언어와 다르게 public, private, protected 과 같은 정보 은닉과 관련된 프로퍼티를 사용할 수 없다.
하지만 클로져를 통해 상태를 은닉함으로써, 간접적으로 private 프로퍼티를 사용한 것과 같이 자유변수 counter
를 외부로부터 보호한다.
만일 변수 counter
가 this 바인딩이 되어 this.counter
와 같은 형태로 선언이 되었다면 public 프로퍼티를 사용한 것과 같이 외부에서도 해당 변수에 접근할 수 있었을 것이다.
function CreateCounter() {
var counter = 0;
this.increase = function () {
return ++counter;
};
this.decrease = function () {
return --counter;
};
}
const countNum1 = new CreateCounter();
const countNum2 = new CreateCounter();
console.log(countNum1.increase()); // 1
console.log(countNum1.increase()); // 2
console.log(countNum2.decrease()); // -1
console.log(countNum2.decrease()); // -2
동일한 로직의 클로져 함수를 2개의 독립적인 함수로 사용 시에는 위 예시와 같이 별개로 변수에 생성하면 된다. 이렇게 되면 개별의 함수 실행 컨텍스트가 생성되어 countNum1과 countNum2는 자유 변수 counter를 공유하지 않고 각자의 counter 상태를 가지면서 내부함수를 실행하게 된다.
데이터 공유
앞서 활용 예시 1과 같이 여러 개의 함수가 하나의 상태 데이터를 공유할 수 있다.
캡슐화가 가능하다.
상태를 담은 변수와, 상태를 변경하는 함수를 하나로 묶어 관심사의 분리를 이뤄낼 수 있다.
모듈화가 가능하다.
활용 예시 3과 같이 동일한 로직으로 재사용이 필요한 경우, 모듈화하여 코드 반복없이 사용 가능하다.
메모리 누수가 발생할 수 있다.
클로져는 외부함수의 변수를 참조하고 있기 때문에 더이상 외부함수의 변수를 사용하지 않더라도 참조로 인한 메모리 사용이 이뤄지고 있을 수 있다.
함수를 중첩해서 선언하기 때문에, 코드 가독성이 상대적으로 떨어질 수 있다.
디버깅이 복잡하다.
외부함수의 변수를 클로져가 참조하고 있기 때문에 클로져 함수 자체만을 디버깅 툴로 돌렸을 때 문제가 발생할 수 있어, 유의해야한다.
모던 자바스크립트 DEEP DIVE & POIEMA Web