JavaScript - Closure (클로저)

Tae Yun Choi·2022년 9월 11일
0

개발새발 JavaScript

목록 보기
1/2
post-thumbnail

클로저는 자바스크립트의 개념 중 하나로 이해하기에 까다롭기로 유명한 개념이다.

DeepDive 스터디를 통해 공부한 개념을 다시금 복습하는 의미로 글을 작성해보려한다.

클로저란

A closure is the combination of a function and the lexical environment within which that function was declared

이는 MDN에서 정의한 클로저의 개념이다. 이것만 보면 도대체 클로저가 어떠한 개념인지 와닿지는 않는다. 하지만 몇가지 키워드가 눈에 띈다.

combination of a function and the lexical environment

함수와 렉시컬 환경으로 인해 파생된 개념으로 유추하고 추가적인 내용을 봐야할 것 같다.

클로저 예시

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

위 코드에서 innerFunc는 10을 출력한다. innerFunc의 Environment Record에 x에 대한 정보가 없지만, 스코프 체인을 통해 상위 스코프인 outerFunc의 렉시컬 환경을 참조하게 되고, outerFunc의 x에 접근 할 수 있기 때문이다. 이러한 이해는 실행컨텍스트를 공부하면 충분히 이해가 되는 상황이다.

같은 개념을 적용해서,

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

위 코드에서 x는 1을 출력한다. 자바스크립트는 렉시컬 스코프를 따르는 프로그래밍 언어이기 때문에, 현재 innerFunc의 렉시컬 환경의 상위스코프는 전역 스코프이고 전역스코프에 선언된 x는 1이다.

렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프이다.

이러한 이해를 바탕으로, 이제 클로저에 대한 예제를 살펴보자

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

const innerFunc = outer();
innerFunc();

클로저를 모르는 상태에서는 위 코드를 실행하면 undefined가 나올 것으로 예상된다. 왜나하면 outer함수의 실행이 종료되고, outer함수의 실행컨텍스트는 제거되었기 때문에 outer함수 내부에 있는 x값도 제거될 것으로 생각되기 때문이다. 하지만 위 코드를 실행하면 10이 출력된다. 함수가 종료되었는데 어떻게 함수 내부에서 정의된 x값에 접근이 가능한 것 일까?

외부 함수보다 내부함수가 더 오래 유지되는 경우 내부함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 내부 함수를 클로저라고 부른다.

이해하기 편하게 스스로 정의를 해보자면,
함수 내부에서 정의된 함수를 외부에서 참조하고 있을 경우 외부 함수가 종료되어도 그 참조를 유지한다는 것이다. 외부 함수가 종료될 때, 외부 함수의 실행컨텍스트는 사라지지만 외부 함수 렉시컬 환경의 변수가 다른 곳에서 계속 참조를 하고 있을 경우, 함수가 종료되어도 렉시컬 환경은 사라지지 않는다. 이 부분은 [[Environment]]라는 내장 객체와 연관이 있다.

클로저 활용

이러한 클로저의 기능은 상태를 안전하게 변경하고 유지하기 위해 사용될 수 있다.

let num = 0;
const increase = function(){
  return ++num;
};
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

위와 같은 코드는 num 변수를 전역으로 설정하였기 때문에 어디서든 접근할 수 있고, 이로 인해 Side Effect를 방지하기 어렵다.

그렇다면 지역 변수로 설정하면 어떨까?

const increase = function(){
  let num = 0;
  return ++num;
};
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1

위와 같은 코드는 num 변수를 함수 내부에서 선언하였기 때문에 외부에서 접근은 불가능하다. 하지만 함수를 실행할 때마다 0으로 초기화가 되기 때문에 제대로 동작하지 않는다.

그럼 이제 이를 해결하기 위해 클로저를 사용해보자

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

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

위 코드는 즉시실행함수로 실행되는 즉시 실행되고 return 된 익명함수의 참조값이 increase 변수에 할당된다. 즉시실행함수는 종료된 상태이고, 즉시실행함수 내부에서 선언된 익명함수는 increase에서 참조하고 있는 상태이다. 따라서 즉시실행함수 내부의 num 변수에는 반환된 익명함수로만 접근이 가능하게 되었다. 이처럼 클로저는 상태에 대한 side effect를 방지하고, 특정 함수에게만 상태 변경을 허용하도록 하기 위하여 사용된다.

마치며

클로저는 실행컨텍스트, 렉시컬 환경에 대한 이해가 있어야 이해하기에 수월하다. 처음 실행컨텍스트와 렉시컬 환경에 대한 이론을 접하고, 클로저를 접했을 때 정말 복잡하고 어려웠었다. 거기에 자주 써보지 않았던 즉시실행함수까지 사용되니 어려움은 배가 되었던 기억이 있다. 그래도 계속 상기시키려 노력하고 클로저를 직접 구현해서 사용해보면서 처음보다는 많이 친숙해진 느낌이다. 또한, 최근 React를 공부하며 useState Hook이 클로저로 구현이 되어있다는 것을 알았고, 내부 구현을 해보며 다시 상기되었었다. 자바스크립트가 일급 객체를 지원하는 언어인 만큼 이러한 복잡한 개념이 많지만, 잘 사용할 수만 있다면 그만큼 다양한 프로그래밍을 구사할 수 있을 것 같다는 생각이 든다.

profile
hello dev!!

0개의 댓글