본문 바로가기
JavaScript

[JavaScript] 스코프와 비동기적 실행,렉시컬 환경, 클로저

by hotdog7778 2023. 12. 13.

다루는 내용

 - 함수가 외부 변수를 기억하고 유지하는 메커니즘인 클로저
 - 렉시컬 환경과 함수: 함수가 생성된 렉시컬 환경에 대한 참조와 변수 접근
 - 중첩 함수와 렉시컬 환경: 함수 내부에서 선언한 함수의 동작과 렉시컬 환경
 - 중첩 함수로 인해 외부 렉시컬 환경을 참조하게되며 가비지컬렉션이 동작하지 않음(메모리에 유지)

 

 

비동기 동작과 외부 스코프 변수 예제코드 1

function countdown() {
  let i;

  console.log('Countdown:');

  for (i = 5; i >= 0; i--) {
    setTimeout(function () {
      console.log(i === 0 ? 'GO!' : i);
    }, (5 - i) * 1000);
  }
}

countdown();

 

 

위 코드 실행시 5에서부터 카운트 다운을 할 것 같지만

아래와 같은 출력값을 가진다.

 

즉시 Countdown:
즉시 -1 
1초후 -1
1초후 -1
1초후 -1
1초후 -1
1초후 -1

 

이유를 알아보자

 

1. setTimeout 실행

 - setTimeout 함수는 비동기적으로 동작하며, 호출되었을 때 코드의 실행을 차단하지 않고 지정된 시간 후에 콜백 함수를 비동기적으로 실행합니다.
 - for 루프는 동기적으로 실행되지만, setTimeout 함수는 비동기적으로 동작하므로 for 루프가 모두 실행된 후에 비로소 setTimeout 함수의 콜백 함수가 실행됩니다.
 - for 루프의 각 반복에서 setTimeout 함수가 호출되며, 백그라운드에서는 각각의 타이머가 설정되어 대기합니다.
 - 각 setTimeout 함수는 독립적인 스코프를 가지므로, 콜백 함수에서 참조하는 변수는 클로저를 통해 유지됩니다.

 

따라서 모든 setTimeout 함수는 거의 동시에 실행되며, 각각의 대기 시간(0초, 1초, 2초, 3초, 4초, 5초)이 경과한 후에 콜백 함수가 실행

 

 

2. 왜 "-1" 만 출력될까

 - 콜백함수가 실제로 실행되는 시점은 for 루프 완료시점 이후이다.

 - 콜백함수가 실행될때 외부 변수 i 를 참조하게 되면서 클로저가 발생한다.

 - 클로저는 for 루프 밖에서 선언된 변수 i 를 사용할 수 있는 참조값을 가진다. 이로 인해 중첩 함수에서 외부변수 i 를 사용할 수 있다.

 - 위의 예시에서 6번의 반복이 이루어 지며 각각 콜백함수가 실행될 때 마다 클로저를 형성하고 이 클로저는 모두 같은 외부 변수 i 를 참조한다.

 - for 루프 완료 후 i 값은 -1이 되고 모든 콜백 함수들은 클로저에의해 모두 같은 i 값을 사용하게 되는 것 이다.

 

 

요약 하면,

비동기적인 동작 때문에 for 루프가 전부 돌고나서야 setTimeout함수의 콜백 함수들이 실행 될 수 있는 것이며, 클로저로 인해 모든 콜백 함수들이 전부 같은 외부변수 i 를 참조하게 되는것이다.

 

 

※ 클로저

클로저는 함수가 선언될 때의 환경을 기억합니다.

여기서 "환경"은 함수가 정의되었을 때의 스코프(변수, 함수 등이 정의된 위치와 그 범위)
콜백 함수에서 외부 변수 i를 참조할 때, 클로저는 그 변수를 기억합니다. 

클로저는 해당 변수에 대한 참조를 유지하고, 이 참조를 통해 함수가 호출될 때마다 변수의 최신 값을 가져옵니다.

이것이 클로저의 핵심 동작 원리입니다.

 

 

클로저에 대해 조금 더 알아보기 위해 예제코드 2

function makeCounter() {
  let count = 0;

  return function () {
    return count++;
  };
}

let counter = makeCounter();

console.log(counter()); // 0
console.log(counter()); // 1

 

위 코드는 중첩 함수를 실행시킬 때 마다 count 변수값의 증가가 유지된다.

왜 유지될까?

 

결론만 말하자면, 중첩 함수가 반환되면서 클로저가 형성되고, 그로인해 중첩함수가 호출될때 count 값에 대한 참조를 사용할 수 있게되며 이러한 참조는 계속해서 유지되었고 메모리에서 해제되지 않았으며 count값이 업데이트되는것이 유지될 수 있는것이다.

 

순서대로 알아보면

 - makeCounter()이 호출되면 새로운 렉시컬 환경이 생성됩니다.

 - 이 렉시컬 환경은 count과 같은 변수들을 저장하는 곳이며, makeCounter()에 대한 참조를 가지고 있습니다.

 - 중첩함수가 반환됨과 동시에 counter.[[Environment]] 라는 숨김 프로퍼티에 이 렉시컬 환경에 대한 참조를 포함한다.

 - counter() 함수가 호출되면 중첩함수가 실행되고 count 변수를 찾기위해

 - counter.[[Environment]] 프로퍼티에서 렉시컬 환경에 대한 참조를 사용하고 count 변수를 사용할 수 있게 된다.

 - counter.[[Environment]] 프로퍼티에서 렉시컬 환경에 대한 참조를 유지하고 있기 때문에 렉시컬 환경 객체는 메모리에 유지되며 count 변수의 값도 유지될 수 있는 것이다.

 

이를 클로저로 이야하기하면

중첩함수가 반환됨과 동시에 클로저가 형성되며, 이 클로저는 외부 함수인 makeCounter()의 렉시컬 환경에 대한 참조를 포함하고

counter() 함수가 호출되면 클로저를 통해 중첩함수가 실행된다고 할 수 있습니다.

 

 

 

렉시컬 환경

여기서 클로저란 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미합니다.

 

코드, 함수, 변수는 각각의 렉시컬 환경을 가지며, 렉시컬 환경에 정보들이 저장되어 있는 객체라고 생각하면 된다.

렉시컬 환경 객체는 환경 레코드와 외부 렉시컬 환경에 대한 참조로 구성되어 있다.

 

 - 환경 레코드(Environment Record) : 모든 지역 변수를 프로퍼티로 저장하고 있는 객체입니다. this 값과 같은 기타 정보도 여기에 저장됩니다.
 - 외부 렉시컬 환경(Outer Lexical Environment) 에 대한 참조 – 외부 코드와 연관됨

 

’변수’는 특수 내부 객체인 환경 레코드의 프로퍼티이며,

'변수를 가져오거나 변경’하는 것은 '환경 레코드의 프로퍼티를 가져오거나 변경’함을 의미한다.

 

즉, 렉시컬 환경 객체의 Environment Record에 변수값들이 저장되어 있다는 건데

렉시컬 환경 객체는 내부-외부-외부 와 같은 구조로 서로 참조를 하기도 하는것이다.

 

함수를 호출해 실행하면 새로운 렉시컬 환경이 자동으로 만들어집니다.

모든 함수는 함수가 생성된 곳의 렉시컬 환경에 대한 참조를 [[Environment]] 라는 숨김 프로퍼티에 저장

 

 

 

 

** 렉시컬 환경과 클로저에 대해서도 포스팅 하고 싶지만 글이 길어질것 같아 좀더 학습 후에 실행컨텍스트, 렉시컬 환경, 클로저 에 대해 포스팅 하기