JavaScript에서 반복문 내에서 addEventListener를 사용할 때 발생하는 일반적인 문제와 해결 방법, 그리고 클로저(Closure)의 개념과 활용에 대해 자세히 설명합니다.

반복문과 addEventListener

HTML 요소에 이벤트 리스너를 추가할 때, 반복문을 사용하여 여러 요소에 대해 이벤트를 등록하는 경우가 많습니다. 하지만 이 과정에서 변수 스코프와 클로저의 동작 방식에 따라 예기치 않은 결과가 발생할 수 있습니다.

1.1 문제 상황: var키워드 사용 시 문제

아래는 여러 버튼에 클릭 이벤트 리스너를 추가하는 예제입니다.

HTML<button class="button-element">Button 1</button>
<button class="button-element">Button 2</button>
<button class="button-element">Button 3</button>
Javascriptconst buttons = document.querySelectorAll('.button-element');

  // var로 선언한 경우
  for (var i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', function() {
      console.log('Button ' + (i + 1) + ' clicked');
    });
  }

위 코드를 실행하면 모든 버튼을 클릭했을 때 항상 Button 4 clicked가 출력됩니다. 이는 var 키워드가 함수 스코프를 가지기 때문에, for 문이 종료된 후 i의 값이 buttons.length(여기서는 3)으로 고정되기 때문입니다. 이벤트 핸들러가 실행될 때, 모든 핸들러가 동일한 전역 스코프의 i를 참조하므로 최종 값인 3을 사용하게 됩니다.

1.2 해결 방법 1: let키워드 사용

ES6에서 도입된 let키워드는 블록 스코프를 제공합니다. 이를 사용하면 각 반복마다 새로운 i 변수가 생성되므로, 각 이벤트 핸들러가 올바른 i값을 참조할 수 있습니다.

Javascriptconst buttons = document.querySelectorAll('.button-element');

// let으로 선언한 경우
for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', function() {
    console.log('Button ' + (i + 1) + ' clicked');
    });
    }

버튼을 차례로 클릭하면 Button 1 clicked, Button 2 clicked, Button 3 clicked가 출력됩니다. 이는 let이 각 반복마다 새로운 스코프를 생성하여 i값을 캡처하기 때문입니다.

1.3 해결 방법 2: 즉시 실행함수(IIFE) 사용

var를 사용해야 하는 환경(예: ES5 이하)에서는 즉시 실행함수(IIFE; Immediately Invoked Function Expression)를 사용하여 각 반복마다 새로운 함수 스코프를 생성할 수 있습니다.

Javascriptconst buttons = document.querySelectorAll('.button-element');

for (var i = 0; i < buttons.length; i++) {
  (function(i) {
    buttons[i].addEventListener('click', function() {
      console.log('Button ' + (i + 1) + ' clicked');
    });
  })(i);
}

IIFE는 각 반복마다 새로운 함수 스코프를 생성하고,i의 현재 값을 매개변수로 전달하여 캡처합니다. 따라서 각 이벤트 핸들러는 고유한 i값을 참조하게 됩니다.

클로저(Closure)란?

클로저는 함수가 선언될 때의 환경(즉, 변수와 스코프)을 기억하고, 이후 함수가 다른 스코프에서 호출되더라도 그 환경에 접근할 수 있게 하는 JavaScript의 기능입니다.

2.1. 클로저 기본 예제

클로저의 기본적인 동작Javascriptfunction outerFunction() {
  let outerVariable = "I am from the outer function";
  function innerFunction() {
    console.log(outerVariable);
  }
  return innerFunction;
}
const closureExample = outerFunction();
closureExample(); // 출력: "I am from the outer function"
  • outerFunctionouterVariable을 정의하고, innerFunction을 반환합니다.
  • innerFunction은 외부 함수의 outerVariable을 참조합니다.
  • outerFunction이 실행을 마친 후에도 innerFunction은 클로저를 통해 outerFunction에 접근할 수 있습니다.

2.2. 클로저의 활용

정보 은닉 :
private 변수를 구현하여 데이터에 대한 직접 접근을 제한합니다.
비동기 프로그래밍 :
비동기 콜백에서 특정 상태를 유지합니다.
함수 팩토리 :
특정 설정을 기반으로 함수를 생성합니다.
함수 팩토리 예제Javascriptfunction createCounter() {
  let count = 0;

  return {
    increment: function() {
      count++;
      console.log(count);
    },
    decrement: function() {
      count--;
      console.log(count);
    }
  };
}

const counter = createCounter();
counter.increment(); // 출력: 1
counter.increment(); // 출력: 2
counter.decrement(); // 출력: 1

createCounter는 클로저를 사용하여count변수를 내부에 유지하며, 외부에서 incrementdecrement 메서드를 통해 간접적으로만 접근할 수 있게 합니다.