오늘은 클로저(Closure)에 대해 알아보고자 합니다.
클로저(Closure)
클로저의 정의는 함수와 그 함수가 선언된 렉시컬 환경(Lexical Environment)의 조합입니다.
간단히 바꿔 설명하자면 내부 함수가 외부함수의 변수에 접근할 수 있는 것을 말합니다.
렉시컬 환경이라는 게 어떤 의미인지 이해가 안 가서 더 찾아보니
함수가 만들어질 때의 주변 환경을 기억하는 것 정도로 설명이 되더군요.
우리가 어딜 가든 자신의 집 주소를 기억하고 있는 것처럼,
함수도 자신이 어디서 만들어졌는지를 기억한다고 이해하시면 될 거 같습니다.
function outer() {
let x = "외부";
function inner() {
console.log(x); // "외부" 출력
}
inner();
}
outer();
여기서 inner 함수는 자신이 선언된 위치인 outer 함수 내부의 x를 참조
프로젝트를 진행하며 내가 겪었던 클로저(Closure) 문제
유니티에서 각 버튼 클릭 시 ID 값을 넘겨주고자 다음과 같이 코드를 짰었습니다.
코드 일부 공개
// 잘못된 방식
for (int i = 0; i < _stageBtn.Length; i++)
{
_stageBtn[i].onClick.AddListener(() =>
{
var currentStageData = GameManager.Instance.TotalStageID[i];
//이하 생략
});
}
// 이 경우 람다식이 loop variable 'i'를 직접 참조.
// 모든 리스너가 같은 'i'를 참조하므로, 루프가 끝난 후에는 모든 리스너가 'i'의 최종값을 사용
// 내가 해결한(사용한) 방법 : 클로저 변수 캡처(Closure Variable Capture) 방식 : C#에서 주로 사용하는 패턴
for (int i = 0; i < _stageBtn.Length; i++)
{
int index = i;
var stageData = GameManager.Instance.TotalStageID[index]; //현재 반복의 데이터를 새로운 변수에 저장
// stageData는 이 리스너가 생성될 때의 값을 "캡처"하여 유지
// 각 반복에서 생성된 고유한 변수를 참조
_stageBtn[i].onClick.AddListener(() =>
{
//이하 생략
});
}
//결과는 같지만 다른 방식 : 즉시 실행 함수 사용 : JavaScript에서 주로 사용하는 패턴
for (int i = 0; i < _stageBtn.Length; i++)
{
_stageBtn[i].onClick.AddListener((Action)(() => {
int index = i;
var currentStageData = GameManager.Instance.TotalStageID[index];
//이하 생략
}));
}
변수 캡쳐 vs 즉시 실행 함수(IIFE)
변수 캡쳐 방식
단순히 현재 값을 보존하려는 경우
여러 변수를 캡처해야 하는 경우
캡처된 값을 여러 곳에서 재사용하는 경우
비동기 작업에서 현재 상태를 유지해야 하는 경우
주로 사용한다고 합니다.
즉시 실행 함수(IIFE) 방식
독립된 스코프( = 변수의 유효 범위 )가 필요한 경우
private 변수/함수를 만들어야 하는 경우
모듈 패턴을 구현할 때
여러 비동기 작업을 하나의 스코프로 묶어야 할 때
주로 사용한다고 합니다.
정리
IIFE는 명시적으로 새로운 스코프를 만들고,
변수 캡처는 기존 스코프 내에서 새 변수를 만든다는 차이점이 있습니다.
스코프는 내부에서 외부로는 접근할 수 있으나 내부 변수는 접근이 불가능합니다.
let global = "전역";
function outer() {
let outerVar = "외부";
function inner() {
let innerVar = "내부";
console.log(global); // 전역 스코프 접근
console.log(outerVar); // 외부 스코프 접근
console.log(innerVar); // 현재 스코프 접근
}
}
이를 스코프 체인이라고 한다.
마무리
처음에 잘못된 방식으로 코드를 짰다가 왜 안되지? 찾고 해결하는 과정에서
클로저(Closure)라는 용어를 처음 들어서 이렇게 정리해 봤습니다.
지금 보니까 대표적인 클로저 문제 예시인 이벤트 리스너 문제와 완전 같네요;;
대표적인 클로저 문제 예시
for문 setTimeout 문제
// 문제가 있는 코드
for(var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// 예상: 1, 2, 3, 4, 5가 1초 간격으로 출력
// 실제: 6, 6, 6, 6, 6이 출력됨
이벤트 리스너 문제
// 문제: 각 버튼이 자신의 인덱스를 기억하게 만들기
function createButtons(num) {
for(var i = 1; i <= num; i++) {
var button = document.createElement('button');
button.innerText = '버튼' + i;
// 잘못된 방법
button.onclick = function() {
console.log(i);
}
// 올바른 방법
button.onclick = (function(index) {
return function() {
console.log(index);
}
})(i);
document.body.appendChild(button);
}
}