[모던 자바스크립트 튜토리얼] 6.3 변수의 유효범위와 클로저

개발견 배도르만·2023년 4월 27일
0
post-thumbnail

변수의 유효범위와 클로저

자바스크립트는 함수 지향 언어이다.

함수 지향 언어란?
~~

코드 블록

코드 블록 {...} 안에서 선언한 변수는 블록 안에서만 사용할 수 있다.

용도는 특정 작업을 수행하는 코드를 한데 묶어두는 것이다.

블록 안엔 작업 수행에만 필요한 변수가 들어간다.

예시:

{
  // 지역 변수를 선언하고 몇 가지 조작을 했지만 그 결과를 밖에서 볼 수 없습니다.

  let message = "안녕하세요."; // 블록 내에서만 변숫값을 얻을 수 있습니다.

  alert(message); // 안녕하세요.
}

alert(message); // ReferenceError: message is not defined

다른 코드블록이라면 let으로 같은 이름을 가진 변수를 선언 가능하다.

{
  // 메시지 출력
  let message = "안녕하세요.";
  alert(message);
}

{
  // 또 다른 메시지 출력
  let message = "안녕히 가세요.";
  alert(message);
}

이미 선언된 변수와 동일한 이름을 가진 변수를 별도의 블록 없이 let으로 선언하면 에러가 발생한다.

// 메시지 출력
let message = "안녕하세요.";
alert(message);

// 또 다른 메시지 출력
let message = "안녕히 가세요."; // SyntaxError: Identifier 'message' has already been declared
alert(message);

if, for, while 등에서도 마찬가지로 {...} 안에서 선언한 변수는 오직 블록 안에서만 접근 가능합니다.

if (true) {
  let phrase = "안녕하세요!";

  alert(phrase); // 안녕하세요!
}

alert(phrase); // ReferenceError: phrase is not defined

if 블록 밖에 있는 alertphrase에 접근할 수 없기 때문에 위 예시를 실행하면 에러가 발생한다.

이런 특징은 변수의 유효 범위를 블록 범위, 특히 if 분기문 범위로 한정시킬 수 있어서 아주 유용하다.

if 뿐만 아니라 for, while 반복문에서도 동일한 특징이 적용된다.

참고로 for문에서 for소괄호 안에서 선언한 변수(let i 등)는 {...} 밖에 있긴 하지만 블록 {...}에 속하는 코드로 취급된다.

중첩 함수

함수 내부에서 선언한 함수는 '중첩(nested)' 함수라고 부른다.

중첩 함수는 새로운 객체의 프로퍼티 형태나 중첩 함수 그 자체로 반환될 수 있다. 이렇게 반환된 중첩 함수는 어디서든 호출해 사용할 수 있다. 물론 이때도 외부 변수에 접근할 수 있다는 사실은 변함없다.

아래 함수 makeCounter는 호출될 때마다 다음 숫자를 반환해주는 ‘카운터’ 함수를 만든다.

function makeCounter() {
  let count = 0;

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

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

만약 makeCounter 함수에 counter를 여러 개 만들었을 때, 이 함수들은 서로 독립적일까?
함수와 중첩 함수 내 count 변수엔 어떤 값이 할당될까?

알아보자.

렉시컬 환경

렉시컬 환경(Lexical Environment)이라 불리는 내부 숨김 연관 객체(internal hidden associated object)가 있다.

자바스크립트에서는 실행 중인 함수, 코드 블록, 스크립트 전체가 렉시컬 환경을 갖는다.

렉시컬 환경은 함수, 코드 블록, 스크립트의 실행 컨텍스트에 따른 정보(변수 정보, 외부 환경 정보)를 저장한다.

렉시컬 환경이 저장하는 정보를 자세히 표현하면 다음과 같다.


1. 환경 레코드(Environment Record)

모든 지역 변수를 프로퍼티로 저장하고 있는 객체이다.
this 값과 같은 기타 정보도 여기에 저장된다.


2. 외부 렉시컬 환경(Outer Lexical Environment) 에 대한 참조

한 단계 상위의 렉시컬 환경에 대한 참조이다.

결론적으로 프로퍼티 변수 저장소 객체 하나, 상위 참조값 하나로 구성된 것이 렉시컬 환경이다.

더 간단하게 표현하면 저장소 하나, 주소 하나를 갖는 것이다. 이 주소를 찾아가면 또다른 저장소와 주소를 가진 객체가 나오는 것이다.

단계 1. 변수

변수’는 특수 내부 객체인 환경 레코드의 프로퍼티일 뿐입니다. '변수를 가져오거나 변경’하는 것은 '환경 레코드의 프로퍼티를 가져오거나 변경’함을 의미합니다.

아래 두 줄짜리 코드엔 렉시컬 환경이 하나만 존재한다.

이렇게 스크립트 전체와 관련된 렉시컬 환경은 전역 렉시컬 환경(global Lexical Environment)이라고 한다.

위 그림에서

  • 네모 상자 : 변수가 저장되는 환경 레코드
  • 붉은 화살표 : 외부 렉시컬 환경에 대한 참조

를 나타낸다.

전역 렉시컬 환경의 외부 참조는 null이다.


코드가 실행되고 실행 컨텍스트가 이어져 나가면서 렉시컬 환경은 변화한다.

코드가 한 줄, 한 줄 실행될 때마다 전역 렉시컬 환경이 어떻게 변화하는지를 보여주는 그림이다.

  1. 스크립트 시작 : 스크립트 내에서 선언한 변수 전체가 렉시컬 환경에 올라간다(pre-populated).
    변수의 상태 - 특수 내부 상태(special internal state)인 'uninitialized'(엔진에 의해 인지O, 참조X)
  2. let phrase : 할당 없이 선언만 하여 프로퍼티 값은 undefined
    phrase는 이 시점 이후부터 사용할 수 있다.
  3. phrase에 값 할당
  4. phrase의 값 변경

요약

변수 : 특수 내부 객체인 환경 레코드프로퍼티
환경 레코드 : 현재 실행 중인 함수와 코드 블록, 스크립트의 변수 저장 공간(객체)
변수 변경 : 환경 레코드의 프로퍼티를 변경하는 것

참고

'렉시컬 환경’은 명세서에서 자바스크립트가 어떻게 동작하는지 설명하는 데 쓰이는 ‘이론상의’ 객체이다. 따라서 코드를 사용해 직접 렉시컬 환경을 얻거나 조작하는 것은 불가능하다.

단계 2. 함수 선언문

함수는 변수와 마찬가지로 값이다.

하지만 변수가 let을 만나 선언이 될 때까지 사용할 수 없는 반면에, 함수 선언문으로 선언한 함수는 바로 초기화되어 선언되기 전의 코드에서도 참조할 수 있다.(함수 표현식은 해당되지 않는다.)

단계 3. 내부와 외부 렉시컬 환경

함수 호출 및 실행새로운 렉시컬 환경이 자동으로 만들어진다.

이 렉시컬 환경엔 함수 호출 시 넘겨받은 매개변수와 함수의 지역 변수가 저장된다.

함수가 호출 중인 동안에 해당 함수를 위한 렉시컬 환경이 새로 만들어진다고 하였다.

하나의 렉시컬 환경은 해당 범위의 한 단계 상위의 외부 렉시컬 환경의 참조를 갖는다.

  • 내부 렉시컬 환경 : 함수 say에 상응. 변수는 인자로 받은 name만 존재
  • 외부 렉시컬 환경 : 전역 렉시컬 환경. 변수는 phrase, say

코드에서 변수에 접근 시 프로세스

  1. 내부 렉시컬 환경을 검색 범위로 잡는다.
  2. 1에서 변수를 찾지 못하면 참조하는 외부 렉시컬 환경으로 범위를 확장한다.
  3. 변수를 찾지 못하면 같은 과정을 전역 렉시컬 환경으로 확장될 때까지 반복한다.

단계 4. 함수를 반환하는 함수

makeCounter 예시로 돌아가 보자.

function makeCounter() {
  let count = 0;

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

let counter = makeCounter();

조금 헷갈리게 생긴 코드이다. 내용은 다음과 같다.

  1. makeCouner() 함수 선언
  2. 변수 countermakeCounter() 할당
  3. makeCounter()가 실행되어 변수 count가 0으로 초기화되고 익명함수를 반환
  4. 반환값이 함수이므로 변수 counter의 값은 makeCounter()의 반환값에 해당하는 익명함수가 됨.

makeCounter()를 호출 시 마찬가지로 함수에 대응하는 렉시컬 환경과 전역 렉시컬 환경이 생성된다.

실행흐름이 makeCounter() 내에서 return문에 도달하면 반환하는 함수에 대한 새로운 렉시컬 환경을 추가적으로 생성한다.

반환하는 함수는 매개변수도, 지역변수도 없기 때문에 환경 레코드가 빈 상태이다.

반환하는 함수 본문에 변수 count에 1을 추가하는 코드가 있다.
참조하는 외부 렉시컬 환경에 변수 count가 선언 및 할당되어 있기 때문에 검색에 성공하면 count의 값이 1 증가해야 한다.

이 때 변수값의 갱신은 변수가 저장된 렉시컬 환경에서 이루어진다.

우리가 이상하게 생각해 볼만 한 부분은 여기에 있다.

코드를 실행시켜 보면 counter() 호출 시마다 count가 1씩 증가하여 0, 1, 2, 3이 표시되는 것을 볼 수 있다.

counter()에 할당된 함수는 count를 반환하고 1 증가시키는 함수이다. 그런데 전역에서 이러한 함수를 사용한다면 변수 count를 어디서 찾는 것인가?
즉, 어떻게 count가 있는 렉시컬 환경을 참조할 수 있는 것인가?

이러한 동작을 가능하게 하는 중요한 이유가 있다.
모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다는 점이다.

함수는 [[Environment]]라는 숨김 프로퍼티를 갖는데, 여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.

counter.[[Environment]]{count: 0}이 있는 렉시컬 환경에 대한 참조가 저장되는 것이다.

이 값은 함수 생성 시 딱 한 번 세팅되고 영원히 변하지 않는다.

가비지 컬렉션

함수 호출 종료 시 함수에 대응하는 렉시컬 환경이 메모리에서 제거된다.
따라서 환경 레코드에 저장된 변수 또한 제거된다.
이러한 이유로 함수 호출 종료 시 관련 변수를 참조할 수 없는 것이며, 모든 객체는 도달 가능한 상태일 때만 메모리에 유지되는 것이다.

그런데 호출 종료 이후에도 여전히 도달 가능한 중첩 함수가 있을 수 있다.

중첩 함수의 [[Environment]] 프로퍼티에 외부 함수 렉시컬 환경에 대한 정보가 저장되어 도달 가능한 상태이기 때문이다.

이러한 유형에서는 중첩 함수를 통한 도달 가능성을 제거해야 이를 감싸는 렉시컬 환경과 그 안의 변수도 메모리에서 삭제된다.

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // g가 살아있는 동안엔 연관 렉시컬 환경도 메모리에 살아있습니다.

g = null; // 도달할 수 없는 상태가 되었으므로 메모리에서 삭제됩니다.

또한, 주의할 점이 있다.

중첩 함수를 여러 번 호출하면 각각의 렉시컬 환경 모두가 메모리에 유지된다는 점이다.

아래에는 3개의 렉시컬 환경이 만들어지는 코드이다.

function f() {
  let value = Math.random();

  return function() { alert(value); };
}

// 배열 안의 세 함수는 각각 f()를 호출할 때 생성된
// 렉시컬 환경과 연관 관계를 맺습니다.
let arr = [f(), f(), f()];

이 경우 또한 렉시컬 환경 제거의 방법은 동일하다.

최적화 프로세스

함수가 살아있는 동안엔 이론상으로 모든 외부 변수 역시 메모리에 유지된다.

그러나 실제로는 자바스크립트 엔진이 이를 지속해서 최적화한다. 변수 사용을 분석하고 외부 변수가 사용되지 않는다고 판단되면 이를 메모리에서 제거하는 것이다.

디버깅 시, 최적화 과정에서 제거된 변수를 사용할 수 없다는 점은 V8 엔진의 주요 부작용이다.

function f() {
  let value = Math.random();

  function g() {
    debugger; // Uncaught ReferenceError: value is not defined가 출력됩니다.
  }

  return g;
}

let g = f();
g();

let value = "이름이 같은 다른 변수";

function f() {
  let value = "가장 가까운 변수";

  function g() {
    debugger; // 콘솔에 alert(value);를 입력하면 '이름이 같은 다른 변수'가 출력됩니다.
  }

  return g;
}

let g = f();
g();
profile
네 발 개발 개

0개의 댓글