JavaScript는 함수 지향 언어이다. 그리고 자바스크립트에서 중요한 개념 중 하나인 클로저(Closure)는 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.
클로저(Closure)의 개념
클로저는 자바스크립트만의 고유한 개념은 아니지만, MDN은 다음과 같이 클로저에 대해 정의하고 있다.
클로저는 함수와 함수가 선언된 어휘적 환경(Lexical Environment)의 조합이다.
조금 더 쉽게 설명하자면, 함수가 속한 렉시컬 환경(Lexical Environment)을 기억해서 함수가 렉시컬 환경 밖에서 실행될 때도 상위 스코프에 접근할 수 있는 방식을 말한다.
여기서 렉시컬 환경(Lexical Environment) 또는 렉시컬 스코프(Lexical Scope)란, 함수가 실행되는 위치가 아닌 선언되는 위치에 따라서 달라지는 유효 범위에 대한 개념이다.
Lexical Environment에 대해 정리한 포스팅을 먼저 읽고 온다면 클로저에 대해 더욱 쉽게 이해할 수 있다.
[JavaScript] Execute Context(실행 컨텍스트)
자바스크립트의 코드가 실행되는 원리인 excute context에 대한 내용을 정리해보면서 변수의 유효 범위에 대해서도 함께 이해할 수 있도록 정리해보자. 실행 컨텍스트 (Execute Context )란? 실행할 코드
small-habit.tistory.com
렉시컬 스코핑(Lexical Scoping)
자바스크립트는 함수 안에서 또 다른 함수를 선언할 수 있다. 이를 내부함수라고 한다.
function outer(){
function inner(){
let title = 'small-habit';
console.log(title);
}
inner();
}
outer(); // small-habit
위의 예제 코드의 결과는 'small-habit'이 출력될 것이다.
내부함수는 외부 함수의 지역 변수에 접근할 수 있다. 아래의 예제 코드를 보자.
function outer(){
let title = 'small-habit';
function inner(){
console.log(title);
}
inner();
}
outer(); // small-habit
위 코드를 실행하면 inner( ) 함수 안에서 외부함수(부모함수)에서 정의한 변수 title의 값인 'small-habit'이 출력될 것이다. 이 예시를 통해서 함수가 중첩된 상황에서 Parser가 어떻게 변수를 처리하는지 알 수 있다.
inner( ) 함수가 outer( ) 함수의 내부에 선언된 내부함수이므로 inner( )는 자신이 속한 렉시컬 스코프(Lexical Scope)를 참조할 수 있다. 이것을 실행 컨텍스트(Execute Context)의 관점에서 살펴보자.
내부함수 inner( )가 호출되면 자신의 실행 컨텍스트가 Call Stack에 쌓이고 변수 객체(Variable Object)와 스코프 체인(Scope Chain) 그리고 this에 바인딩할 객체가 결정된다. 이 때 스코프 체인은 전역 스코프를 가리키는 전역 객체와 외부 함수 outer( )의 스코프를 가리키는 outer( )의 활성 객체(Activation Object), 그리고 함수 자신의 스코프를 가리키는 활성 객체를 순차적으로 바인딩한다. 스코프 체인이 바인딩한 객체가 바로 렉시컬 스코프이다.
내부함수 inner( )가 자신을 포함하고 있는 외부함수 outer( )의 변수 title에 접근할 수 있는 것, 다시 말해 상위 스코프에 접근할 수 있는 것은 렉시컬 스코프의 외부 환경 참조(Outer Environment Reference)를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색했기 때문에 가능한 것이다. 좀 더 자세히 설명하면 아래와 같다.
1. inner( ) 함수 스코프 내에서 변수 title을 검색한다 → 검색 실패
2. inner( ) 함수를 포함하는 외부 함수 outer( ) 함수의 스코프에서 변수 title을 검색한다. → 검색 성공
클로저(Closure)
이번에는 내부함수 inner( )를 outer( ) 함수에서 호출하는 것이 아니라 반환하도록 변경해 보자.
function outer(){
let title = 'small-habit';
function inner(){
console.log(title);
}
return inner;
}
let myFunc = outer();
myFunc(); // small-habit
예제를 살펴보면, myFunc라는 변수는 outer( ) 함수를 호출하고 있다. 그래서 myFunc라는 변수를 실행하게 되면 코드의 결과는 small-habit이 출력될 것이다. 여기서 유심히 봐야 할 점은 outer( ) 함수는 내부함수 inner( )를 반환하고 life-cycle이 종료되었는데, 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수 title에 접근할 수 있다는 것이다.
이러한 매커니즘을 클로저(Closure)라고 한다. 즉, 클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical Environment)인 렉시컬 스코프를 기억하여 자신이 선언됐을 때의 스코프 밖에서 호출되어도 그 스코프에 접근할 수 있는 함수를 말한다.
| 클로저는 자신이 생성될 때의 환경(Lexical Environment)을 기억하는 함수이다.
클로저(Closure)의 활용
클로저(Closure)는 자신이 생성될 때의 환경(Lexical Environment)를 기억해야 하므로 메모리 차원에서는 손해를 볼 수 있다. 하지만 클로저는 프론트엔드 실무에서 다방면으로 이용이 가능한 강력한 기능이기 때문에 적극적으로 사용해야 한다.
#1. 상태 유지
클로저가 유용하게 쓰이는 기능 중 하나는 현재 상태를 기억하고, 상태가 변경되면 이를 최신 상태로 유지하는 것이다.
<!DOCTYPE html>
<html>
<body>
<button class="toggle">toggle</button>
<div class="txt">
<h1>small-habit</h1>
</div>
<script>
let txtField = document.querySelector('.txt');
let toggleBtn = document.querySelector('.toggle');
let toggle = (function () {
let isVisable = false;
// 1.클로저를 반환
return function () {
txtField .style.display = isVisable ? 'block' : 'none';
// 3. 상태 변경
isVisable = !isVisable;
};
})();
// 2. 이벤트 프로퍼티에 클로저를 할당
toggleBtn.onclick = toggle;
</script>
</body>
</html>
결과
small-habit
위의 코드에서 보면
- 즉시 실행 함수는 함수를 반환한 뒤 바로 소멸된다. 이때 반환된 함수는 자신이 생성됐을 때의 렉시컬 환경(Lexical environment)에 속한 변수 isVisable을 기억하는 클로저(Closure)이다. 클로저가 기억하는 변수 isVisable은 txt 요소의 표시 상태를 나타낸다.
- 클로저는 이벤트 핸들러로서 버튼 이벤트 프로퍼티에 할당했다. 이벤트 프로퍼티에서 이벤트 핸들러인 클로저를 제거하는 않는 이상 클로저가 기억하는 렉시컬 환경의 변수 isVisable은 소멸하지 않는다. 다시 말해 현재 상태를 기억한다.
- 버튼 클릭 시 클로저가 호출되는데 이 때, .txt 요소의 표시 상태를 나타내는 변수 isVisable의 값이 변경된다. 이 변수는 클로저에 의해 참조되고 있기 때문에 자신의 변경된 최신 상태를 계속 유지할 수 있다.
#2. 전역 변수 억제
전역 변수를 통해서 공유될 변수를 작성하면 누구든 이 전역 변수에 접근할 수 있고, 값을 변경할 수 있게 된다. 이러한 상황은 오류가 발생할 확률이 매우 높아진다.
이러한 경우에 클로저를 활용하여 전역 변수를 함수의 지역 변수로 바꾸면 의도치 않은 상태 변경을 방지할 수 있다.
<!DOCTYPE html>
<html>
<body>
<button id="inclease">+</button>
<p id="count">0</p>
<script>
let incleaseBtn = document.getElementById('inclease');
let count = document.getElementById('count');
let increase = (function () {
// 카운트 상태를 유지하기 위한 자유 변수
let counter = 0;
// 클로저를 반환
return function () {
return ++counter;
};
}());
incleaseBtn.onclick = function () {
count.innerHTML = increase();
};
</script>
</body>
</html>
결과
0
위의 코드 예제는 클로저를 설명할 때 흔히 사용되는 예제이다. 버튼이 클릭될 때마다 클릭한 횟수가 누적되어 화면에 출력된다.
- 코드가 실행되면 즉시실행함수가 호출되고, 변수 increase에는 함수 function () { return ++counter; }가 할당된다. 이 함수는 자신이 생성됐을 때의 렉시컬 환경(Lexical Environment)를 기억하는 클로저(Closure)이다.
- 즉시실행함수는 호출된 이후 소멸되지만, 즉시실행함수가 반환한 함수는 변수 increase에 할당되어 버튼을 클릭하면 이벤트 핸들러 내부에서 호출된다. 이때 클로저는 자신이 선언됐을 때의 렉시컬 환경인 즉시실행함수의 스코프(Scope)에 속한 지역변수 counter를 기억한다. 변수 counter는 자신을 참조하는 함수가 소멸될 때까지 유지된다.
즉시실행함수는 한번만 실행되기 때문에 increase가 호출될 때마다 변수 counter가 초기화되는 일은 없다. 변수 counter는 외부에서 직접 접근할 수 없는 private 변수이므로 의도치않게 값이 변경되는 오류가 발생하지 않는다.
#3. 데이터의 은닉
function Counter() {
// 카운트를 유지하기 위한 자유 변수
let counter = 0;
// 클로저
this.increase = function () {
return ++counter;
};
// 클로저
this.decrease = function () {
return --counter;
};
}
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0
위의 예제는 생성자 함수 Counter를 생성하고 이를 통해 counter 객체를 만드는 코드이다.
- 생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스를 생성한다. 이 메소드들은 모두 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 스코프에 속한 변수 counter를 기억하는 클로저이다. 생성자 함수가 생성한 객체의 메소드는 자신이 기억하는 렉시컬 환경의 변수에도 접근할 수 있다.
- 이 때 생성자 함수 Counter의 변수 counter는 this에 바인딩된 프로퍼티가 아니라 변수이다. 그렇기 때문에 생성자 함수 Counter 내에서 선언된 변수 counter는 생성자 함수의 외부에서는 접근할 수 없다. 하지만 생성자 함수가 생성한 인스턴스의 메소드인 increase와 decrease는 클로저이기 때문에 생성자 함수의 변수 counter에 접근할 수 있다.
이러한 클로저(Closure)의 특징을 사용해 Java와 같은 클래스 기반 언어의 private 메소드를 흉내낼 수 있다.
[참고자료]
클로저 - JavaScript | MDN
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
developer.mozilla.org
Closure | PoiemaWeb
클로저(closure)는 자바스크립트에서 중요한 개념 중 하나로 자바스크립트에 관심을 가지고 있다면 한번쯤은 들어보았을 내용이다. execution context에 대한 사전 지식이 있으면 이해하기 어렵지 않
poiemaweb.com
코어자바스크립트 강의정리 - 5. 클로저 - 휴식중인 까치님의 블로그 - 인프런 | 커뮤니티
코어자바스크립트 강의정리 - 5. 클로저 - 클로저란 실행 컨텍스트 A 안에서 함수 B가 선언 되었을 때, 'A의 lexical environment와 내부함수 B의 조합에서 나타나는 특별한 현상' 이다. 대체 어떤 특
www.inflearn.com
'Front-End > JavaScript' 카테고리의 다른 글
[JavaScript] 콜백함수(Callback Function) (0) | 2023.09.08 |
---|---|
[JavaScript] var, let, const 에 대해 알아보자 (0) | 2023.09.01 |
[JavaScript] Execute Context(실행 컨텍스트) (0) | 2023.08.31 |