오늘은 5단원인 "클로저" 를 포스팅 할 것이다.
클로저(closure)는 자바스크립트에서 중요한 개념 중 하나지만,
사실 이번에 처음 제대로 파본다...
실행 컨텍스트에 대한 사전지식이 없으면 분명 이해가 안 갈 것이다.
마침 2단원에서 실행컨텍스트를 다뤘으니 꼭! 꼭! 보고 오길 권장한다.
https://ilikezzi.tistory.com/55
05. 클로저
클로저의 의미 및 원리 이해
클로저는 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다.
클로저의 설명은 다양한 문헌에서 제각각 다르게 정의 또는 설명하고 있지만,
처음 문장을 접하면 이해하기 어려울 수 있다.
클로저는 내부 함수가 외부 함수의 변수에 접근할 수 있게 해주는 자바스크립트의 특별한 기능
다양한 설명 중에 이 설명이 그나마 처음 접했을 때 제일 와닿았다.
MDN(Mozilla Developer Network)에서는 클로저에 대해 이렇게 설명했다.
A Closure is the combination of a function and the lexical environment within which that function was declared.
직역해보면 다음과 같다.
"클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상"
여기서 lexical environment는 실행 컨텍스트의 구성요소 중 하나인 outerEnvironmentReference에 해당한다.
그리고 상호관계란 내부 함수에서 외부 변수를 참조하는 때에만 만을 뜻한다.
지금까지 파악한 내용에 따르면 클로저란
"어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상" 이라고 볼 수 있겠다.
이제 외부함수의 변수를 참조하는 내부함수 예제 코드를 보자.
var outer = function(){
var a = 1;
var inner = function(){
return ++a;
};
return inner();
};
var outer2 = outer();
console.log(outer2); // 2
6번째 라인을 보면 함수를 실행한 결과를 return 하고있으므로,
outer함수의 실행 컨텐스트가 종료된 시점에는 a 변수를 참조하는 대상이 없어진다.
a, inner변수 값들은 가비지 컬렉터에 의해 소멸될 것이다.
이제 outer의 실행 컨텍스트가 종료된 후에도 inner 함수를 호출하는 예제 코드를 보자.
var outer = function(){
var a = 1;
var inner = function(){
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
이번에는 6번째 라인에서 함수의 실행결과가 아닌 inner 함수 자체를 return 했다.
그러면 outer 함수의 실행 컨텍스트가 종료될 때,
outer2 변수는 outer의 실행 결과인 inner 함수를 참조하게 될 것이다.
여기서 의문이 들 것이다.
inner 함수의 실행 시점에는 outer 함수는 이미 실행이 종료된 상태인데,
outer함수의 LexicalEnvironment에 어떻게 접근하는 걸까?
정답은 가비지 컬렉터의 동작 방식 때문이다.
가비지 컬렉터는 어떤값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.
그 덕에 inner 함수가 이 변수에 접근할 수 있었던 것이다.
이제 다시 클로저의 정의에 대해서 살펴보면
"클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달 할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상"
이라고 정의할 수 있을 것이다.
메모리 관리
클로저를 사용할 때 주의해야할 점이 있다.
클로저는 메모리를 차지하기 때문에 과도하게 사용하면 성능 문제가 생길 수 있다.
흔히 "메모리 누수" 라는 표현을 쓰기도 하는데
어떤 값의 참조 카운트가 0이 되지 않아 가비지 컬렉터의 수거 대상이 되지 않는 경우를 얘기하는 것이다.
하지만 관리방법은 정말 간단하다.
클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다.
그렇다면 그 필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 해주면 된다.
참조 카운트를 0 으로 만들어주면 언젠간 가비지 컬렉터가 수거해갈 것이다.
보통 null이나 undefined를 할당하면 해결된다.
클로저 메모리 관리 예제 코드로 살펴보자.
var outer = function(){
var a = 1;
var inner = function(){
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());
outer = null; // outer 식별자의 inner 함수 참조를 끊음
마지막 라인을 보면 이렇게 null을 할당하면 끝이다.
클로저 활용
정보은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서,
모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념이다.
쉽게 말해 외부에서는 오직 return 한 정보에만 접근할 수 있다.
return 값이 외부에 정보를 제공하는 유일한 수단인 것이다.
외부에 제공하고자 하는 정보들을 모아서 return하고,
내부에서만 사용할 정보들은 return하지 않는 것으로 접근 권한 제어가 가능하다.
따라서 return한 변수들은 공개 멤버(public member)가 되고,
그렇지 않은 변수들은 비공개 멤버(private member)가 되는 것이다.
간단한 게임으로 해당 예제를 보자.
var car = {
fuel: Math.ceil(Math.random() * 10 + 10), // 연료
power: Math.ceil(Math.random() * 3 + 2), // 연비
moved:0, // 총 이동거리
run: function(){
var km = Math.ceil(Math.random() * 6);
var wasteFuel = km / this.power;
if(this.fuel < wasteFuel){
console.log('이동불가');
return;
}
this.fuel -= wasteFuel;
this.moved += km;
console.log(km + 'km 이동 (총 ' + this.moved + 'km)');
}
}
// 각 턴마다 주사위를 굴려 나온 숫자(km)만큼 이동한다.
// 차량별로 연료량과 연비는 랜덤으로 생성된다.
// 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동하지 못한다.
// 모든 유저가 이동할 수 없는 턴에 게임이 종료된다.
// 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리한다.
이렇게 간단한 게임을 만들어 보았다.
모든 유저가 run 메서드만 호출하면 해당 게임의 코드는 아무런 문제가 없다.
하지만 일부 유저가 JS를 다룰줄 안다면 조작을 해버릴 수 있다.
car.fuel = 99999999;
car.power = 9999999;
car.moved = 9999999;
이런식으로 해버리면 치트키가 따로 없다.
이런 케이스에서 값을 바꾸지 못하도록 방어할 때 클로저를 활용하는 것이다.
객체가 아닌 함수로 만들고, 필요한 멤버만을 return 하면 된다.
var createCar = function() {
var fuel = Math.ceil(Math.random() * 10 + 10); // 연료
var power = Math.ceil(Math.random() * 3 + 2); // 연비
var moved = 0;
return {
get moved() {
return moved;
},
run: function() {
var km = Math.ceil(Math.random() * 6);
var wasteFuel = km / power;
if (fuel < wasteFuel) {
console.log('이동불가');
return;
}
fuel -= wasteFuel;
moved += km;
console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel);
}
};
};
var car = createCar();
fuel, power 변수는 비공개 멤버로 지정해 외부에서 접근을 제한하고,
moved 변수는 getter만을 부여함으로써 읽기 전용 속성을 부여했다.
이제 외부에서는 run 메서드 실행과, 현재 moved 값을 확인하는 두가지 동작만 할 수 있다.
커링 함수
다음으로 이제 커링 함수에 대해 살펴보자.
커링 함수란 여러개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서
순차적으로 호출될 수 있게 체인형태로 구성한 것을 말한다.
함수를 실행한 결과는 그 다음 인자를 받기 위해 대기만 할 뿐,
마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다.
이것도 관련 코드를 먼저 보자.
var curry5 = function(func){
return function(a){
return function(b){
return function(c){
return function(d){
return function(e){
return func(a, b, c, d, e);
};
};
};
};
};
};
var getMax = curry5(Math.max);
console.log(getMax(1)(2)(3)(4)(5));
결과적으로 이 코드는 5개의 숫자 중에서 가장 큰 숫자를 한번에 찾아줄 것이다.
하지만 한눈에 봐도 코드가 길고 가독성이 떨어진다.
ES6에서 제공하는 화살표 함수를 써서 한줄에 표기할 수 있다.
const curry5 = func => a => b => c => d => e => func(a, b, c, d, e);
이렇게 화살표 함수로 변환하면 한줄에 표기할 수 있다.
각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 가비지 컬렉터되지 않고,
메모리에 차곡차곡 쌓였다가, 마지막 호출로 실행 컨텍스트가 종료된 후에야
한꺼번에 가비지 컬렉터의 수거 대상이 된다.
이런식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 셈이된다.
이 커링 함수가 유용한 경우는 바로 지연실행이다.
원하는 시점까지 지연시켰다가 실행하는 것이 필요할때 커링을 쓰기 적합하다.
프로젝트 내에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고 일부만 바뀌는 경우에도 적합할 것이다.
이렇게 클로저에 대해서 제대로 알아보았다.
클로저는 확실히 코드를 보고나서 다시 설명을 보니 이해가 잘 가는것 같다.
그리고 클로저 메모리는 항상 주의를 해야겠다.
다음 챕터는 "프로토타입" 이다.
'Programming Language > Javascript' 카테고리의 다른 글
[Javascript] 비동기의 핵심, 이벤트 루프(Event loop) 파헤치기 (0) | 2023.12.25 |
---|---|
[Javascript] 콜백 함수_코어 자바스크립트 (0) | 2023.09.04 |
[Javascript] this_코어 자바스크립트 (0) | 2023.08.22 |
[Javascript] console.dir()로 객체의 모든 depth 구조 출력 (0) | 2023.08.15 |
[Javascript] new Set() 배열에서 쉽게 중복값 제거하기 (0) | 2023.08.14 |