개요
Closure 란, Lexical Scope 개념에서 스코핑 개념을 조금 더 확장시킨 것이다.
외부 함수가 값을 반환했음에도 Lexical Scope
에 의해 내부 함수에서 외부 함수 스코프에 존재하는 변수에 접근 가능하다는 것이 핵심이다.
클로저 예제 코드 1: 기본 개념
function outerFunction() {
const outerVariable = "Mozilla";
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const myFunc = outerFunction();
myFunc();
innerFunction()
에서는outerFunction()
스코프에 존재하는outerVariable
을 출력(console.log()
)하려 한다.Lexical Scope
에 의해 내부 함수에서 외부 함수의 변수 값에 접근하는 것은 아무런 문제가 없다.- 위 코드에서 눈여겨봐야 할 것은 외부 함수가 반환된 이후에도 외부 함수에 선언되었던 변수에 여전히 접근 가능하다는 것이다.
outerFunction()
은 소위 말하는Function Factory
형태로 구현된 함수이다.- 코드를 직접 실행시켜보면 결과를 알 수 있다.
클로저 예제 코드 2: 파라미터를 이용한 클로저
function makeAdder(x) {
return function (y) {
return x + y;
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
파라미터도 Function Scope
에 있는 변수처럼 접근 가능하기 때문에 변수 선언을 하지 않고 이를 이용해서도 클로저 코드를 만들 수 있다.
function makeSizer(size) {
return function () {
document.body.style.fontSize = `${size}px`;
};
}
const size12 = makeSizer(12);
const size14 = makeSizer(14);
const size16 = makeSizer(16);
/*
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
*/
콜백 함수의 함수 팩토리 형태로 만들어보았다.
클로저 예제 코드 3: 자바의 private
멤버 따라하기
const makeCounter = function () {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment() {
changeBy(1);
},
decrement() {
changeBy(-1);
},
value() {
return privateCounter;
},
};
};
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1.value()); // 0.
counter1.increment();
counter1.increment();
console.log(counter1.value()); // 2.
counter1.decrement();
console.log(counter1.value()); // 1.
console.log(counter2.value()); // 0.
- 위의
makeCounter()
함수는 3가지의 메서드를 가진 오브젝트를 반환하는데, 3개의 메서드 모두Lexical Scoping
에 의해privateCounter
변수 값에 접근이 가능하다. - 오직 반환된 오브젝트 내부에 있는 3개의 메서드에 의해서만 접근이 가능하다.
- 별 것 아닌 것 같은 일이어도 변수를
private
화 할 수 있다는 것은 설계상 매우 엄청난 일이다.- 누군가가
privateCounter
변수값을 직접 바꿀 수 있다는 위협 없이 안정적으로 코드를 작성해나갈 수 있다.
- 누군가가
여태까지의 예제 코드로 본 클로저의 장점 정리
- 사실 대부분 객체지향 프로그래밍에 관련된 장점이다.
- 데이터 은닉과 캡슐화가 용이해진다.
클로저 스코프 체인
클로저는 3개의 스코프가 있다.
- 로컬 스코프
- 인클로징(Enclosing) 스코프 (ex. 블록, 함수, 모듈 스코프 등)
- 글로벌 스코프
// 글로벌 스코프 (global scope)
const 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)); // log 20
익명 함수를 빼면 아래와 같다.
// global scope
const e = 10;
function sum(a) {
return function sum2(b) {
return function sum3(c) {
// outer functions scope
return function sum4(d) {
// local scope
return a + b + c + d + e;
};
};
};
}
const sum2 = sum(1);
const sum3 = sum2(2);
const sum4 = sum3(3);
const result = sum4(4);
console.log(result); //log 20
function outer() {
const x = 5;
if (Math.random() > 0.5) {
const y = 6;
return () => console.log(x, y);
}
}
outer()(); // logs 5 6
모듈 예제 코드
// myModule.js
let x = 5;
export const getX = () => x;
export const setX = (val) => {
x = val;
};
import { getX, setX } from "./myModule.js";
console.log(getX()); // 5
setX(6);
console.log(getX()); // 6
이렇게 모듈을 이용해도, 같은 영역을 공유하기 때문에 모듈 내부에 있는 전역 변수의 레퍼런스를 유지할 수 있다.
// myModule.js
export let x = 1;
export const setX = (val) => {
x = val;
};
// closureCreator.js
import { x } from "./myModule.js";
export const getX = () => x; // Close over an imported live binding
import { getX } from "./closureCreator.js";
import { setX } from "./myModule.js";
console.log(getX()); // 1
setX(2);
console.log(getX()); // 2
이렇게 Getter 와 Setter 가 다른 모듈 안에 있어도 가능하다.
클로저로 인해 벌어질 수 있는 실수
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email" /></p>
<p>Name: <input type="text" id="name" name="name" /></p>
<p>Age: <input type="text" id="age" name="age" /></p>
function showHelp(help) {
document.getElementById("help").textContent = help;
}
function setupHelp() {
var helpText = [
{ id: "email", help: "Your e-mail address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function () {
showHelp(item.help);
};
}
}
setupHelp();
JSFiddle 에서 실행해보기
위 코드만 보고 힌트없이 코드에서 발생하는 문제점을 찾는다면,
var
키워드를 이용해 코딩한 경험이 많은 사람일 것이다.
위 코드에서의 핵심적인 문제를 짚자면, onfocus
에 할당된 익명 함수에 들어있는 item.help
의 평가 시점이 콜백함수를 할당할 때가 아니라 실제로 포커스가 들어갔을 때이기 때문이다.
풀어서 설명하자면, onfocus
에 할당한 모든 콜백 메서드는 (원치 않았겠지만) 클로저(closure)이다. var
로 선언된 helpText
는 함수가 끝난 뒤에도 콜백함수 내부에서 참조하고 있기 때문에 스코프가 계속 기억된다.
그래서 콜백함수가 모두 할당된 이후 i
의 값이 증가하면서 다음 루프로 넘어갈 때 showHelp(item.help)
의 item.help
는 스코프가 끝나지 않으므로 계속 같은 메모리 값을 참조하게 된다.
function setupHelp() {
var helpText = [
{ id: "email", help: "Your e-mail address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function () {
showHelp(item.help);
};
}
console.log("item", item);
}
스코프가 지워지지 않으므로, 위의 코드에서의 console.log(item)
도 정상적으로 마지막으로 사용했던 item
을 출력하게 된다.
결국 item.help
는 모두 같은 메모리 주소를 참조하게 되므로 의도한대로 동작하지 않게 되는 것이다. 해결 방법은 그냥 item
을 선언할 때 const item
으로 선언하는 것 하나만으로 가능하다.
혹은 다른 스코프를 만들어 즉시 평가되도록 하여도 된다.
// ...
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = ((txt) =>
function () {
showHelp(txt);
})(item.help);
}
// ...
이렇게 코드를 작성하면, item.help
가 아닌 txt
라는 파라미터의 스코프를 따르기 때문에 파라미터의 스코프는 함수가 끝나면서 만료되어 기존의 item
처럼 계속 남아있지 않는다.
더 간단히 수정하면 아래와 같이 코드를 바꾸어도 스코프가 나뉜다.
function showHelp(help) {
document.getElementById("help").innerHTML = help;
}
function setupHelp() {
var helpText = [
{ id: "email", help: "Your e-mail address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
setCallback(item.id, item.help);
}
}
function setCallback(id, txt) {
document.getElementById(id).onfocus = function () {
showHelp(txt);
};
}
결국 이 모든게 lexical scope & closure 라는 개념 때문에 생긴 버그이다.
성능을 위해 고려해봄직한 것
위에서도 잘 나와있다시피 함수는 자신의 스코프와 클로저까지 관리하기 때문에 이러한 클로저를 불필요하게 늘리는 것은 메모리 낭비이자 속도 저하의 원인이 될 수 있다.
새로운 오브젝트나 클래스를 만들 때는 생성자보다는 prototype
을 고려하자.
생성자에 메서드를 두게 되면 오브젝트나 클래스를 생성할 때마다 메서드가 재할당되어버린다.
나쁜 예제
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function () {
return this.name;
};
this.getMessage = function () {
return this.message;
};
}
클로저 때문에 매 클래스 생성 시에 함수에서 name
과 message
의 스코프를 저장한다. 클로저를 사용하지 않는 방향으로 개선하는 것이 좋다.
개선된 예제
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName() {
return this.name;
},
getMessage() {
return this.message;
},
};
더이상 각 오브젝트/클래스의 name
과 message
스코프를 기억하진 않지만, 프로토 타입을 재정의하는 것은 권장되지 않는다.
좋은 예제
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function () {
return this.name;
};
MyObject.prototype.getMessage = function () {
return this.message;
};
- 모든
MyObject
에서 동일하게 사용된다. - 오브젝트 생성 시마다 스코프를 기억하지 않는다.
- 프로토타입을 재정의하지 않는다.
레퍼런스
'자바스크립트 > 개념' 카테고리의 다른 글
자바스크립트 호이스팅 매우 간단히 정리 (0) | 2022.07.30 |
---|---|
자바스크립트 DOMContentLoaded vs load (onload) 의 차이 (0) | 2022.07.24 |
자바스크립트 var , let , const 의 스코프 차이에 대해 알아보자. (0) | 2022.07.13 |
자바스크립트의 Proxy 란? (0) | 2022.06.30 |
자바스크립트 fetch API 알아보기 (feat. ajax) (0) | 2022.06.26 |