[번역] JS에서의 클로저

2024년 3월 16일

글 링크

- 클로저 정리

자바스크립트의 클로저는 현대 JS 개발을 하는데에 굉장히 중요한 역할을 하는 기본적이면서도 강력한 개념입니다.

클로저는 언어에서 스코프, 데이터 은닉화, 그리고 함수의 동작을 이해하는데에 있어서 핵심입니다.

- 클로저의 중요성

자바스크립트는 개발자들이 기능적이면서도 표현력 있는 코드를 작성할수 있도록 하는 다양성으로 잘 알려져 있습니다.

클로저는 이러한 유연성의 핵심입니다.

클로저는 주변 상태를 포착하고 기억할 수 있는, 함수라고 불리는 독립적인 코드 단위를 만들 수 있도록 합니다.

이러한 "데이터를 기억하는" 능력이 클로저를 특별하게 만들고 필수적인 요소로 자리매김 하게 합니다.

클로저는 이런 것들을 가능하게 합니다.

1. 데이터 캡슐화

함수 범위 내의 변수를 캡슐화 해서, 의도하지 않은 수정이나 전역 네임 스페이스의 오염을 방지할 수 있습니다.

이러한 캡슐화는 유지보수가 쉽고 확장 가능한 어플리케이션을 구축하는데에 있어 매우 중요합니다.

2. 프라이빗한 변수 만들기

클로저를 사용하면, 외부에서 직접 접근할수 없는 변수를 생성하여 데이터의 개인성과 보안성을 향상 시킬수 있습니다.

3. 콜백 구현

콜백 함수는 자바스크립트에서 흔히 사용되는 패턴으로, 상태를 유지하고 비동기 작업을 수행하기 위해 클로저에 크게 의존합니다.

4. 모듈형 코드 구축

클로저는 모듈형 코드 조직에 있어 근본적인 역할을 하며, 재사용 가능한 컴포넌트와 라이브러리의 생성을 용이하게 합니다.

- 클로저와 렉시컬 스코프

클로저의 개념은, 렉시컬 스코핑(정적 스코핑이라고도 알려져있는)과 밀접하게 연관이 되어있습니다.

렉시컬 스코핑이란, 변수의 범위가 소스 코드 내의 위치에 의해 결정 된다는 의미입니다.

즉, 함수를 호출할 때가 아니라, 함수를 어디에 선언하였는지에 따라서 스코프가 결정 됩니다.

자바스크립태에서 클로저는 함수와 렉시컬 스코핑 간의 상호작용으로 발생합니다.

하나의 함수가 다른 함수 내부에서 정의 될때, 그 함수는 부모 함수의 스코프와 변수들을 캡쳐함으로써 클로저를 생성합니다.

이 캡쳐된 상태는 지속 되며, 외부 함수의 실행이 끝난 후에도 내부 함수가 데이터에 접근하고 조작할수 있도록 합니다.

- 클로저란 무엇인가 ?

클로저는 자바스크립트에서 매우 흥미롭고 기능적인 개념입니다.

이는 실행 위치와 관계 없이 자신이 정의된 렉시컬 스코프를 기억하는 함수로 종종 설명 됩니다.

이 섹션에서는 클로저가 변수를 어떻게 캡쳐하는지 설명하며, 클로저의 기본 개념을 설명하기 위한 예제를 제공할 것입니다.

자바스크립트에서 클로저는, 외부 (내부함수를 둘러싼) 함수가 실행을 마친후, 생명주기가 끝났음에도 그 외부 함수의 변수와 매개변수에 접근할 수 있는 함수를 의미합니다.

다시 말해서, 클로저는 생성될때의 환경과 함께 묶인 함수입니다.

이 환경에는 클로저가 생성 될때 당시에 범위에 있던 모든 변수, 매개변수, 함수들이 포함 됩니다.

- 클로저의 특성

  1. 함수내의 함수

클로저는 일반적으로 한 함수 내부에서 다른 함수가 정의 될때 발생합니다.

이러한 구조는 내부 함수가 외부 함수의 변수와 매개변수에 접근할수 있도록 하며, 이는 클로저의 기본적인 형태를 형성합니다.

  1. 렉시컬 스코프(정적 스코프)

클로저는 렉시컬 스코핑 규칙을 따릅니다.

즉 클로저는 자신이 정의된 스코프 내의 변수들을 캡쳐하고 기억합니다.

이는 함수의 실행 컨텍스트가 아닌, 그들이 정의된 소스 코드의 위치에 따라서 변수의 범위가 결정된다는 의미입니다.

  1. 외부 스코프에 대한 접근

클로저는 본인을 둘러싸고 있는 외부 스코프의 변수들에 접근하고 조작할 수 있습니다.

심지어 그 스코프가 종료 된 후에도, 이러한 접근이 가능합니다.

이는 클로저가 외부 스코프의 상태를 기억할수 있게 하며, 이를 통해 특정 데이터를 효과적으로 은닉하고 관리할 수 있습니다.

  1. 데이터 캡슐화

클로저를 통해 함수 스코프 내에서 프라이빗 변수와 함수를 생성함으로써, 데이터 캡슐화를 실현할 수 있습니다.

이는 클로저가 외부에서 접근할수 없는 변수를 유지할수 있도록 하고, 데이터 보호 및 은닉화를 가능하게 합니다.

이러한 특성은 모듈 패턴과 같은 설계 패턴을 구현할때 특히 유용하며, 코드의 재사용성과 유지보수성을 향상 시킵니다.

- 어떻게 클로저는 변수를 캡쳐하는가 ?

클로저는 생성될때의 전체 환경에 대한 참조를 유지함으로써 변수를 캡쳐합니다.

이 참조에는 로컬 스코프 내의 모든 변수와 외부 스코프에서의 어떤 변수들도 포함 됩니다.

이러한 동작은 클로저에 대한 참조가 있는 한 변수들이 가비지컬렉션의 대상이 되지 않도록 보장합니다.

코드를 한번 살펴보겠습니다.


function outer(){
  var message = 'Hello, ";

  function inner(name){
    console.log(message + name);
  }

  return inner;
}

var greet = outer(); // 'greet'은 현재 `inner` 함수를 가지고 있습니다.

greet('Alice'); // Hello, Alice

위의 예를 들어서, inner 함수는 외부 함수의 스코프로부터 message변수를 캡쳐합니다.

outer 함수의 실행이 끝난 후에도, 우리가 만약 greet('Alice')를 호출한다면, 클로저로 인하여 message 변수에 접근할수 있게 됩니다.

- 클로저를 이용한 카운터 예시

function createCounter() {
  var count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

var conter = createCounter();
counter(); // 1
counter(); // 2

위의 예에서, createCounter 함수는 count 변수를 캡쳐하고 있는 클로저를 반환합니다.

매번 클로저를 호출할때마다, count를 증가시키고 콘솔을 출력합니다.

여기서 클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수라고 정의해볼 수 있습니다.

- 데이터 은닉화

function createPerson(name) {
  var age = 0;

  return {
    getName: function () {
      return name;
    },
    getAge: function () {
      return age;
    },
    celebrateBirthday: function () {
      age++;
    },
  };
}

var person = createPerson('Alice');

console.log(person.getName()); // Alice
console.log(person.getAge()); // 0
person.celebrateBirthday();
console.log(person.getAge()); // 1

위 예에서 createPerson 함수는 nameage변수를 은닉화한 메서드들을 가진 객체를 반환합니다.

메서드 내부의 클로저들은 이 변수들을 캡쳐하여, 제어된 접근 및 조작을 가능하게 합니다.

즉 저 createPerson 함수 안에 선언된 age 변수를, 외부에서 조작할수 없습니다.

- 렉시컬 스코핑과 변수 접근

정적 스코핑으로도 알려진 렉시컬 스코핑은, 자바스크립트에서 근본적인 원리중 하나입니다.

이 원리는 변수의 스코프가 소스코드 내의 위치에 따라 정의 되는 방식을 결정합니다.

렉시컬 스코핑은 코드의 구조에 따르며, 실행 시점의 흐름이 아니라 코드를 작성한 형태에 기반을 둡니다.

다시 말해서, 함수나 블록 내에서 변수를 선언하면 그 스코프 내서 정의된 모든 중첩된 함수와 블록에서 해당 변수에 접근할수 있게 됩니다.

외부 함수는 또한 그들을 둘러싼 함수들에서 선언된 변수에 접근할 수 있으며, 이렇게 해서 스코프 체인이 형성 됩니다.

- 클로저에서의 렉시컬 스코핑, 정적 스코핑의 역할

클로저는 렉시컬 스코핑을 활용하여 외부(함수 바깥에 있는)스코프에서 변수를 캡쳐합니다.

함수 내부에서 클로저가 정의 될때, 해당 스코프가 종료된 후에도 그것이 생성된 렉시컬 스코프 내의 모든 변수와 매개변수에 대한 접근을 유지합니다.

이 행위는 클로저가 그들의 주변 환경을 기억할수 있도록 해주며, 상태 유지와 데이터와 관련한 은닉화를 위한 강력한 도구로 만들어 줍니다.

- 어떻게 클로저는 변수 접근을 허용하는가 ?

클로저는 외부 스코프의 변수에 접근할 수 있게 하기 위해, 클로저 환경 내의 해당 변수들에 대한 참조를 보존함으로써 작동합니다.

이러한 참조들은 그대로 유지되어, 외부 함수가 실행을 완료했더라도 클로저가 실행 될때마다 변수들이 접근 가능하도록 보장합니다.

한가지 예를 들어보겠습니다.


- ❓ 원글 외의 궁금증, 그렇다면 변수들에 대한 참조를 보존하는 곳은 어디인가 ❓

여기서의 참조를 보존하는 것은, JS 엔진 내부의 메커니즘이다.

클로저가 생성 될때 그 클로저는 자신이 생성된 렉시컬 환경에 있는 모든 변수에 대한 참조를 포함하고 있는 스코프 체인을 포함한다.

이때의 스코프 체인은, 클로저가 정의 된 시점의 스코프를 캡쳐하며 클로저 내부에서 사용되는 외부 변수에 대한 참조를 유지하게 된다.

JS 엔진은 이때 참조되는 외부 변수들을 보존하여 GC의 대상이 되지 않도록 한다.


function outer() {
  var message = 'Hello, ';

  function inner(name) {
    console.log(message + name);
  }

  return inner;
}

var greet = outer(); // 여기서 greet 변수는, inner 함수를 가지고 있습니다.

// 여기서 outer 함수의 실행이 끝났다고 하더라도,
// greet은 여전히 message 변수에 접근 가능합니다.

greet('Alice'); // Hello, Alice

이 예시에서, 내부 함수는 외부 스코프(outer 함수)로부터 message 변수를 캡쳐합니다.

greet("Alice")를 호출 할때, 비록 outer의 실행이 완료 되었음에도 불구하고 greet 내의 클로저는 렉시컬 스코핑 덕분에 message 변수에 대한 접근을 유지합니다.

- 클로저를 만드는것

자바스크립트를 능숙하게 다루는데에 있어서 클로저가 어떻게 생성되는지 이해하는 것은 중요한 단계입니다.

이 섹션에서는 클로저가 어떻게 형성 되는지에 대한 단계별 가이드를 제공하고, 클로저 생성을 보여주는 코드 예제를 제공할 것입니다.

- 어떻게 클로저가 생성 되는지에 대한 단계별 가이드

클로저를 생성하는 것은 한 함수가 다른 함수 내부에서 정의 될때 발생하는 일련의 단계를 포함합니다.

클로저가 생성되는 단계별 가이드는 다음과 같습니다.

  1. 외부 함수 정의하기

외부 함수를 정의하는 것으로 시작합니다.

이 함수는 클로저의 컨테이너 역할을 합니다.

  1. 변수 선언

외부 함수 내에서 클로저가 캡쳐할 변수들을 선언합니다.

이 변수들은 클로저 환경의 일부가 됩니다.

  1. 내부 함수 정의

외부 함수 내부에 클로저가 될 내부 함수를 정의 합니다.

이 내부 함수는 외부 함수의 스코프에 선언된 변수들에 접근할 수 있습니다.

  1. 내부 함수 반환

마지막으로, 외부 함수에서 내부 함수를 반환 합니다.

이 단계는 클로저와 그 주변 환경을 캡쳐하는 반환값이기 때문에 필수적입니다.


- 클로저 생성을 설명하기 위한 코드 예시

function outer() {
  var message = 'Hello from outer!';

  function inner() {
    console.log(message);
  }

  return inner; // 클로저를 생성하여 inner function을 반환합니다.
}

var closureFunction = outer();

// closureFunction 함수를 호출했더라도, 이는 아직도 message 변수에 접근할 수 있습니다.
closureFunction(); // "Hello from outer!"

이 예시에서, outer 함수는 message 변수를 정의 합니다.

그리고 inner 함수는 클로저와 함께 이를 캡쳐합니다.

우리가 outer()함수를 호출할때, 이는 클로저를 생성하며 inner 함수를 반환합니다.

outer 함수의 실행이 끝난 후에도 closureFunctionmessage 변수를 유지하고 있습니다.

- 인자가 포함되는 클로저

function createMultiplier(factor) {
  return function (number) {
    return factor * number;
  };
}

var double = createMultiplier(2);
var triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

이 예시에서, createMultiplier 함수는 factor라는 매개변수를 받아서, 주어진 숫자에 factor를 곱하는 클로저를 반환합니다.

우리가 doubletriple 클로저를 생성할때, 각각의 factor 값을 기억하고 있어서, 그에 따라 숫자들을 곱할 수 있습니다.


- 클로저의 주의사항과 메모리 관리

클로저는 자바스크립트에서 매우 강력하고 다양하게 사용 될 수 있는 기능입니다.

하지만 클로저를 주의 깊게 사용하지 않는다면, 메모리 관련 문제를 일으킬 수 있는 잠재적인 위협이 있습니다.

- 클로저와 관한 잠재적인 메모리 이슈의 논의

클로저는 변수와 주변 환경을 캡쳐하는 방식 때문에, 의도치 않게 메모리 누수를 일으키거나 예상치 못한 메모리 낭비를 초래할 수 있습니다.

주의해야할 몇가지 일반적인 문제는 다음과 같습니다.

1. 실수로 인한 유지

문제 : 클로저는 자신을 둘러싼 외부 스코프를 캡쳐하기 때문에, 변수에 대한 참조를 계속 유지하게 됩니다. 심지어 그 변수들이 더 이상 필요하지 않게 되더라도 유지할 수 있습니다.

결과 : 클로저가 큰 객체를 캡쳐하거나 이유 없이 참조를 계속 유지하게 된다면 메모리 사용량이 증가하게 됩니다.

2. 순환 참조

문제 : 순환 참졸즐 포함하는 객체를 참조하는 클로저는 해당 객체들이 가비지 컬렉션이 되는 것을 방해할 수 있습니다.

결과 : 순환 참조는 본래 정리 되어야할 객체들에 의해 메모리가 점유 되는 메무리 누수를 발생 시킬 수 있습니다.

  • 예시
function createCircularReference() {
  var objectA = { name: 'A' };
  var objectB = { name: 'B' };

  // 서로를 참조하는 순환 참조 생성
  objectA.reference = objectB;
  objectB.reference = objectA;

  return function closure() {
    // 클로저가 objectA와 objectB를 참조
    console.log(objectA.reference.name, objectB.reference.name);
  };
}

var myClosure = createCircularReference();
// 여기서 myClousre는 objectA와 objectB의 참조를 유지하므로 이들은 GC의 대상이 되지 못합니다.

3. 전역 변수

문제 : 클롲러는 의도치 않게 전역 변수를 캡쳐할 수 있으며, 이로 인하여 해당 변수들이 가비지 컬렉션의 대상이 되지 못하게 할수 있습니다.

결과 : 클로저에 의해 의도치 않게 유지되는 전역 변수들은 장기간 메모리 소비를 초래할 수 있습니다.


클로저를 활용한 효과적인 메모리 관리 전략들

클로저를 사용할때 메모리 관련 이슈들을 맞닥뜨리지 않기 위해서, 이러한 전략들을 고려해볼 수 있습니다.

1. 캡쳐되는 데이터를 최소화 하기

클로저에 의해서 캡쳐되는 데이터를 신중하게 선택하세요.

크거나 불필요한 객체의 캡쳐를 피하고 대신에 클로저가 필요로 하는 구체적인 변수들만 캡쳐하세요.

2. 참조 해제하기

클로저 사용이 끝났을때, 명시적으로 그들에 대한 참조를 해제하세요.

클로저를 담고 있는 변수를 null로 설정하거나, 해당하는 경우 이벤트 리스너를 제거하세요.

3. 순환 참조를 피하기

클로저가 순환 참조를 가진 객체를 캡쳐할때 주의하세요.

가능하다면 코드를 재설계해서 순환 참조를 끊거나, 일부 자바스크립트 환경에서 사용 가능한 약한 참조(weak references)를 사용하세요.

- ❓ 원글 외의 궁금증, 그렇다면 약한 참조는 무엇인가 ❓

자바스크립트에서 약한 참조는 대표적으로 WeakMapWeakSet을 예로 들수 있다.

이들은 객체가 생성되더라도 후의 다른 참조가 없다면 가비지 컬렉션의 대상이 되기때문에 메모리 누수를 방지할 수 있다.

let weakMap = new WeakMap();
let obj = {};

// obj를 키로 하여 약한 참조를 생성
weakMap.set(obj, 'some value');

let weakSet = new WeakSet();
let obj = {};

// obj를 WeakSet에 추가
weakSet.add(obj);

4. 임시 스코프를 위한 즉시 실행 함수를 사용.

장기간 변수를 캡쳐할 필요가 없는 단명의 클로저를 위해서, 임시 스코프를 생성하기 위한 즉시 실행 함수를 사용하는 것을 고려해보세요.

5. 클로저를 절약하여 사용하기

클로저는 강력한 기능을 제공하는 대신에, 과도하게 사용하면 메모리 소비가 증가할 수 있습니다.

클로저가 명확한 이점을 제공하는 경우에만 사용하고, 적절할때에 사용하는 것을 고려해보세요.

- 중첩 클로저

중첩 클로저란 클로저 내부 안에 새로운 클로저를 만들수 있도록 해주는 자바스크립트의 강력한 기능입니다.

이번 섹션에서, 우리는 중첩 클로저가 무엇인지, 그들의 사용 경우, 그리고 어떻게 효과적으로 클로저를 중첩할지에 대해서 살펴보겠습니다.

중첩 클로저는 다른 클로저 내부에서 정의 된 클로저를 말합니다.

즉, 바깥 함수와 더 높은 수준의 바깥 함수들로부터 변수를 캡쳐하는 내부 함수 안에 또 다른 함수가 포함되어 있습니다.

이러한 클로저의 중첩은 데이터 캡슐화와 스코프에 대한 복잡하고 세밀한 제어를 가능하게 합니다.

- 중첩 클로저의 사용 사례

중첩 클로저는 특히 다음과 같은 시나리오에서 유용합니다.

1. 비공개 변수 생성

여러 단계의 클로저 내부에서 데이터를 캡슐화하여 변수에 대한 다양한 수준의 접근 제어를 제공할 수 있습니다.

2. 상태 관리

중첩 클로저는 라이브러리 및 모듈과 같은 프로그램의 다양한 수준 내에서 상태를 관리하는데에 도움 됩니다.

3. 커스텀 작동

클로저를 중첩하고 다양한 수준에서 매개변수를 전달할수 있게 하여 맞춤형 행동을 가진 함수를 생성할 수 있습니다.

4. 함수 팩토리 구현

중첩 클로저를 활용하여, 특정 사용 사례에 맞춤화 된 함수를 생성하는 함수 팩토리를 구현할 수 있습니다.

- 예시들

은닉화된 변수들

function outer() {
  var outerVar = '바깥 함수!';

  return function inner() {
    var innerVar = '내부 함수!';
    console.log(outerVar + ' ' + innerVar);
  };
}

var closureFunction = outer();
closureFunction(); // 바깥 함수! 내부 함수!

이 예시에서 outer 함수는 중첩된 클로저를 만들면서 inner 함수를 리턴합니다.

outerVarinnerVar 둘다 캡쳐되므로, 두 수준 모두에서 inner가 변수에 접근할수 있도록 허용해줍니다.

함수 공장

function multiplier(factor) {
  return function (number) {
    return factor * number;
  };
}

var double = multiplier(2);
var triple = multiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

여기서 multiplier 함수는 특정 계수를 가진 곱셈 함수를 생성하기 위한 팩토리입니다.

중첩 클로저는 factor 매개변수를 캡쳐하여, 각각 생성된 함수가 그 계수를 기억할수 있게 합니다.

중첩 클로저는 변수 접근을 제어하고, 다양한 행동을 가진 함수를 생성하는 강력한 방법을 제공합니다.

이 개념을 마스터함으로써, 더 모듈화 되고 맞춤화 된 코드를 작성할 수 있습니다.

- 클로저와 비동기 자바스크립트

클로저는 자바스크립트에서 비동기 작업을 처리하는데에 중요한 역할을 합니다.

이 섹션에서는 비동기 코드에서 클로저가 어떻게 사용되는지 탐구하고, 비동기 시나리오에서 그 사용법을 보여주는 예제를 제공할 것입니다.

- 비동기 작업을 처리하는 데에서의 클로저 사용 방법

자바스크립트는 네트워크 요청을 하는것, 파일을 읽는 것, 사용자 상호작용을 기다리는 것과 비동기 작업을 수행하는 데 자주 사용됩니다.

이러한 시나리오에서 클로저는 비동기 함수가 정의 된 맥락을 보존함으로써 데이터의 무결성과 제어 흐름을 유지하는 데에 도움을 줍니다.

function fetchData(url, callback) {
  setTimeout(function () {
    var data = 'Data from' + url;
    callback(data);
  }, 1000);
}

function processData(data) {
  console.log('Processing data : ', data);
}

fetchData('https://example.com/api/data', processData);

이 예제에서, fetchData는 URL과 콜백 함수를 인자로 받습니다.

fetchData 내부에서 setTimeout 함수가 data 변수를 캡쳐할때 클로저가 생성됩니다.

타임아웃이 완료 될때, 콜백 processData는 클로저 덕분에 여전히 data 변수에 접근할 수 있습니다.

- ❗️ 원글 외의 설명 위 코드의 흐름❗️

  1. fetchData 함수가 호출 될때, 이 안에 setTimeout 안에 있는 콜백함수가 fetchData의 인자인 url을 참조하고 있다.

  2. setTimeout의 시간이 완료 되고, 안에 있는 콜백함수가 실행 될때 이 함수는 여전히 자신이 생성될 때 캡쳐한 url값을 가지고 있기 때문에, data라는 변수가 생성 되고, 이를 processData라는 콜백 함수로 전달한다.

  3. processData는 이 데이터를 받아서 콘솔에 출력한다.


- 이벤트 핸들링 예시

function createCounter() {
  var count = 0;

  function increment() {
    count++;
    console.log('Count : ', count);
  }
  document.getElementById('increment-button').addEventListener('click', increment);
}

createCounter();

createCounter는 클로저를 사용하여 이벤트 리스너를 설정합니다.

increment 버튼이 클릭 되면, createCounter 가 실행을 마친후에도 클로저는 여전히 count 변수에 접근할 수 있습니다.

비동기 코드에서 클로저는 비동기 작업을 처리할 필요한 컨텍스트와 데이터를 유지하는 데에 신뢰할 수 있는 메커니즘을 제공합니다.


- 결론

결론적으로, 클로저와 렉시컬 스코핑을 마스터하는 것은 숙련된 자바스크립트 개발자의 특징일 뿐만 아니라, 견고하고 효율적이며 유지보수가 용이한 코드를 작성하는 길로 안내합니다.

클로저의 힘을 활용함으로써, 복잡한 문제에 대한 우아한 해결책을 창출하고, 현대 웹개발의 끊임없이 변화하는 풍경을 탐색할 수 있도록 합니다.

이제 클로저에 대한 더 깊은 이해를 얻었음으로, 자신감과 창의성을 가지고 자바스크립트의 도전 과제들에 대처할수 있게 됐습니다.


- 번역 글 이외의 클로저 이해

function a() {
  for (var i = 0; i < 5; i++) {
    setTimeout(() => {
      console.log(i);
    }, i * 1000);
  }
}

a();
// 5
// 5
// 5
// 5
// 5

여기서 a()를 실행시키면 5를 5번 출력하게 된다.

왜 그러는걸까 ?

여기서 var 변수는 함수 스코프를 따른다.

var i 변수는 함수 a내에서 선언이 됐기 때문에 a 함수 스코프 내에서 접근이 가능한 지역 변수다.

하지만 var로 선언 됐기 때문에 for 루프의 블록 스코프를 가지고 있지 않다.

즉, 함수 안에서 하나의 스코프를 통해서 단 하나의 i 값이 업데이트 된다.

setTimeout 함수는 비동기적으로 실행 된다.

즉, setTimeout은 호출됨과 동시에 콜스택에 들어가서 바로 실행 되는것이 아니라, Web API로 들어가서 타이머 예약이 진행된다.

하지만 여기서 예약이 진행 될때는 반복문 내의 i 값이 업데이트 될때의 값을 따른다.

1. for문이 1번 반복문을 돌때 (즉, i가 0일때)
setTimeout 함수를 0초후에 실행되도록 예약

2. for문이 2번 반복문을 돌때 (즉, i가 1일때)
setTimeout 함수를 1초후에 실행되도록 예약

3. for문이 3번 반복문을 돌때(즉, i가 2일때)
setTimeout 함수를 2초후에 실행되도록 예약

4. for문이 4번 반복문을 돌때(즉, i가 3일때)
setTimeout 함수를 3초후에 실행되도록 예약

5. for문이 5번 반복문을 돌때(즉, i가 4일때)
setTimeout 함수를 4초후에 실행되도록 예약

6. 그 후에 i++가 다시 발생해서 최종적인 i는 결국 5가 되고 for문은 종료됨

7. 타이머 예약이 진행되었던(Web API를 통해 예약된) setTimeout 함수들이 콜백큐(테스트큐)로 들어감

8. 콜스택이 비어있기때문에 이벤트루프가 콜백큐에 있는 함수를 하나씩 콜스택으로 이동 시킴

9. 하지만 내부 함수인 console.log(i)가 참조하는것은 console.log()함수가 실행 될때의 i 값임 (이때 i는 이미 5)

10. 그렇기 때문에 0, 1, 2, 3, 4초마다 5를 출력함

이러한 순서를 따른다고 볼 수 있다.

이러한 문제점을 해결하기 위해서는 블록 스코프를 따르는 let을 통해 i를 선언해주거나, 해당 함수마다 스코프를 생성해주는 즉시 실행 함수를 사용해줄수 있다.

for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j);
    }, i * 1000);
  })(i);
}

위의 예로 들면, 각 반복문마다 i가 즉시 실행 함수의 j 인자로 들어가서 해당 함수만의 스코프를 생성하게 된다.

그래서 각 함수마다 서로 다른 j값을 참조하게 된다.