heyday2024 님의 블로그
[JS 문법 5주차(4)] 클로저 본문
클로저 (Closure)
A closure is the combination of a function and the lexical environment within which that function was declared - MDN
(클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합임.)
??? 이게 무슨 말인지 하나씩 해석해보자!
함수가 선언된 렉시컬 환경(record + outer)이란??
----> 함수가 선언될 당시의 외부 변수 등의 정보
//클로저
const x = 1;
function outerFunc() {
const x = 10;
function innerFunc() {
//함수가 선언된 렉시컬 환경
// 렉시컬환경(LE)는 record(식별자 정보), outer(외부 환경 정보) 가짐
// --->즉, 함수가 선언된 렉시컬 환경 === 함수가 선언될 당시의 외부 변수 등의 정보
console.log(x); // 10
}
innerFunc();
}
outerFunc();
- innerFunc() 내부의 console.log(x)에서 참조하고 있는 x 값은
- 먼저 innerFunc() 스코프 내부에서 x 값을 찾음.
- 없는 경우 scope chain에 의해 바로 바깥쪽 scope인, outerFunc()에서 x를 찾음(10).
- 실행컨텍스트에서 배웠던 outer를 찾는 것임
- outer는 해당 실행컨텍스트의 생성시점의 LexicalEnvironment를 갖고 있음.
- 그래서 10에 먼저 접근하고, console.log(x)는 10이 출력됨. ---> innerFunc()의 LE는 outerFunc() 의 정보와 x =10의 정보를 가지고 있음. 거기서 기억하고 있는 x 정보를 출력한 것임.
자바스크립트는 함수를 어디서 정의했는지에 따라 함수의 상위 스코프 결정함!!!!
- 렉시컬 스코프(lexical scope)/ 정적 스코프(static scope): 함수 정의가 평가되는 시점에서 상위 스코프가 결정됨.
const x = 1;
// innerFunc()에서는 outerFunc()의 x에 접근할 수 없죠.
// Lexical Scope를 따르는 프로그래밍 언어이기 때문
function outerFunc() {
const x = 10;
innerFunc(); // 1
}
function innerFunc() {
console.log(x); // 1
}
outerFunc();
- innerFunc()가 정의된 위치를 보면 innerFunc()의 상위 스코프는 outerFunc()가 아닌 전역 객체(함수) 라는 것을 알 수 있음.
- 즉, x = 1을 출력함.
- outerFunc와 innerFunc는 서로 다른 Scope를 가지고 있어서 변수를 공유할 수 없음. 즉, innerFunc 함수를 outerFunc 함수의 내부에서 호출한다 하더라도 outerFunc 함수의 변수에 접근할 수는 없음.
다시 말하지만, JS 엔진은 함수를 어디서 호출했는지가 아니라 어디에 정의했는지에 따라 상위 스코프 결정함!!!
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
- “외부 렉시컬 환경에 대한 참조”에 저장할 참조값, 즉, 스코프에 대한 참조는 함수정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프(정적 스코프라고도 함.)
클로저와 렉시컬 환경(LexicalEnvironment)
외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 여전히 참조할 수 있다. ← 이 개념에서 중첩 함수가 바로 클로저에요
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
}
return inner;
}
// outer 함수를 실행해서, innerFunc에 그 리턴값을 담는다.
// outer함수의 리턴값: function () { console.log(x); }
const innerFunc = outer();
// --------------------- 여기서는 outer 함수의 실행컨텍스트는?? 콜스택에서 빠져나가지만, 여전히 innerFunc()는 10을 출력
// 여기서의 inner 함수가 클로저
innerFunc();
- 여기서의 inner함수가 클로저임
- outer 함수를 호출하면 중첩 함수 inner를 반환(return)해요.
- 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스탭에서 팝되어 제거된다(역할을 다 했으니깐)
- inner 함수는 런타임에 평가된다.
- inner함수가 innerFunc에 전달되었는데, 이는 outer 함수의 렉시컬환경을 (여전히) 참조하고 있다.
- 즉, outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다. 그래서 10을 출력할 수 있었던 것
<구체적으로 어떻게 outer함수의 실행컨텍스트가 콜스택에서 제거되었는데도, 10을 출력할수 있었는지 알아보자.>
자바스크립트의 가비지 컬렉터는 참조되지 않는 데이터를 메모리에서 제거하는 역할을 함.
그런데 클로저가 생성된 경우, 중첩 함수가 외부 함수의 변수에 접근하고 있으므로 참조가 계속 유지됨.
==> 즉, 가비지 컬렉터는 해당 변수가 여전히 사용되고 있다고 판단하므로 메모리에서 제거하지 않음.
자바스크립트에서 함수의 실행 컨텍스트는 함수 실행이 끝나면 콜 스택에서 제거되지만, 클로저가 참조하는 변수들은 힙에 남아 있게됨. 이 힙 메모리에는 클로저에 의해 참조된 변수들이 저장되어 있어서 중첩 함수가 계속해서 외부 함수의 변수에 접근할 수 있음.
function foo() {
const x = 1;
const y = 2;
// 일반적으로 클로저라고 하지 않아요.
function bar() {
const z = 3;
//상위 스코프의 식별자를 참조하지 않기 때문이죠.
console.log(z);
}
return bar;
}
const bar = foo();
bar();
- 여기서의 bar()는 클로저가 아님!!!
- bar()는 외부 변수를 참조하는 것이 아닌 본인 스코프 내의 변수를 참조하고 있음.
function foo() {
const x = 1;
// bar 함수는 클로저였지만 곧바로 소멸한다.
// 외부로 나가서 따로 호출되는게 아니라, 선언 후 바로
// 실행 + 소멸
// 이러한 함수는 일반적으로 클로저라고 하지 않는다.
function bar() {
debugger;
//상위 스코프의 식별자를 참조한다.
console.log(x);
}
bar();
}
foo();
- 여기서의 bar()도 클로저가 아님
- 상위 스코프, 즉, 외부 변수를 참조는 하지만, foo() 실행 시 bar() 그냥 그 자리에서 x를 출력함. 즉, bar는 선언 후 바로 실행과 동시에 소멸함.
function foo() {
const x = 1;
const y = 2;
// 클로저의 예
// 중첩 함수 bar는 외부 함수보다 더 오래 유지되며
// 상위 스코프의 식별자를 참조한다.
function bar() {
debugger;
console.log(x);
}
return bar;
}
const bar = foo();
bar();
- 여기서의 bar()는 클로저임
- 외부 함수인 foo()의 실행 컨텍스트가 콜스택에서 제거되어도, bar는 여전히 살아있고 그 상위 스코프의 변수(foo() 내부 변수)를 여전히 참조함으로 클로저임.
클로저 활용
클로저는 JS의 강력한 기능
1. 클로저는 주로 상태를 안전하게 변경하고 유지하기 위해 사용함. 의도치 않은 상태의 변경을 막기 위해서 사용!
2. 클로저를 이용해 상태를 안전하게 은닉한다(특정 함수에게만 상태 변경을 허용한다)
// 카운트 상태 변경 함수 #1
// 함수가 호출될 때마다 호출된 횟수를 누적하여 출력하는 카운터를 구현해요!
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
console.log(increase()); //1 //1
// num = 100; // 치명적인 단점이 있어요.
console.log(increase()); //2 //101
console.log(increase()); //3 //102
- 위는 num을 increase함수를 이용해서 1씩 증가시킨다
- 만약, 중간에 num =100 처럼 값을 재할당 시켜버리면, num의 값이 쉽게 변경되어버린다..
<위 코드의 문제점을 보완하기 위해 고려해야할 부분!!>
- 카운트 상태(num 변수의 값)는 increase 함수가 호출되기 전까지 변경되지 않고 유지돼야 한다.
- 이를 위해 카운트 상태(num 변수의 값)는 increase 함수만이 변경할 수 있어야 한다.
- ======> 결국 이 모든 건 전역변수인 num이 문제다.
====> num을 지역변수로 변경해보자!!!
// 카운트 상태 변경 함수 #2
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
// 이전 상태값을 유지 못함
console.log(increase()); //1
console.log(increase()); //1
console.log(increase()); //1
- 하지만, 문제는 이전 상태값을 기억을 못하고 함수 호출할 떄마다 num이 0으로 초기화되어서 계속 ++num, 즉 0에서 1증가한 1만 결과값으로 계속 출력된다.
<위 코드의 문제점을 보완하기 위해 고려해야할 부분!!>
- 이전 상태를 유지해야 함!
===> 클로저를 사용해보자!
// 카운트 상태 변경 함수 #3
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
return ++num;
};
})();
// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3
- 위 코드가 실행되면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환한 함수가 increase 변수에 할당된다.
- increase 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저다
- 즉시 실행 함수는 호출된 이후 소멸되지만, 즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출된다.
- 이때 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하고 있다. ==> 즉, num을 기억하고 있다(계속 참조 되니까)
- 따라서 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경할 수 있다.
- num은 초기화되지 않을 것이며, 외부에서 직접 접근할 수 없는 은닉된 private 변수이므로, 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없다.
===> 클로저는 상태(state)가 의도치 않게 변경되지 않도록 안전하게 은닉(information hiding) 하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용합니다.
// 카운트 상태 변경 함수 #4
// 클로저 카운트 기능 확장(값 감소 기능 추가)
const counter = (function () {
//카운트 상태 변수
let num = 0;
// 클로저인 메서드(increase, decrease)를 갖는 객체를 반환한다.
// property는 public -> 은닉되지 않는다.
return {
increase() {
return ++num;
},
decrease() {
return num > 0 ? --num : 0;
},
};
})();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
---> 카운트 함수 완성
캡슐화(encapsulation)와 정보 은닉
캡슐화란? 프로퍼티와 메서드를 하나로 묶는 것!
===> 주로 클래스를 통해 구현함
- 프로퍼티: 객체의 상태(state)
- 메서드: 프로퍼티를 참조하고 조작할 수 있는 동작(behavior)
객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용해요. 가지고 있는 정보가 예민하고 민감해서 정보 은닉(information hiding)이 필요한 경우겠죠!
- 객체의 상태 변경을 방지함으로써 정보 보호
- 객체 간의 의존성(결합도 - coupling)을 낮춤
java 등 기타 객체지향 언어에서 사용하는 public, private, protected → 접근을 제한할 수 있는 기능있지만, 자바스크립트는 제공 안함:
즉, 별도의 조치를 취하지 않으면 기본적으로 외부 공개가 된다는 의미!
// 생성자 함수
function Person(name, age) {
this.name = name; //public
let _age = age; //private
// 인스턴스 메서드
// 따라서, Person 객체가 생성될 때 마다 중복 생성됨
// : 해결방법 -> prototype
this.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
}
const me = new Person("Choi", 33);
me.sayHi(); // Hi!, My name is Choi. I am 33.
console.log(me.name); // Choi
console.log(me._age); // undefined
const you = new Person("Lee", 30);
you.sayHi(); // Hi! My name is Lee. I am 30.
console.log(you.name); // Lee
console.log(you.age); // undefined
- 클래스를 만들면, 캡슐화가 됨.
- _ 를 붙여서 private한 외부에서 접근 가능한 비공개 속성을 만들 수 있음
- 이 코드의 문제: sayHi는 인스턴스 메서드로, Person 객체가 생성될 때마다 새롭게 생성
===> 각 객체마다 중복 생성되기 때문에 메모리 낭비가 발생할 수 있음.
모든 인스턴스가 동일한 메서드 기능을 공유할 경우, 각 인스턴스에 메서드를 개별적으로 생성할 필요가 없다!
하지만 생성자 함수 내부에 메서드를 정의하면, 인스턴스가 생성될 때마다 해당 메서드가 중복 생성됨.
function Person(name, age) {
this.name = name; // public 속성
let _age = age; // private 속성
// 이 방식은 각 인스턴스가 개별적으로 `sayHi` 메서드를 생성하게 함
this.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
}
const me = new Person("Choi", 33);
const you = new Person("Lee", 30);
console.log(me.sayHi === you.sayHi); // false (메서드가 인스턴스마다 다름)
- 위 코드에서 me.sayHi와 you.sayHi는 서로 다른 함수. 각 인스턴스가 자신만의 sayHi 메서드를 가지므로, 메모리 낭비가 발생할 수 있음.
===> prototype 메서드로 공유
자바스크립트에서는 모든 함수가 생성될 때 기본적으로 prototype 객체가 생성됨. prototype을 통해 메서드를 정의하면, 해당 메서드를 모든 인스턴스가 공유할 수 있음.
즉, prototype에 정의된 메서드는 인스턴스에서 호출될 때마다 새로 생성되지 않고, 하나의 메서드를 참조하게 됨
function Person(name, age) {
this.name = name;
let _age = age;
this.getAge = function () {
return _age;
};
}
// `Person.prototype`에 `sayHi` 메서드를 정의하여 공유 메서드로 사용
Person.prototype.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${this.getAge()}.`);
};
const me = new Person("Choi", 33);
const you = new Person("Lee", 30);
console.log(me.sayHi === you.sayHi); // true (같은 메서드를 공유)
- Person.prototype.sayHi에 정의된 메서드는 모든 인스턴스가 공유하는 하나의 메서드. 이 방식으로 메모리 낭비를 줄일 수 있음.
이렇게 자바스크립트의 프로토타입 기반 상속 구조를 활용해도 좋음.
<자주 발생하는 실수>
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () { return i; };
}
for (var j = 0; j < funcs.length; j++) {
console.log(funcs[j]());
}
// for 문의 변수 선언문에서 var 키워드로 선언한 i 변수는 "블록 레벨 스코프"가 아닌
// "함수레벨 스코프"이다.
// expectation
// 0, 1, 2
// result
// 3, 3, 3
- var로 선언된 변수 i는 블록 레벨 스코프를 따르지 않고 함수 레벨 스코프를 따름. 따라서 for 루프가 돌 때마다 새로운 i가 생성되는 것이 아니라, 하나의 i를 계속 공유하게 됨.
- for 루프가 끝난 후, i의 값은 3이 됨. 이는 i가 마지막에 2 + 1로 증가하면서 3이 되었기 때문임.
- 각 함수 funcs[i]는 i의 최종 값인 3을 참조하게 됨.
- funcs 배열에는 다음과 같은 함수들이 저장됨:
- funcs[0] → function () { return i; }
- funcs[1] → function () { return i; }
- funcs[2] → function () { return i; }
-
- 그리고 루프가 끝날떄 i는 3이고, 같은 i를 참조해서 세 결과 모두 3이됨.
- funcs 배열에는 다음과 같은 함수들이 저장됨:
즉, funcs 배열 안의 각 함수는 모두 하나의 공유된 i를 참조하기 때문에, funcs[0](), funcs[1](), funcs[2]()를 호출할 때 모두 3을 반환하게 됨.
===> 클로저 이용
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = (function (id) {
return function () {
return id;
};
}(i));
}
for (var j = 0; j < funcs.length; j++) {
console.log(funcs[j]());
}
- for 루프 안에서 (function(id) { ... })(i) 형태로 즉시 실행 함수가 호출됨. 여기서 i 값을 id라는 매개변수로 전달하고, 함수 내부에서는 id를 캡처하게 됨.
- id 값은 클로저에 의해 함수 내부에 유지되며, return을 통해 새로운 함수를 반환하게 됨. 이 함수는 id 값을 참조하는 클로저가 됨.
- 루프가 끝나면 funcs 배열에는 0, 1, 2 값을 각각 반환하는 함수들이 저장됨
<var가 함수 스코프 범위어서 생긴 문제인데 사실 let을 사용하면 쉽게 해결가능>
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() { return i; };
}
for (let i = 0; i < funcs.length; i++) {
console.log(funcs[i]()); // 0 1 2
}
'프론트엔드 부트캠프' 카테고리의 다른 글
[React 1주차(2)] 자바스크립트 복습: template literals, destructuring, spread, rest, 화살표 함수, 조건 연산자 , 단축평가 (2) | 2024.10.29 |
---|---|
[React 1주차(1)] 자바스크립트 복습: 변수, 객체, 배열 (4) | 2024.10.28 |
[JS 문법 5주차(3)] 클래스 상속과 정적 메소드 (1) | 2024.10.26 |
[JS 문법 5주차(2)] Class -생성, getters & setters (1) | 2024.10.26 |
[JS 문법 5주차] DOM, DOM_API (3) | 2024.10.24 |