MDN에서는 클로저에 대해서 이렇게 설명하고 있다
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
….?
일단 lexical scoping을 이해해야 한다고 하니까 이해해보도록 하자
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // ?
bar(); // ?
위 코드가 실행되었을때, foo()와 bar()가 실행될 때 결과가 어떻게 나올지 예측할 수 있겠는가?
foo()가 실행되었을 때는 10, bar()가 실행되었을 때는 1이라고 생각할수도 있을 것이다.
하지만 결과부터 말해 주자면, 둘 다 결과는 1이 나오게 된다.
왜냐하면 자바스크립트가 렉시컬 스코핑을 따르기 때문이다. 렉시컬 스코핑은 정적 스코핑이라고도 하는데, 이는 함수를 어디서 선언했는지에 따라 상위 스코프가 결정되게 되는 것이고, 함수 bar의 스코프는 전역이 되기 때문에 상위 스코프는 전역이 되고 전역 변수 x의 값을 출력하게 된다.
그럼 렉시컬 스코핑이란 어디서 선언되었는지에 따라 그 상위 스코프가 결정되는 것인지 라고 짧게 설명할 수 있겠다. 그래서 이거랑 클로져랑 무슨 관련이 있나고?
function makeAdder(x) {
var y = 1;
return function(z) {
y = 100;
return x + y + z;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
//클로저에 x와 y의 환경이 저장됨
console.log(add5(2)); // 107 (x:5 + y:100 + z:2)
console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
//함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산
위의 코드는 예전의 글에서 한번 본 적이 있을 것이다(MDN의 클로져 관련 설명 글에서 퍼왔다링크)
위의 예제에서는 x와 y와 z를 더하는 함수가 makeAdder 안에 선언되어있고, makeAdder 함수를 2번 사용하여 add5와 add10이라는 함수를 생성하고 있다.
위의 렉시컬 스코핑 개념을 가져와서 이해를 하자면, makeAdder(5)가 반환하는 함수로 만든 add5(2)를 풀자면
x = 5
y = 1
function(z = 2){
y = 100;
return x + y + z;
}
가 된다. makeAdder 내부에 함수가 선언되어 있기 때문에 y의 상위 스코프가 y = 1이고 이 하위 스코프에서 y = 100으로 값을 다시 정의해 주기 때문에 결국 return 5 + 100 + 2가 되어서 107이 되게 된다.
가장 많이 본 활용법이라고 하면 클로져를 활용해서 class의 프라이빗 메소드를 흉내내는 방법이다. 자바스크립트는 태생적으로 이런 방법을 제공하지 않지만, 클로저를 이용하여 어디까지나 흉내내는 것이다. 이렇게 클로저를 사용하여 프라이빗 함수와 변수에 접근하는 퍼블릭 함수를 정의하는 방법을 모듈 패턴링크이라고 한다.
코드로 보자면,
let makeCounter = function() {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
let counter1 = makeCounter();
let counter2 = makeCounter();
alert(counter1.value()); /* 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* 2 */
counter1.decrement();
alert(counter1.value()); /* 1 */
alert(counter2.value()); /* 0 */
alert(counter1.privateCounter); /* undefined */
위의 예제를 봤을떄, 선언 이후 사용하는 방법이 객체지향 프로그래밍 언어에서 클래스를 사용하는 방법과 매우 유사하다.
또 다른 클로저 활용 방법으로 모듈화 패턴에서는 IIFE, 즉시 호출 함수 표현식와 함께 사용되는 패턴이 주로 사용된다.
module.exports = (function(){
const mysql = require("mysql2/promise");
require("dotenv").config();
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
connectionLimit: 5
});
async function getConnection(){
return await pool.getConnection(async conn => conn)
.catch((err)=>{
console.log(err);
return undefined;
});
}
return{
getConnection: async function(){
return await getConnection();
}
};
})();
위와 같은 모습이다. 해당 모듈을 외부 코드에서 불러 들였을때 내부의 pool과 같은 변수에는 접근이 불가능하다. 이러한 점을 통해서 모듈의 은닉화를 진행할 수 있고, 스코프가 겹치지 않기 때문에 같은 이름의 변수가 존재한다고 해도 외부 스코프와 충돌할 문제가 전혀 없다.
이와 같이 스코프에 대해서 간략하게 알아보았다. 스코프를 통해서 클래스를 구현하는 등의 것들은 타입스크립트로 넘어갈 때에 조금 더 쉽게 이러한 것들을 해결할 수 있다. 하지만 중요한 것은, private 이라는 키워드는 타입스크립트에서만 존재한다. 그렇기 때문에 트랜스코드를 트랜스파일 하여 자바스크립트로 만들어줄 때 위 예제와 같이 undefined로서 만들수는 없다. 이러한 차이점에 대해서 또한 짚고 넘어가야 할 듯 하다