티스토리 뷰
자바스크립트에서 함수는 일급객체(first-class object)이다.
일급 객체란 변수처럼 대상을 다룰 수 있는 것을 얘기하는데 즉, 자바스크립트에서 함수는 변수처럼 다룰 수 있다.
클로저는 함수의 일급객체성질을 이용한다.
함수가 생성될 때 함수 내부에서 사용되는 변수들이 외부에 존재하는 경우 그 변수들은 함수의 스코프에 저장된다.
함수와 함수가 사용하는 변수들을 저장한 공간을 클로저라고 한다.
여기까지는 아직 무슨 말인지 이해가 안가니 클로저의 정의에 대해 더 찾아보기로 했다.
클로저(Closure) 정의 찾아보기
- 함수와 그 함수가 선언된 어휘적 환경( Lexical Environment)의 조합이다.
- 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 가르킨다.
- 내부함수가 외부함수의 지역변수에 접근 할 수 있고, 외부함수는 외부함수의 지역변수를 사용하는 내부함수가 소멸될 때까지 소멸되지 않는 특성을 의미한다.
- 클로저는 함수를 지칭하고 또 그 함수가 선언된 환경과의 관계라는 개념이 합쳐진것이다.
- 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수 (→ 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수)
- 내부함수가 유효한 상태에서 외부함수가 종료하여 외부함수의 실행 컨텍스트가 반환되어도, 외부함수 실행 컨텍스트 내의 활성 객체(Activation object)는 내부함수에 의해 참조되는 한 유효하여 내부함수가 스코프 체인을 통해 참조할 수 있는 것을 의미한다.
어휘적 범위 지정(Lexical scoping)과 클로저
아래와 같은 코드를 입력하고 디버깅을 해보면
let l0 = 'l0';
function fn2() {
let l2 = 'l2';
console.log(l0, l1, l2);
}
function fn1() {
let l1 = 'l1';
console.log(l0, l1);
fn2();
}
fn1();
function fn2의 console.log 줄에서 에러가 발생한다.
l1은 fn2의 execute context에서 scope chain에 존재하지 않는다.
어디에서 호출했는지에 따라 접근할 수 있는 유효범위가 달라진다면 dynamic scope(동적 스코프)라고 한다.
자바스크립트는 정적 스코프를 채택하고 있다.
fn2를 fn1안에 작성하고 실행해보면 에러가 나지 않는다.
자바스크립트는 함수를 리턴하고, 리턴하는 함수가 클로저를 형성하기 때문이다. 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.
Scope 부분을 살펴보면 Closure(fn1) 이라는 것이 생겼고, Closure 안에 fn1 함수의 local이 저장되어 있는걸 확인할 수 있다.
l1을 불러올 때 local를 보고 없으면 그 parent인 closure(fn1)를 본다.
→ 함수를 함수 안에 정의하면 그 함수의 부모 함수의 scope에 접근할 수 있다.
함수의 유효범위(scope)는 함수를 어디서 실행했느냐가 아니라, 함수가 어디서 정의되었느냐(선언했느냐)에 따라 달라진다.
이를 Lexical scoping(혹은 static scoping)이라고 한다.
여기서 "Lexical"이란, 어휘적 범위 지정(lexical scoping) 과정에서 변수가 어디에서 사용 가능한지 알기 위해 그 변수가 소스코드 내 어디에서 선언되었는지 고려한다는 것을 의미한다. 단어 "lexical"은 이런 사실을 나타낸다. 중첩된 함수는 외부 범위(scope)에서 선언한 변수에도 접근할 수 있다.
fn2함수 안에 fn3를 한번 더 중첩시켜보면
function 더하기함수공장(초기값){
var 보정값 = 1;
function 덧셈(숫자){
보정값 = 100;
return 초기값 + 숫자;
}
return 덧셈;
}
let 더하기1 = 더하기함수공장(1);
let 더하기2 = 더하기함수공장(2);
// 클로저에 "초기값"과 "보정값"의 환경이 저장됨
console.log(더하기1(1));
console.log(더하기1(2));
// 함수 실행 시 클로저에 저장된 "초기값", "보정값" 값에 접근하여 값을 계산
위와 같은 코드에서
더하기1(1)은 더하기함수공장()이 return하는 덧셈()의 return값
덧셈()에서 "초기값"에 접근하는 방법은 closure를 통해 접근할 수 있다.
더하기1, 더하기2 둘 다 클로저이다. 이 둘은 같은 함수 본문 정의(function body definition)를 공유하지만 서로 다른 어휘적 환경(lexical environments)을 저장한다.
더하기1의 어휘적 환경에서 클로저 내부의 "초기값"은 1이지만 더하기2의 어휘적 환경에서 "초기값"은 2이다.
또한, 리턴되는 함수에서 초기값이 1로 할당된 "보정값"에 접근하여 "보정값"값을 100으로 변경한 것을 보면,
클로저가 리턴된 후에도 외부함수의 변수들에 접근 가능하며 클로저에 단순히 값 형태로 전달되는 것이 아니라는 것을 의미한다. ("초기값"값도 동일하게 변경 가능)
함수가 생성될 당시의 외부 변수를 기억하고, 생성 이후에도 계속 접근이 가능하다.
외부 함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수에 접근할 수 있다.
이때 내부함수가 외부함수에 있는 변수의 복사본이 아니라 실제 변수에 접근한다는 것에 주의하여야 한다.
덧셈() 이라는 함수를 더하기함수공장()에서 호출한 것이 아니라
return된 console.log()에서 호출된 것임에도 불구하고
더하기공장함수(1)의 인수 1을 내부적으로 scope으로 간직하고 있으니 접근 가능하다.
클로저 스코프 체인
모든 클로저에는 3가지 스코프가 있고, 세가지 범위 모두 접근할 수 있다.
- 지역 범위 (Local Scope, Own scope)
- 외부 함수 범위 (Outer Functions Scope)
- 전역 범위 (Global Scope)
// 전역 범위 (global scope)
var e = 10;
function sum(a){
return function(b){
return function(c){
// 외부 함수 범위 (outer functions scope)
return function(d){
// 지역 범위 (local scope)
return a + b + c + d + e;
}
}
}
}
console.log(sum(1)(2)(3)(4)); // 20
클로저 활용
활용 1) 상태 유지
현재 상태를 기억하고 변경된 최신 상태를 유지할 수 있다.
<!DOCTYPE html>
<html>
<body>
<button class="toggle">toggle</button>
<div class="box" style="width: 100px; height: 100px; background: red;"></div>
<script>
var box = document.querySelector('.box');
var toggleBtn = document.querySelector('.toggle');
var toggle = (function () {
var isShow = false;
// ① 클로저를 반환
return function () {
box.style.display = isShow ? 'block' : 'none';
// ③ 상태 변경
isShow = !isShow;
};
})();
// ② 이벤트 프로퍼티에 클로저를 할당
toggleBtn.onclick = toggle;
</script>
</body>
</html>
① 즉시실행함수가 반환한 함수는 자신이 생성됐을 때의 렉시컬 환경에 속한 변수 isShow를 기억하는 클로저다.
클로저가 기억하는 변수 isShow는 box 요소의 표시 상태를 나타낸다. (즉시실행함수는 함수를 반환하고 즉시 소멸한다)
② 클로저를 이벤트 핸들러로서 이벤트 프로퍼티에 할당했다.
이벤트 프로퍼티에서 이벤트 핸들러인 클로저를 제거하지 않는 한 클로저가 기억하는 렉시컬 환경의 변수 isShow는 소멸하지 않는다. 다시 말해 현재 상태를 기억한다.
③ 버튼을 클릭하면 이벤트 프로퍼티에 할당한 이벤트 핸들러인 클로저가 호출된다.
이때 .box 요소의 표시 상태를 나타내는 변수 isShow의 값이 변경된다. 변수 isShow는 클로저에 의해 참조되고 있기 때문에 유효하며 자신의 변경된 최신 상태를 계속 유지한다.
만약 자바스크립트에 클로저라는 기능이 없다면 상태를 유지하기 위해 전역 변수를 사용할 수 밖에 없다.
전역 변수는 언제든지 누구나 접근할 수 있고 변경할 수 있기 때문에 많은 부작용을 유발해 오류의 원인이 되므로 사용을 억제해야 한다.
활용 2) 전역 변수의 사용 억제
변수의 값은 누군가에 의해 언제든지 변경될 수 있어 오류 발생의 근본적 원인이 될 수 있다. 상태 변경이나 가변(mutable) 데이터를 피하고 불변성(Immutability)을 지향하는 함수형 프로그래밍에서 부수 효과(Side effect)를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.
예제 참고
https://poiemaweb.com/js-closure
활용 3) 정보의 은닉화
function Counter() {
// 카운트를 유지하기 위한 자유 변수
var 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는 increase, decrease 메소드를 갖는 인스턴스를 생성한다. 이 메소드들은 생성자 함수 Counter의 스코프에 속한 변수 counter를 기억하는 클로저이며 렉시컬 환경을 공유한다.
생성자 함수 Counter가 생성한 객체의 메소드는 객체의 프로퍼티에만 접근할 수 있는 것이 아니며 자신이 기억하는 렉시컬 환경의 변수에도 접근할 수 있다.
이때 생성자 함수 Counter의 변수 counter는 this에 바인딩된 프로퍼티가 아니라 변수다. counter가 this에 바인딩된 프로퍼티라면 생성자 함수 Counter가 생성한 인스턴스를 통해 외부에서 접근이 가능한 public 프로퍼티가 되지만 생성자 함수 Counter 내에서 선언된 변수 counter는 생성자 함수 Counter 외부에서 접근할 수 없다.
하지만 생성자 함수 Counter가 생성한 인스턴스의 메소드인 increase, decrease는 클로저이기 때문에 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 변수 counter에 접근할 수 있다.
참고 자료
🔗 [javascript.info] 변수의 유효범위와 클로저
🔗 [코딩앙마] 자바스크립트 중급 강좌 #11 클로저(Closure) 5분만에 이해하기
'개발공부 > 🟨 JavaScript' 카테고리의 다른 글
[JS] 자바스크립트 비동기 처리 (0) | 2023.01.29 |
---|---|
[JS] 호이스팅 (Hoisting) (0) | 2023.01.18 |
[JS] 실행 컨텍스트 (Execute context) (0) | 2023.01.04 |
[JS] 이벤트 루프 (Event Loop) (0) | 2022.09.30 |
[JS] 내가 놓쳤던 자바스크립트 기초문법 정리 (0) | 2022.08.15 |
프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그