[javascript] 클로저(Closure)의 이해
"만들어진 시점의 실행 환경을 기억하는 함수."
를 클로저, 또는 클로저 함수라고 합니다.
짧게 요약하면 그렇습니다.
클로저 함수를 호출한 함수가 종료되더라도, 호출한 함수의 환경(변수 등)을 클로저 함수가 기억하고 있는 것입니다.
함수를 예를 들어 설명하겠습니다.
클로저는 아래와 같은 방식으로 익명함수를 반환해서 구현을 하므로 구조를 잘 숙지해두는게 좋습니다.
function outerFunc(name){ let saying = name + ' 안녕!'; return function(){ return saying; }}
let closure1 = outerFunc('라이언');let closure2 = outerFunc('콘');
console.log(closure1());console.log(closure2());
outerFunc() 는 익명함수를 반환합니다.
반환된 함수는 변수에 저장되고, 익명함수를 실해하면 익명함수가 반환하는 saying 변수의 값을 콘솔에 표시합니다.
여기서 중요한 점은 outerFunc() 함수가 이미 종료되었다는 점입니다.
outerFunc() 함수가 종료되었기 때문에 함수 로컬 변수인 saying 도 사라져야 합니다.
하지만 클로저를 실행하면 saying 변수의 값이 출력되고, 거기다 더해 넘긴 파라메터 값이 적용된 saying 변수가 출력된다는 점입니다.
바로 이점이 클로저의 최대 장점입니다.
클로저가 호출되는 시점의 로컬 변수 정보를 클로저 객체가 존재하는 동안 참조를 제공합니다.
또 다른 장점은 외부에서 로컬 변수(saying)를 접근할 수 없게 막을 수 있다는 점입니다.
클로저 함수는 유지되는 실행환경 참조로 인해 saying 변수에 접근할 수 있지만, outerFunc()는 이미 종료했기 때문에 클로저 말고는 로컬 변수에 접근할 수 있는 방법이 없습니다.
따라서, 사용자가 임의로 변경하면 안되는 설정 변수 등을 사용할 필요가 있는 경우 클로저는 훌륭한 대안이 될 수 있습니다.
반복문으로 클로저 생성
클로저는 실행환경을 기억하는 특성때문에 페이지의 여러가지 상황에 따른 메시지를 표시하거나, 위치에 맞는 값을 표시하는 등의 반복되는 이벤트 처리를 구현하는데 매우 뛰어난 효율을 보입니다.
코드를 단순화하고 확장성을 높일 수 있지만 구현상의 실수로 반복문 지옥에 빠지기도 합니다.
반복문으로 클로저를 생성할 경우 흔하게 발생하는 문제로 실행환경의 변수값이 원하는 값이 아닌 경우입니다.
루프를 돌면서 각각의 클로저에 맞는 실행환경 변수값이 적용될거라고 예상하지만 실제로는 마지막 값을 참조하는 경우입니다.
잘못된 구현을 예로 들어 설명하겠습니다.
메뉴 항목에 마우스 클릭을 하면 툴팁 메시지(알림창으로 표시하기로 함)를 표시하는 클로저를 구현한 것입니다.
메뉴 항목 아이디(ID)에 따라 다른 툴팁 메시지가 보이는 클로저를 생성합니다.
코드의 addEventLister() 안의 콜백 함수가 클로저입니다.
누구에게나 그럴듯한 계획이 있지만 실제로 구현해보면 helper의 3번째 메시지만 나옵니다.
루프안의 msg 변수 선언자로 var 를 사용한 것에 주의해야 합니다.
아주 중요합니다.
var helper = [{id: 'menu1', txt:'About Our Service'}, {id: 'menu2', txt:'Product Info'}, {id: 'menu3', txt:'Customer Service'}];
for(var i = 0; i < helper.length; i++){ var msg = helper[i]; document.getElementById(msg.id).addEventListener('click',()=>{tooltip(msg.txt);});}
function tooltip(msg){ alert(msg);}
이 코드의 문제점은 루프 안에서 var msg 로 선언한 변수가 전역전수라는 것입니다.
전역 변수인 msg는 루프를 모두 돈 후, helper 배열의 마지막 요소를 가지고 있습니다.(
클릭 이벤트가 발생한 시점에 msg.txt를 파라메터로 참조해 넘기게 되는데, msg가 전역변수이므로 마지막 값을 가지고 있게 됩니다.
문제의 해결 방법은 의외로 간단합니다.
var msg => let msg
로 변경하면 msg 변수는 클로저 로딩 시점의 환경변수를 참조하는데, msg 변수는 루프를 돌 때마다 새로 선언하는 로컬 변수이기 때문에 루프 종료 후에도 msg 로컬변수에 대한 참조가 유지됩니다.
ES6 부터 새로 생긴 많은 기능들이 ES6의 변수인 let을 기준으로 사용하는 것을 고려해서 만들어졌습니다.
클로저 또한 let 을 사용하면 잠재적으로 발생할 수 있는 오류나 버그를 없앨 수 있습니다.
클로저의 단점
실행 환경을 기억하는 것은 파라메터를 일일이 넘겨야 하는 번거로움을 줄여주고, 데이터의 보안성도 동시에 높이는 장점이 있습니다.
반면, 실행될 때마다 각자의 참조 영역을 유지해야 하기 때문에 메모리 사용량이 늘어납니다.
실행환경에서 유지해야 하는 데이터가 큰 경우 클로저는 생각보다 많은 메모리를 소모하게 됩니다.
따라서 클로저는 사용 후에는 메모리를 해제해 불필요한 메모리 소모를 줄여야 합니다.
클로저 해제는
closure1 = null;
과 같이 클로저 변수에 널을 대입하면 됩니다.
관리상의 번거로움이 있고, 메모리 소모가 더 있지만 클로저는 장점이 훨씬 더 많습니다,
클로저는 변수를 탐색할 때 실행환경에서만 찾기 때문에 스코프 체인을 따라 올라가 전역변수까지 확인하는 과정이 없어, 스코프 탐색에 시간이 덜 소모되는 등 실행 속도상의 잇점이 있습니다.
또한, 비동기 처리 후 결과를 비교하거나 조합하는 경우, 실행 환경의 참조에 필요한 데이터를 가지고 있기 때문에 전역 변수를 따로 만들어 처리하는 하는 방법보다 훨씬 세련된 처리가 가능해집니다.