
클로저는 JavaScript에서 가장 강력하면서도 때로는 이해하기 어려운 개념 중 하나다.
간단히 말하면, 클로저는 '함수가 자신이 생성된 환경(스코프)을 기억하는 현상'을 말한다.
클로저를 일상에 빗대어보면 자기 자신과도 같다. 우리 집을 상상해보자. 우리 집(외부 함수)안에는 내 방(내부 함수)가 있다. 나는 내 방의 물건뿐만 아니라 집 전체의 물건을 사용할 수도 있다. 내가 독립을 해 다른 곳으로 이사(함수 반환)를 하게 되더라도, 내가 원래 살던 본가의 물건들을 계속 기억하고 사용할 수도 있다. 이것이 바로 클로저다.
JavaScript에서 클로저는 다음과 같이 구성된다.
function 외부함수() {
let 외부변수 = '나는 외부 변수';
function 내부함수() {
console.log(외부변수); // 외부 함수의 변수에 접근 가능
}
return 내부함수;
}
const 클로저함수 = 외부함수();
클로저함수(); // "나는 외부 변수" 출력
클로저를 커피 주문 시스템으로 이해해 보자.
function 고객정보생성(이름) {
const 고객이름 = 이름;
const 포인트 = 0;
return function(메뉴, 가격) {
console.log(`${고객이름}님이 ${메뉴}를 주문하셨습니다. 가격은 ${가격}원입니다.`);
// 여기서 고객이름은 외부함수의 변수이지만 접근 가능함
};
}
const 김철수_주문 = 고객정보생성('김철수');
김철수_주문('아메리카노', 4500); // "김철수님이 아메리카노를 주문하셨습니다. 가격은 4500원입니다."
이 예시에서 김철수_주문 함수는 고객정보생성 함수가 실행을 마친 후에도 고객이름 변수를 기억하고 있다. 이것이 클로저의 핵심이다 🚩
클로저를 사용하면 변수를 외부에서 직접 접근하지 못하게 하면서, 특정 함수를 통해서만 접근할 수 있게 만들 수 있다.
이는 마치 금고에 중요한 물건을 보관하는 것과 같다. 금고는 비밀번호(함수)를 알고 있는 사람만 내용물(변수)에 접근할 수 있다. 이렇게 하면 데이터가 예상치 못하게 변경되는 것을 방지하고, 항상 정해진 규칙에 따라서만 데이터가 수정되도록 할 수 있다.
실제 웹 개발에서는 사용자 정보, 인증 상태 등을 클로저를 통해 안전하게 관리하는 패턴이 많이 사용된다. 예를 들어 로그인 상태 관리, 개인 설정 저장 등이 이에 해당한다.
function 은행계좌(초기금액) {
let 잔액 = 초기금액;
return {
예금하기: function(금액) {
잔액 += 금액;
return `${금액}원이 입금되었습니다. 현재 잔액: ${잔액}원`;
},
출금하기: function(금액) {
if (잔액 >= 금액) {
잔액 -= 금액;
return `${금액}원이 출금되었습니다. 현재 잔액: ${잔액}원`;
} else {
return '잔액이 부족합니다.';
}
},
잔액확인: function() {
return `현재 잔액: ${잔액}원`;
}
};
}
const 내계좌 = 은행계좌(10000);
console.log(내계좌.예금하기(5000)); // "5000원이 입금되었습니다. 현재 잔액: 15000원"
console.log(내계좌.출금하기(3000)); // "3000원이 출금되었습니다. 현재 잔액: 12000원"
console.log(내계좌.잔액확인()); // "현재 잔액: 12000원"
// 직접 잔액에 접근 불가능
console.log(내계좌.잔액); // undefined
여기서 잔액 변수는 외부에서 직접 접근할 수 없지만, 클로저를 통해 정의된 함수들은 이 변수에 접근하고 수정할 수 있다. 이렇게 데이터를 보호하면서도 제어된 방식으로만 접근하게 함으로써 프로그램의 안정성을 높일 수 있다.
클로저를 사용하면 비슷한 함수를 여러 개 만들어낼 수 있다. 이는 마치 같은 틀로 다양한 제품을 찍어내는 공장과 같다.
함수 팩토리는 특히 UI 컴포넌트 개발이나 이벤트 핸들러 등을 생성할 때 유용하다. 예를 들어, 다양한 버튼에 각각 다른 기능을 부여하거나, 여러 사용자에 대한 맞춤형 함수를 생성할 때 활용할 수 있다.
function 인사말생성기(인사말) {
return function(이름) {
return `${인사말}, ${이름}님!`;
};
}
const 안녕인사 = 인사말생성기('안녕하세요');
const 환영인사 = 인사말생성기('환영합니다');
console.log(안녕인사('철수')); // "안녕하세요, 철수님!"
console.log(환영인사('영희')); // "환영합니다, 영희님!"
여기서 인사말생성기는 다양한 인사 함수를 만드는 팩토리 역할을 한다. 각 생성된 함수는 자신만의 인사말을 기억하고 있으면서, 전달받는 이름에 따라 맞춤형 문구를 생성한다. 이처럼 클로저를 활용하면 코드 재사용성을 높이고 중복을 줄일 수 있다.
웹 개발에서 클로저는 이벤트 핸들러나 비동기 콜백 함수에서 자주 활용된다. 클로저가 없다면 전역 변수에 의존해야 하는 상황이 많아지고, 이는 코드의 복잡성과 오류 가능성을 높인다.
특히 Ajax 요청이나 타이머 함수처럼 나중에 실행되는 코드에서 현재의 상태를 기억해야 할 때 클로저는 필수적이다.
function 카운터버튼생성(초기값, 버튼ID, 결과ID) {
let 카운트 = 초기값;
document.getElementById(버튼ID).addEventListener('click', function() {
카운트++;
document.getElementById(결과ID).textContent = `현재 카운트: ${카운트}`;
});
// 리셋 기능 추가
document.getElementById('리셋버튼').addEventListener('click', function() {
카운트 = 초기값;
document.getElementById(결과ID).textContent = `카운트 리셋: ${카운트}`;
});
}
// 여러 개의 독립된 카운터를 생성할 수 있음
카운터버튼생성(0, '버튼1', '결과1');
카운터버튼생성(10, '버튼2', '결과2');
이 예시에서 각 이벤트 핸들러는 자신만의 카운트 변수를 기억한다. 버튼을 클릭할 때마다 해당 카운터의 값만 변경된다. 클로저가 없다면 모든 카운터가 하나의 전역 변수를 공유해야 했을 것이고, 이는 여러 버튼이 서로 간섭하는 문제를 일으켰을 것이다.
비동기 요청에서도 클로저는 유용하다
function 데이터요청하기(사용자ID) {
const 타임스탬프 = Date.now(); // 요청 시작 시간
fetch(`https://api.example.com/users/${사용자ID}`)
.then(function(응답) {
// 이 콜백 함수는 나중에 실행되지만, 여전히 사용자ID와 타임스탬프에 접근 가능
console.log(`${사용자ID}의 데이터를 받아왔습니다. 소요시간: ${Date.now() - 타임스탬프}ms`);
return 응답.json();
})
.then(function(데이터) {
console.log(`${사용자ID}의 이름은 ${데이터.이름}입니다.`);
});
}
데이터요청하기('user123');
fetch 요청은 비동기적으로 실행되므로, .then() 안의 콜백 함수들은 나중에(응답이 도착한 후) 실행된다. 이때 중요한 점은 콜백 함수가 실행될 때 사용자ID와 타임스탬프 변수에 여전히 접근할 수 있다는 것이다.
여러 네트워크 요청을 동시에 보내는 상황을 생각해보자. 클로저가 없다면 어떤 응답이 어떤 요청에 대한 것인지 추적하기 어렵다. 클로저는 각 요청이 자신만의 컨텍스트(사용자ID 등)를 유지하도록 해준다.
클로저는 마치 택배 운송장과 같다. 여러 택배를 동시에 보내도, 각 택배에는 고유한 운송장 번호가 있어 어떤 택배가 어디로 가야 하는지 알 수 있다. 비동기 요청에서 클로저는 이 운송장 역할을 한다.
// 전역 객체를 사용한 방식 (좋지 않은 방식)
const 요청정보 = {};
function 데이터요청하기_클로저없이(사용자ID) {
const 요청키 = `요청_${Date.now()}`;
요청정보[요청키] = {
사용자ID: 사용자ID,
타임스탬프: Date.now()
};
fetch(`https://api.example.com/users/${사용자ID}`)
.then(function(응답) {
const 정보 = 요청정보[요청키];
console.log(`${정보.사용자ID}의 데이터를 받아왔습니다. 소요시간: ${Date.now() - 정보.타임스탬프}ms`);
return 응답.json();
})
.then(function(데이터) {
console.log(`${요청정보[요청키].사용자ID}의 이름은 ${데이터.이름}입니다.`);
// 처리 완료 후 메모리 정리
delete 요청정보[요청키];
});
}
이 방식은 다음과 같은 여러 문제를 야기한다.
따라서 클로저는 JavaScript의 비동기 프로그래밍에서 없어서는 안 될 요소이며, 특히 API 요청처럼 시간 간격을 두고 실행되는 코드에서 컨텍스트를 유지하는 데 필수적이다.
클로저는 JavaScript의 강력한 기능으로, 함수가 자신이 생성된 환경을 기억하는 현상이다.
이를 통해 데이터 은닉, 함수 팩토리 생성, 이벤트 핸들링 등 다양한 기능을 구현할 수 있다.
마치 어떠한 공간을 떠나도 그 공간에 있던 물건들을 기억하고 사용할 수 있는 것처럼 말이다.
다만 클로저는 메모리를 계속 사용하므로, 불필요하게 많은 클로저를 생성하면 메모리 누수가 발생할 수 있어 필요한 경우에만 사용하는 것이 좋다.