1. 🎬 클로저의 개념 이해하기
클로저를 설명하기 전에, 먼저 '함수'와 '스코프'에 대한 기본 개념을 살펴보는 것이 좋겠습니다.
함수란 무엇인가?
함수는 특정 작업을 수행하기 위해 설계된 코드 블록입니다. 쉽게 말해, 함수는 입력을 받아 처리한 후 결과를 반환하는 기계와 같습니다.
function add(a, b) {
return a + b;
}
console.log(add(5, 3)); // 출력: 8
스코프란 무엇인가?
스코프(Scope)는 변수의 유효 범위를 말합니다. 자바스크립트에서는 크게 전역 스코프와 함수 스코프로 나뉩니다.
let globalVar = "전역 변수"; // 전역 스코프
function exampleFunction() {
let localVar = "지역 변수"; // 함수 스코프
console.log(globalVar); // 전역 변수 접근 가능
console.log(localVar); // 지역 변수 접근 가능
}
exampleFunction();
console.log(globalVar); // 전역 변수 접근 가능
// console.log(localVar); // 오류! 함수 외부에서 지역 변수 접근 불가
클로저의 정의
이제 클로저에 대해 알아볼까요? 클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합입니다. 쉽게 말해, 클로저는 함수가 자신이 생성된 환경(스코프)를 기억하고 접근할 수 있는 메커니즘입니다.
간단한 예를 살펴봅시다:
function makeCounter() {
let count = 0; // 외부 함수의 지역 변수
return function() {
return count++; // 내부 함수가 외부 함수의 변수에 접근
};
}
const counter = makeCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2
여기서 makeCounter 함수는 내부 함수를 반환합니다. 이 내부 함수는 makeCounter 함수의 지역 변수 count에 접근하고 있습니다. 일반적으로 함수가 종료되면 그 함수의 지역 변수는 사라지지만, 클로저 덕분에 내부 함수는 count 변수를 계속 기억하고 접근할 수 있습니다.
2. 💻 클로저와 콜 스택의 관계
자바스크립트 엔진은 코드를 실행할 때 '콜 스택'이라는 자료구조를 사용합니다. 콜 스택은 함수 호출을 추적하는데, 함수가 호출되면 스택에 추가(push)되고, 함수 실행이 완료되면 스택에서 제거(pop)됩니다.
그렇다면 클로저는 이 콜 스택과 어떤 관계가 있을까요?
function outer() {
let outerVar = "나는 외부 변수";
function inner() {
console.log(outerVar); // 외부 함수의 변수를 참조
}
return inner;
}
const myInner = outer();
// outer 함수는 실행을 마치고 콜 스택에서 제거됨
myInner(); // "나는 외부 변수" 출력
// 그런데 어떻게 외부 함수의 변수에 여전히 접근할 수 있을까?
외부 함수 outer가 실행을 마치고 콜 스택에서 제거된 후에도, 반환된 내부 함수 inner는 여전히 outerVar에 접근할 수 있습니다. 이것이 바로 클로저의 마법입니다!
자바스크립트 엔진은 함수가 다른 함수 내에서 정의되고, 외부 함수의 변수를 참조하는 것을 감지하면, 해당 변수를 특별한 메모리 공간(클로저)에 저장합니다. 그래서 외부 함수가 이미 실행을 마쳤더라도, 내부 함수는 이 클로저를 통해 외부 함수의 변수에 계속 접근할 수 있습니다.
3. 🧠 클로저의 개념 이해하기
클로저는 추상적인 개념이라 이해하기 어려울 수 있습니다. 그래서 실생활의 비유를 통해 설명해 보겠습니다.
클로저는 마치 가방 같은 것입니다
함수가 생성될 때, 그 함수는 자신만의 '가방'을 받습니다. 이 가방 안에는 함수가 접근할 수 있는 모든 변수가 들어 있습니다. 함수가 다른 곳으로 이동하더라도(예: 반환되거나 변수에 할당됨), 이 가방을 계속 가지고 다닙니다.
function createPet(name) {
// 'name'은 createPet 함수의 지역 변수입니다.
return {
getName: function() {
return name; // 내부 함수가 'name' 변수에 접근
},
setName: function(newName) {
name = newName; // 내부 함수가 'name' 변수를 수정
}
};
}
const myPet = createPet("멍멍이");
console.log(myPet.getName()); // "멍멍이"
myPet.setName("야옹이");
console.log(myPet.getName()); // "야옹이"
여기서 getName과 setName 함수는 둘 다 createPet 함수의 name 변수에 접근할 수 있습니다. 이 두 함수는 같은 '가방'(클로저)을 공유하기 때문에, 한 함수에서 변수를 수정하면 다른 함수에서도 그 변화를 볼 수 있습니다.
4. ✨ 클로저의 작용 원리
클로저가 어떻게 작동하는지 더 깊이 이해하기 위해, 자바스크립트 엔진의 내부 동작을 살펴보겠습니다.
렉시컬 환경(Lexical Environment)
자바스크립트 엔진은 코드를 실행할 때 '렉시컬 환경'이라는 것을 생성합니다. 이 환경은 두 부분으로 구성됩니다:
- 환경 레코드(Environment Record): 현재 환경의 변수와 함수를 저장
- 외부 환경 참조(Outer Environment Reference): 외부 렉시컬 환경에 대한 참조
function makeAdder(x) {
// makeAdder의 렉시컬 환경: { x: 5 }
return function(y) {
// 내부 함수의 렉시컬 환경: { y: 3 }
// 외부 환경 참조 -> makeAdder의 렉시컬 환경
return x + y;
};
}
const add5 = makeAdder(5);
console.log(add5(3)); // 8
여기서 add5는 내부 함수를 참조하고, 이 내부 함수는 makeAdder의 렉시컬 환경에 대한 참조를 유지합니다. 그래서 add5(3)을 호출할 때, 내부 함수는 자신의 렉시컬 환경에서 y의 값(3)을 찾고, 외부 환경(makeAdder의 렉시컬 환경)에서 x의 값(5)을 찾아 이들을 더합니다.
클로저와 가비지 컬렉션
일반적으로 함수 실행이 끝나면, 그 함수의 렉시컬 환경은 가비지 컬렉터에 의해 메모리에서 제거됩니다. 하지만 클로저가 있는 경우, 내부 함수가 외부 함수의 환경을 참조하고 있기 때문에, 외부 함수의 환경은 계속 메모리에 남아 있게 됩니다.
function heavyComputation() {
const bigData = new Array(1000000).fill('데이터');
return function() {
console.log("데이터 크기:", bigData.length);
};
}
const showDataSize = heavyComputation();
// heavyComputation은 실행을 마쳤지만, bigData는 메모리에 남아 있음
showDataSize(); // "데이터 크기: 1000000" 출력
이 예제에서 bigData 변수는 큰 메모리를 차지합니다. heavyComputation 함수가 실행을 마쳐도, 반환된 내부 함수가 bigData를 참조하고 있기 때문에, bigData는 가비지 컬렉션되지 않습니다. 이것은 메모리 누수의 원인이 될 수 있으므로, 클로저를 사용할 때는 주의가 필요합니다.
5. 🏷️ 렉시컬 스코핑과 클로저의 개념
렉시컬 스코핑(Lexical Scoping)은 함수를 어디서 선언했는지에 따라 상위 스코프가 결정되는 것을 말합니다. 이는 클로저의 핵심 개념입니다.
동적 스코핑 vs 렉시컬 스코핑
- 동적 스코핑: 함수를 어디서 호출했는지에 따라 상위 스코프가 결정
- 렉시컬 스코핑: 함수를 어디서 선언했는지에 따라 상위 스코프가 결정
자바스크립트는 렉시컬 스코핑을 사용합니다. 다음 예제를 살펴봅시다:
let name = "전역";
function printName() {
console.log(name);
}
function outerFunction() {
let name = "외부";
printName();
}
outerFunction(); // "전역" 출력 (렉시컬 스코핑)
// 동적 스코핑이었다면 "외부"가 출력됐을 것입니다.
여기서 printName 함수는 전역 스코프에서 선언되었기 때문에, 전역 변수 name을 참조합니다. outerFunction 내부에서 호출되었더라도, printName의 상위 스코프는 전역 스코프입니다.
클로저와 렉시컬 스코핑의 관계
클로저는 렉시컬 스코핑을 기반으로 합니다. 내부 함수는 자신이 선언된 환경(외부 함수의 렉시컬 환경)을 기억합니다.
function outerFunction() {
let outerVar = "외부 변수";
function innerFunction() {
let innerVar = "내부 변수";
console.log(outerVar); // 외부 함수의 변수에 접근
}
return innerFunction;
}
const inner = outerFunction();
inner(); // "외부 변수" 출력
이 예제에서 innerFunction은 자신이 선언된 렉시컬 환경(outerFunction 내부)을 기억하고, 그 환경에 있는 outerVar 변수에 접근할 수 있습니다.
6. 📚 클로저의 활용과 장점
클로저는 다양한 프로그래밍 패턴에서 활용될 수 있으며, 몇 가지 중요한 장점을 제공합니다.
1. 데이터 은닉과 캡슐화
클로저를 사용하면 변수를 함수 외부에서 직접 접근할 수 없게 만들 수 있습니다. 이를 통해 정보 은닉과 캡슐화를 구현할 수 있습니다.
function createBankAccount(initialBalance) {
let balance = initialBalance; // 외부에서 직접 접근 불가능한 변수
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount > balance) {
console.log("잔액 부족!");
return balance;
}
balance -= amount;
return balance;
},
getBalance: function() {
return balance;
}
};
}
const myAccount = createBankAccount(1000);
console.log(myAccount.getBalance()); // 1000
myAccount.deposit(500);
console.log(myAccount.getBalance()); // 1500
myAccount.withdraw(2000); // "잔액 부족!"
console.log(myAccount.getBalance()); // 1500
// console.log(myAccount.balance); // undefined - balance 변수는 직접 접근 불가능
이 예제에서 balance 변수는 외부에서 직접 접근할 수 없고, 오직 제공된 메서드(deposit, withdraw, getBalance)를 통해서만 접근할 수 있습니다. 이렇게 하면 데이터의 무결성을 유지하고, 잘못된 조작을 방지할 수 있습니다.
2. 함수 팩토리 생성
클로저를 활용하면 특정 기능을 가진 함수를 생성하는 팩토리 함수를 만들 수 있습니다.
function multiplyFactory(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyFactory(2);
const triple = multiplyFactory(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
이 예제에서 multiplyFactory는 인자로 받은 factor를 기억하는 새로운 함수를 반환합니다. 이렇게 생성된 함수(double, triple)는 각각 다른 factor 값을 기억하고 있습니다.
3. 모듈 패턴 구현
클로저를 사용하면 자바스크립트에서 모듈 패턴을 구현할 수 있습니다. 모듈 패턴은 관련된 기능을 하나의 단위로 묶고, 필요한 부분만 외부에 노출시키는 패턴입니다.
const calculator = (function() {
let result = 0;
// 비공개 함수
function validate(n) {
return typeof n === 'number';
}
return {
add: function(n) {
if (validate(n)) {
result += n;
}
return this;
},
subtract: function(n) {
if (validate(n)) {
result -= n;
}
return this;
},
getResult: function() {
return result;
}
};
})();
calculator.add(5).subtract(2);
console.log(calculator.getResult()); // 3
// console.log(calculator.result); // undefined - 직접 접근 불가
// calculator.validate(5); // 오류 - 비공개 함수는 외부에서 호출 불가
이 예제에서는 즉시 실행 함수(IIFE)를 사용하여 모듈을 생성했습니다. 모듈 내부의 result 변수와 validate 함수는 외부에서 접근할 수 없고, 오직 반환된 객체의 메서드(add, subtract, getResult)를 통해서만 상호작용할 수 있습니다.
4. 비동기 프로그래밍
클로저는 비동기 프로그래밍에서도 유용하게 사용됩니다. 콜백 함수가 이전 상태의 값을 기억해야 할 때 클로저가 활용됩니다.
function fetchData(url) {
// 시작 시간을 기록
const startTime = new Date().getTime();
// 비동기 요청
fetch(url)
.then(response => response.json())
.then(data => {
// 완료 시간을 계산
const endTime = new Date().getTime();
const duration = endTime - startTime;
// 클로저를 통해 startTime 변수에 접근
console.log(`데이터를 가져오는 데 ${duration}ms가 소요되었습니다.`);
console.log(data);
});
}
fetchData("https://api.example.com/data");
이 예제에서 .then에 전달된 콜백 함수는 startTime 변수를 클로저를 통해 기억합니다. 비동기 요청이 완료되어 콜백이 실행될 때, 이전에 기록한 시작 시간에 접근할 수 있습니다.
정리
클로저는 자바스크립트의 강력한 기능 중 하나로, 함수가 자신이 생성된 환경을 기억하고 접근할 수 있게 해줍니다. 클로저를 이해하고 활용하면 다음과 같은 이점을 얻을 수 있습니다:
- 데이터 은닉과 캡슐화: 변수를 외부에서 직접 접근할 수 없게 보호
- 상태 유지: 함수가 이전 상태 값을 기억하고 접근
- 모듈화: 관련된 기능을 하나의 단위로 묶고, 필요한 부분만 노출
- 함수 팩토리: 특정 동작을 수행하는 함수를 생성
- 비동기 프로그래밍: 콜백 함수에서 이전 상태 값 접근
클로저는 처음에는 이해하기 어려울 수 있지만, 일단 익숙해지면 자바스크립트에서 가장 유용하고 강력한 도구 중 하나가 됩니다. 꾸준한 연습과 실험을 통해 클로저의 개념을 완전히 자신의 것으로 만들어 보세요!
'1일 1CS(Computer Science)' 카테고리의 다른 글
리액트의 Props와 State (0) | 2025.04.03 |
---|---|
이벤트 루프에 대해서 설명해주세요. (0) | 2025.03.28 |
TanStack Query: staleTime과 gcTime (0) | 2025.03.27 |
reflow와 repaint의 차이점과 최적화 방법 (1) | 2025.03.26 |
실행 컨텍스트란? (0) | 2025.03.26 |