클로저
클로저란, 자신이 선언된 렉시컬 환경을 기억하고 참조하는 함수다.
MDN에서는,
클로저는 독립적인 (자유) 변수를 가리키는 함수이다.
클로저 안에 정의된 함수는 만들어진 환경을 ‘기억한다’. 라고 한다.
클로저를 이해하기 위해선 우선 스코프를 알아야 한다.
스코프
스코프에 대해서는 간단한 예시만을 다룰 예정이다.
잘 모른다면,
이전에 정리한 스코프에 대한 글을 먼저 보는게 낫겠다.
위의 예시에서 함수 abc는 내부에서 변수 a를 활용한다.
함수 스코프와 같은 {} 코드 블럭 안에선 변수 a에 대해 참조를 할 때,
함수 스코프를 먼저 탐색하기 때문에, 매개변수에 해당하는 a를 반환하게 된다.
만약 함수 스코프에 지역 변수 a가 없다면?
함수가 선언된 스코프를 탐색하며, 발견이 될 때 까지 전역스코프까지 올라가며, 전역에 선언된 a를 발견하면 출력한다.
전역 스코프에도 변수 a가 선언되어 있지 않다면, 레퍼런스 에러 'a is not defined' 에러가 출력 될 것이다.
두 가지 예를 추가로 이해해보면,
좌측 코드의 스코프를 그림으로 나타내면 우측과 같다.
함수 k에서 b를 찾아서 출력해야 하는데, k 함수 스코프에서 b가 존재하지 않으므로 k 함수가 선언된 if 스코프로 올라간다.
if 스코프에 있는 b=3 값을 출력한다.
이 예시에서는, k 함수 스코프에서 b가 존재하지 않으므로 k 함수가 선언된 if 스코프로 올라간다.
if 스코프에 b가 존재하지 않으므로, if 스코프가 선언된 전역 스코프로 올라간다.
전역 스코프에 b가 선언되어 있으므로, b=2 값을 출력한다.
엄밀히 말하면, const, let 키워드와 var 키워드의 스코프는 다르지만
최근 문법에서는 변수 선언 시 var는 지양되는 편이다.
그래서, const,let의 블록레벨스코프를 기준으로 생각해보자.
클로저의 특징
1. 이미 실행이 끝난 스코프를 참조한다.
실행이 끝난 함수의 스코프는, 더이상 참조가 되지 않는다면 가비지컬렉터에 의해 정리된다.
그러나 클로저의 개념에서는 내부 함수가 해당 스코프를 기억하므로, 실행이 끝난 스코프도 계속 참조되어 사라지지 않는다.
2. 1의 특징을 활용한다면 은닉화가 가능하다.
Private 변수의 예시를 생각하면 되겠다.
클로저를 활용한다면 외부에서 상위 환경에 선언한 변수에 직접 접근할 수 없다.
만약 이 변수에 대한 제어를 하고 싶다면, 내부 함수에 get이나 set 등의 인터페이스를 추가해줘야 한다.
3. 재사용이 가능하다.
클로저를 이용하면 같은 함수 정의를 공유하면서도, 렉시컬 환경(상위 스코프)가 다른 함수를 생성할 수 있다.
클로저를 생성해주는 함수를 실행시키고 그 반환 값을 각각의 변수에 할당하면 끝이다.
동일한 기능을 하는 통장을 발행해주는 은행을 예로 들면
계좌1, 계좌2, 계좌3 선언할 수 있고 각각 독립된 스코프를 가지므로 독립된 데이터(잔고)를 가지도록 할 수 있다.
클로저의 예시
내부 함수에서 외부 함수의 스코프에 접근한다.
여기서 외부 함수의 스코프란, 렉시컬 스코프를 의미하며 아래의 코드에 나타나 있다.
const outerFunc = () => {
const name = 'Tom';
const innerFunc = () => console.log('my name is ' + name);
return innerFunc;
}
const inner = outerFunc(); // my name is Tom
inner(2);
기본적으로, 함수 안에 있는 지역변수들은 그 함수가 처리되는 동안에만 존재하고 사라지므로,
outerFunc 함수의 name 변수 또한 사라질 것이라고 예상하는 것이 일반적이다.
위 코드에서는 outerFunc 함수의 실행 결과로 inner 함수를 반환 받게 되는데,
inner 함수가 존재하는 한, inner함수가 outerFunc 함수의 스코프에 있는 name을 참조하기 때문에,
outerFunc 함수의 스코프에 있는 name 변수는 사라지지 않게 된다.
주절주절 얘기했지만, 결국 inner 함수가 존재하는 한 outer 함수의 스코프에 선언된 변수를 이용할 수 있다는 것이다.
클로저의 예시2
특징3의 계좌의 예시와 같이, 각각 독립된 변수를 다뤄야 한다면
const outerFunc = () => {
let money = 0;
let cnt = 0;
const innerFunc = (amount) => {
if(amount){
money += amount;
cnt++;
}
console.log('계좌 잔액 : ', money);
console.log('계좌 입금 횟수 :', cnt);
};
return innerFunc;
}
const account = outerFunc();
account(10000);
account(10000);
// 계좌 잔액 : 10000
// 계좌 입금 횟수 : 1
// 계좌 잔액 : 20000
// 계좌 입금 횟수 : 2
const account2 = outerFunc();
account2(50000);
account2(30000);
// 계좌 잔액 : 50000
// 계좌 입금 횟수 : 1
// 계좌 잔액 : 80000
// 계좌 입금 횟수 : 2
이와 같이 선언하면 되겠다.
여기서 account, account2는 둘 다 클로저이며, 서로 다른 렉시컬 환경을 가진다.
각각의 변수마다 다른 상위 스코프를 가진다.
클로저의 주의사항
앞선 예시에서 살펴 봤듯이, 이미 실행이 끝난 외부함수의 스코프가 내부함수에 의해 참조되어야 하므로, 사라지지 않고 남아있다.
참조가 더이상 되지 않는 스코프가 가비지컬렉터에 의해 정리가 되는 것이 정상적이다.
그러나 클로저를 이용하면, 이 외부함수의 스코프는 계속 남아있을 것이고 가비지컬렉터가 작동하지 않을 것이다.
즉, 클로저의 남용은 결국 메모리 문제를 일으킬 수도 있다.
따라서 클로저의 장단점을 파악하여 적재적소에 사용해야 하며,
라이프사이클이 끝난 함수는 참조를 제거해서 메모리가 회수될 수 있도록 돕는 것이 좋겠다.
참고 사이트
https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures
https://poiemaweb.com/js-scope
https://hanamon.kr/javascript-클로저/
'JavaScript > theory' 카테고리의 다른 글
[자바스크립트] 일반 객체 다루기 (0) | 2022.01.11 |
---|---|
[자바스크립트] 고차함수와 배열 내장 메서드 (0) | 2022.01.06 |
[객체] 자바스크립트에서 객체를 생성하는 다양한 방법 (0) | 2022.01.04 |
[자바스크립트] 비동기 작업의 동기 처리를 위한 Async/Await (0) | 2021.08.22 |
[자바스크립트] 마이크로 태스크 큐와 프로미스 (0) | 2021.08.22 |