이미지 출처: calibraint blog
클로저의 원리를 이해하기 위해 다양한 예시와 개념을 함께 설명한다. 이 포스트에서는 렉시컬 환경, 클로저의 기본 개념, 클로저가 사용되는 다양한 사례, 그리고 클로저가 코드에 미치는 영향에 대해 다룬다.
렉시컬 환경(어휘적 환경) 은 코드가 작성된 시점에서의 변수와 함수 선언을 포함하는 환경을 말한다. 자바스크립트 엔진은 코드를 실행할 때 어휘적 환경을 참조하여 변수와 함수의 유효 범위를 결정한다.
렉시컬 환경 은 실행 컨텍스트의 한 부분으로, 두 가지 구성 요소를 가진다.
렉시컬 환경은 코드가 작성된 위치를 기준으로 변수와 함수의 유효 범위가 정해진다는 것을 의미한다. 이를 렉시컬 스코핑(Lexical Scoping) 이라고 한다.
환경 레코드는 현재 스코프에서 선언된 변수와 함수의 정보를 저장한다. 예를 들어, 함수 내부에서 변수를 선언하면 해당 변수는 환경 레코드에 저장된다.
function foo() {
let a = 10;
function bar() {
let b = 20;
console.log(a + b); // 30
}
bar();
}
foo();
위 코드에서 foo
함수의 환경 레코드에는 a
변수가 저장되고, bar
함수의 환경 레코드에는 b
변수가 저장된다.
외부 환경 참조는 현재 스코프의 상위 스코프를 가리킨다. 이를 통해 자바스크립트는 현재 스코프에서 변수를 찾지 못하면 상위 스코프로 이동하여 변수를 찾는다.
function outer() {
let x = 10;
function inner() {
console.log(x); // 10
}
inner();
}
outer();
위 코드에서 inner
함수는 외부 환경 참조를 통해 outer
함수의 x
변수에 접근할 수 있다.
렉시컬 환경은 함수가 선언될 때 결정되며, 함수가 호출될 때마다 새로운 실행 컨텍스트가 생성되어 렉시컬 환경이 설정된다. 함수 호출 시 생성되는 렉시컬 환경은 해당 함수의 환경 레코드와 외부 환경 참조를 포함한다.
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1
위 코드에서 makeCounter
함수는 호출될 때마다 새로운 렉시컬 환경을 생성하며, 각각의 counter1
과 counter2
는 독립적인 렉시컬 환경을 가진다. 따라서 counter1
과 counter2
는 서로 다른 count
값을 유지한다.
자바스크립트에서 클로저(Closure) 는 함수와 함수가 선언된 렉시컬 환경(Lexical Environment) 과의 조합을 의미한다. 클로저는 다음과 같은 특성을 가진다.
자바스크립트에서 코드가 실행될 때, 실행 컨텍스트가 생성된다. 실행 컨텍스트는 다음과 같은 구성 요소를 가진다.
function outer() {
let outerVar = 'I am outside!';
function inner() {
console.log(outerVar); // 'I am outside!'
}
return inner;
}
const innerFunc = outer();
innerFunc(); // 'I am outside!'
위 코드에서 inner
함수는 outer
함수의 렉시컬 환경을 기억하며, 이를 통해 outerVar
에 접근할 수 있다. 이처럼 함수가 자신의 스코프 외부에 있는 변수에 접근할 수 있는 것을 클로저라고 한다.
클로저는 다양한 상황에서 유용하게 사용될 수 있다. 아래에서는 클로저의 핵심적인 사용 사례와 이점을 설명한다.
클로저는 데이터 은닉화를 위해 자주 사용된다. 외부에서 접근할 수 없는 변수를 생성하고, 특정 함수만이 그 변수에 접근할 수 있도록 한다. 이를 통해 코드의 안정성과 보안성을 높일 수 있다.
function createCounter() {
let count = 0;
return {
increment() {
count++;
console.log(count);
},
decrement() {
count--;
console.log(count);
}
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
위 코드에서 count
변수는 createCounter
함수 내부에서 선언되었기 때문에 외부에서 직접 접근할 수 없다. 대신 increment
와 decrement
메서드를 통해서만 count
를 조작할 수 있다. 이를 통해 데이터 은닉화가 가능해진다.
클로저는 반복문에서 자주 사용되는 패턴이다. 반복문에서 클로저를 사용하면 반복문 내에서 생성된 변수의 상태를 기억할 수 있다.
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 1, 2, 3
위 코드에서 let
을 사용하면 블록 스코프를 가지므로, 각 반복마다 새로운 i
변수가 생성되어 클로저가 각각의 i
값을 기억하게 된다. 따라서 1초 후에 출력되는 값은 1, 2, 3이 된다.
클로저를 사용하면 함수 호출 간의 상태를 유지할 수 있다. 이는 상태가 필요한 함수형 프로그래밍에서 매우 유용하다. 클로저를 통해 함수가 호출될 때마다 상태를 유지할 수 있다.
function createAdder(x) {
return function(y) {
return x + y;
};
}
const add5 = createAdder(5);
console.log(add5(2)); // 7
console.log(add5(10)); // 15
위 코드에서 createAdder
함수는 x
값을 기억하는 클로저를 반환하며, 이를 통해 함수 호출 간의 상태를 유지할 수 있다.
클로저를 사용하면 모듈 패턴을 구현할 수 있다. 이는 자바스크립트 코드의 모듈화를 통해 코드의 가독성과 유지보수성을 높일 수 있다. 모듈 패턴을 사용하면 코드를 분리하고 독립적으로 관리할 수 있다.
const CounterModule = (function() {
let count = 0;
return {
increment() {
count++;
console.log(count);
},
decrement() {
count--;
console.log(count);
}
};
})();
CounterModule.increment(); // 1
CounterModule.increment(); // 2
CounterModule.decrement(); // 1
위 코드에서 CounterModule
은 즉시 실행 함수 표현식(IIFE)을 사용하여 모듈을 생성하고, 클로저를 통해 count
변수를 은닉하여 모듈 패턴을 구현한다.
즉시 실행 함수 표현식(IIFE: Immediately Invoked Function Expression) 은 정의되자마자 즉시 실행되는 함수 표현식을 의미한다. IIFE는 함수를 정의함과 동시에 호출하기 위해 사용된다. IIFE를 사용하면 코드의 변수와 함수가 외부 스코프에 영향을 주지 않고 독립된 유효 범위를 가지게 할 수 있다.
즉시 실행 함수 표현식의 기본 형태
(function() {
// 내부 로직
})();
또는
(function() {
// 내부 로직
}());
클로저를 사용할 때는 성능 저하 등의 문제가 발생할 수 있다. 대표적인 예는 아래와 같다.
클로저는 참조를 유지하는 변수들이 계속해서 메모리에 남아있기 때문에, 잘못 사용하면 메모리 누수가 발생할 수 있다. 이는 특히 장시간 실행되는 애플리케이션에서 문제를 일으킬 수 있다.
function createFunctionArray() {
let arr = [];
for (let i = 0; i < 1000000; i++) {
arr.push(function() {
console.log(i);
});
}
return arr;
}
const functionArray = createFunctionArray();
위 코드에서 생성된 함수 배열은 많은 메모리를 차지할 수 있으며, 클로저가 변수에 대한 참조를 유지하기 때문에 메모리 누수가 발생할 수 있다.
클로저와 함께 사용하는 고급 패턴은 여러 가지가 있다. 이번에는 해당 부분을 자세히 다루지는 않지만, 다음과 같은 고급 패턴이 있다는 점을 짚고 넘어간다.
클로저는 자바스크립트의 강력한 기능 중 하나로, 함수와 함수가 선언된 렉시컬 환경의 조합을 의미한다. 이를 이해하기 위해서는 함수가 선언된 스코프와 해당 스코프에서 선언된 변수에 접근할 수 있는 메커니즘을 이해하는 것이 중요하다.
렉시컬 환경과 실행 컨텍스트의 개념을 깊이 이해하면 클로저의 동작 방식을 더 명확히 파악할 수 있다. 이는 자바스크립트의 핵심 개념 중 하나이며, 이를 이해하면 더 나은 코드 작성과 디버깅에 큰 도움이 된다.