![[코어자바스크립트] 05. 클로저](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlhC3l%2FbtsL5MVArBz%2FxqLkdfEbiPKkUaCQdSNhbK%2Fimg.jpg)
1. 클로저의 의미 및 원리 이해
클로저란? "어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상"을 의미한다.
var outer = function () {
var a = 1;
var inner = function () {
console.log(++a);
};
inner();
};
outer();
위 코드는 outer 함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들(a, 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
그러나 inner 함수 자체를 반환하게 된다면!!!
outer 함수의 실행 컨텍스트가 종료될 때, outer2 변수는 outer의 실행 결과인 inner 함수를 참조하게 된다. 그래서 outer2를 호출하면 반환된 함수인 inner가 실행되게 된다. inner 함수가 선언된 위치의 LexicalEnvironment가 참조복사 되기 때문에, outer 함수의 LexicalEnvironment가 담기게 된다. 그래서 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근할 수 있게 된다.
LexicalEnvironment 접근 가능한 이유?!
종료된 outer 함수의 LexicalEnvironment를 접근할 수 있는 이유는 바로 가비지 컬렉터 동작 방식 때문이다. 어떤 값을 참조하는 변수가 하나라도 있기 때문에 가비지 컬렉터의 수집 대상에 포함되지 않는다.
그래서 outer 함수가 종료되더라도 inner 함수에서 outerEnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 하기 때문에 가비지 컬렉터 수집 대상에서 제외되어 inner 함수가 outer 함수에 있는 변수에 접근할 수 있게 되는 것이다.
콜스택 흐름
클로저란?
어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상이다.
즉, 함수 A의 실행 컨텍스트가 종료되어도 LexicalEnvironment가 가비지 컬렉팅이 되지 않아 변수에 접근할 수 있게 된다.
2. 클로저와 메모리 관리
개발자 의도와 달리 참조 카운트가 0이 되지 않아 가비지 컬렉터의 수거 대상이 되지 않으면 메모리 누수가 발생할 수 있다.
그래서 참조 카운트를 0으로 만들기 위해서는 식별자에 보통 null이나 undefined를 할당하여 메모리 해제를 할 수 있게 된다.
var outer = (function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음
3. 클로저 활용 사례
3-1. 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때
콜백 함수 중 하나인 이벤트 리스너에 관한 예시이다.
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul'); // (공통 코드)
fruits.forEach(function(fruit) { // (A)
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function() { // (B)
alert('your choice is ' + fruit);
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
forEach 메서드에 넘겨준 콜백 함수(A)에서 addEventListener에 넘겨준 콜백 함수(B)에서는 fruit이라는 외부 변수를 참조하고 있기 때문에 클로저가 있다. 클릭 이벤트에 의해 각 컨텍스트의 (B)가 실행될 때는 (B)의 outerEnvironmentReference가 (A)의 LexicalEnvironment를 참조하게 된다. 따라서 (B) 함수가 참조할 예정인 변수 fruit에 대해서는 (A)가 종료된 후에도 가비지 컬렉터 수거 대상에 제외되어 계속 참조 가능하게 될 것이다.
만약 (B) 함수를 외부로 분리하게 될 경우,
마지막 줄에서는 정상적으로 출력이 되지만, 각 li를 클릭하면 [object PointerEvent]라는 값이 출력된다. 이는 addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 이벤트 객체를 주입하기 때문에 bind 메서드를 활용해 해결할 수 있다.
값이 제대로 출력은 되지만 함수 내부에서의 this가 원래 값과 달라지게 된다. 즉, addEventListener에서 this는 이벤트를 발생시킨 요소를 가리키게 되지만 bind를 사용하게 되면 this 값이 null로 바뀌게 된다. (브라우저에서 실행하면 전역 객체가 출력됨.)
그래서 이러한 이슈를 해결하기 위해서 bind가 아닌 고차함수를 활용한다.
고차함수를 활용하여 함수 내부에서 다시 익명함수를 반환한다. 그래서 alertfruitBuilder 함수를 실행하면서 fruit 값을 인자로 전달하게 되면, 함수의 실행 결과가 다시 함수가 되어 반환된 함수를 이벤트 리스너에 콜백 함수로써 전달하게 된다.
즉, fruit 값을 클로저로 기억할 수 있게 된다.
3-2. 접근 권한 제어(정보 은닉)
정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 프로그래밍 언어의 중요한 개념 중 하나이다.
그러나 자바스크립트는 기본적으로 변수 자체에 접근 권한을 직접 부여하도록 설계돼 있지 않다. 그래서 이를 클로저를 이용하여 함수 차원에서 public 한 값과 private 한 값을 구분할 수 있다.
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());
위 코드에서 outer 함수는 외부로부터 격리된 닫힌 공간이다. 외부 공간에서 노출돼 있는 outer 변수를 통해 outer 함수를 실행할 수 있지만, 함수 내부에는 어떠한 개입을 할 수 없다. 즉, 외부에서는 오직 outer 함수가 return 한 정보에만 접근할 수 있게 되어, return 값이 외부에 정보를 제공하는 유일한 수단이 된다는 것이다.
그래서 이를 활용하여 외부에 제공하고자 하는 정보들을 모아서 return 하고, 내부에서만 사용할 정보들을 return 하지 않은 것으로 접근 권한 제어가 가능하게 된다.
접근 권한 제어 예시 - 자동차 경주 게임
자동차 경주 게임을 아래 코드처럼 객체로 만들었다.
var car = {
fuel: Math.ceil(Math.random() * 10 + 10), // 연료(L)
power: Math.ceil(Math.random() * 3 + 2), // 연비(km/L)
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)');
},
};
이처럼 자동차 경주 게임을 car 변수에 객체를 할당하는 방식으로 코드를 작성하면..
이런 식으로 마음껏 값을 바꿔버릴 수 있게 되어 의도치 않은 동작을 하게 된다. 그래서 해당 값을 바꾸지 못하게 클로저를 활용해야 한다. 즉, 객체가 아닌 함수로 만들고 필요한 것만 return 하는 것이다.
var createCar = function() {
var fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L)
var power = Math.ceil(Math.random() * 3 + 2); // 연비(km / L)
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();
createCar라는 함수를 실행함으로써 객체를 생성했다. 그러면 fuel, power 변수는 외부에서의 접근을 제한했고, moved 변수는 getter만을 부여해서 읽기 전용 속성을 부여했다. 이제 외부에서는 오직 run 메서드를 실행하거나 moved 값을 확인하는 두 가지 동작만 할 수 있게 된다.
그러나...! run 메서드를 다른 내용으로 덮어씌우는 어뷰징이 가능하기 때문에 객체를 return 하기 전에 미리 변경할 수 없게끔 해야 한다. 바로 아래 코드처럼 freeze를 사용하면 된다.
var createCar = function() {
// 생략
var publicMembers = {
// 생략
},
};
Object.freeze(publicMembers);
return publicMembers;
};
var car = createCar();
그러면 아래 결과처럼 run 메서드가 바뀌지 않게 된다.
3-3. 부분 적용 함수
부분 적용 함수란? n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m) 개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다.
이를 bind 메서드를 사용해서 구현할 수 있지만, this의 값을 변경할 수밖에 없기 때문에 메서드에서는 사용할 수 없을 것 같다. 그래서 this에 관여하지 않는 별도의 부분 적용 함수를 구현하는 것이 범용성 측면에서 더 좋을 것이다.
부분 적용 함수 구현
var partial = function() {
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if (typeof func !== 'function') {
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function() {
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
var restArgs = Array.prototype.slice.call(arguments);
return func.apply(this, partialArgs.concat(restArgs));
};
};
var add = function() {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = partial(add, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); // 55
var dog = {
name: '강아지',
greet: partial(function(prefix, suffix) {
return prefix + this.name + suffix;
}, '왈왈, '),
};
dog.greet('입니다!'); // 왈왈, 강아지입니다.
첫 번째 인자에는 원본 함수를, 두 번째 인자 이후부터는 미리 적용할 인자들을 전달한다. 반환할 함수(부분 적용 함수)에서는 다시 나머지 인자들을 받아 이들을 한데 모아(concat) 원본 함수를 호출(apply)한다. 그러면 실행 시점의 this를 그대로 반영함으로써 this에는 아무런 영향을 주지 않게 된다.
부분 적용 함수 실무 예시 - 디바운스
실무에서는 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우, 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 디바운스라는 것을 사용할 수 있다.
3-4. 커링 함수
커링 함수란? 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것이다.
부분 적용 함수와 다른 점은 한 번에 하나의 인자만 전달한다는 점에서 다르다. 또한 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 할 뿐으로, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다.
var curry3 = function(func) {
return function(a) {
return function(b) {
return func(a, b);
};
};
};
var getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith10(8)); // 10
console.log(getMaxWith10(25)); // 25
var getMinWith10 = curry3(Math.min)(10);
console.log(getMinWith10(8)); // 8
console.log(getMinWith10(25)); // 10
그러나 인자가 많아질수록 가독성이 떨어진다는 단점이 있다. 그래서 ES6에서는 화살표 함수를 사용해서 한 줄로 표기할 수 있다.
var curry5 = func => a => b => c => d => e => func(a, b, c, d, e)
커링 함수 - 자연 실행
이러한 커링 함수를 원하는 시점까지 지연시켰다가 실행하는 지연실행에 적합하다.
커링 함수 - 사용 예시
최근에는 여러 프레임워크나 라이브러리 등에서 커링을 사용하고 있으며, 그 예시로는 Flux 아키텍처의 구현체 중 하나인 Redux의 미들웨어이다.
4. 정리
클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달하는 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다
+) useState
이 책에서 나오진 않았지만 클로저를 활용한 예시 중에 유명한(?) 것이 바로 React의 useState 훅이다.
useState 동작 원리
const [value, setValue] = useState(0);
useState가 반환하는 배열인 value와 setValue를 통해 각각 값에 접근하고 변경할 수 있다. 특히 useState를 통해 내부적으로 상태 값을 기억한다. 즉, 렌더링 되어도 값이 초기화되지 않고 상태 값을 유지할 수 있다.
useState가 클로저를 사용해서 동작하는 방식
useState가 클로저를 사용하여 함수(useState) 내부에 선언된 함수(setState)가 함수의 실행이 종료된 이후에도(useState가 호출된 이후에도) 지역변수인 state를 계속 참조할 수 있게 된다. 즉, 함수의 실행이 끝났음에도 함수가 선언된 환경을 기억할 수 있는 방법인 클로저를 사용한 것이다.
그래서 클로저를 사용함으로써 외부에 해당 값을 노출시키지 않고(데이터 은닉) 값을 가져오고 변경할 수 있는 것이다.
(모던 리액트 딥다이브 참고)
'💜 프론트엔드 > JavaScript' 카테고리의 다른 글
[코어자바스크립트] 04. 콜백 함수 (0) | 2025.01.15 |
---|---|
[코어자바스크립트] 03. this (0) | 2025.01.15 |
[코어자바스크립트] 02. 실행 컨텍스트 (1) | 2025.01.11 |
[코어자바스크립트] 01. 데이터 타입 (1) | 2024.12.23 |
[JavaScript] 이벤트 버블링, 캡쳐링, 위임 & HTTP 메소드 (0) | 2024.04.07 |
FE 개발자가 되고 싶은 짱잼이
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!