본문 바로가기

JavaScript

20210707 JavaSciprt DeepDive 06 : 실행 컨텍스트(동작방식), 소스코드, 렉시컬 환경, 클로저, 클로저 활용, 캡슐화, 정보은닉, 접근제한자

JavaScript Deep Dive 06


용어 및 중요사항 정리


실행 컨텍스트


실행 컨텍스트의 구조를 이해하려면 해당 책의 그림을 보는게 빠름


  • 소스코드: 실행 가능한 코드로 실행 컨텍스트를 생성하는 역할
  • 소스코드의 타입
    • 전역 코드 : 전역에 존재하는 소스코드, 전역에 정의된 함수, 클래스 등의 내부코드는 포함되지 않음
      • 전역 코드에서 실행 컨텍스트 역할
        • 전역 스코프 생성 : 전역 변수 관리
        • 전역 객체와 연결 : 전역 변수, 전역 함수를 전역 객체와 연결
    • 함수 코드 : 함수 내부에 존재하는 소스코드, 함수 내부 중첩함수, 클래스 등의 내부 코드는 포함 되지 않음
      • 함수 코드에서 실행 컨텍스트 역할
        • 지역 스코프 생성 : 지역변수, 매개변수, arguments 객체 관리
        • 전역 스코프와 연결 : 스코프 체인을 연결하기 위해서
    • eval 코드 : 빌트인 전역함수인 eval 함수에 인수로 전달되어 실행되는 소스코드를 말함
      • strict mode에서 독자적인 스코프 생성
    • 모듈 코드 : 모듈 내부에 존재하는 소스코드, 모듈 내부의 함수, 클래스 등의 내부 코드는 포함 되지 않음
      • 모듈별 독립적인 모듈 스코프를 생성

  • 소스코드의 평가 :
    • 실행 컨텍스트 생성 -> 변수, 함수 등의 선언문 먼저 실행 -> 생성된 변수, 함수 식별자를 키로 실행컨텍스트가 관리하는 스코프에 등록
  • 소스코드의 실행 :
    • 선언문을 제외한 소스코드가 순차적으로 실행 (런타임 시작)
    • 실행에 필요한 변수나 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 검색하여 취득
    • 실행 결과는 실행 컨텍스트가 관리하는 스코프에 등록

  • JS엔진의 소스코드 평가와 실행 과정 :

    • 전역 코드 평가 :
      • 전역 실행 컨텍스트 생성 -> 실행 컨텍스트 스택에 push -> 전역 코드의 선언문들만 먼저 실행 -> 전역변수,전역함수가 전역 스코프에 등록
    • 전역 코드 실행 :
      • 런타임 시작 -> 전역 코드 순차적 실행 -> 전역 변수에 값할당, 및 함수 호출 -> 함수 호출시 전역 코드 실행 일시 중단 -(실행 순서 변경)-> 함수 내부 진입
    • 함수 코드 평가 :
      • 함수 실행 컨텍스트 생성 -> 실행 컨텍스트 스택에 push -> 함수 내부 매개변수, 지역 변수 선언문 실행 -> 매개변수, 지역변수, arguments 객체 지역 스코프에 등록, this 바인딩 결정
    • 함수 코드 실행 :
      • 런타임 시작 -> 함수 코드 순차적 실행 -> 매개변수, 지역변수 값 할당, 메서드 호출(식별자 스코프체인 검색, 프로퍼티 프로토타입 체인 검색) -> 실행 종료 -> 중단된 위치의 전역 코드 진입
    • 전역 코드로 복귀 :
      • 함수 실행 컨텍스트를 실행 컨텍스트 스택에서 pop하여 제거 -> 전역 실행 컨텍스트도 pop하여 제거

  • 실행 컨텍스트: 소스코드를 실행하는 데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역 (소스코드가 평가될 때 생성됨)
    • 모든 식별자를 스코프를 구분하여 등록하고 식별자에 바인딩된 값의 변화를 관리
    • 스코프 체인을 통해 상위 스코프로 이동하며 식별자 검색할 수 있게 관리
    • 코드 실행 순서 관리(함수 호출 발생시 현재 실행중인 코드는 중단되고 내부로 함수 내부로 진입하기 때문에)
      • LexicalEnvironment 컴포넌트 : 렉시컬 환경을 참조
      • VariableEnvironment 컴포넌트 : 렉시컬 환경을 참조

  • 렉시컬 환경 : 식별자와 스코프 관리하는 환경
    • 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조로 실행 컨텍스트를 구성하는 컴포넌트임
    • 키와 값을 갖는 객체 형태의 스코프를 생성하여 식별자를 키로 등록하고 식별자에 바인딩된 값을 관리 (식별자 - 바인딩 값)
      • 환경 레코드(EnvironmentRecord)
        • 스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소, 소스코드의 타입에 따라 관리하는 내용에 차이가 있음
      • 외부 렉시컬 환경에 대한 참조(OuterLexicalEnvironmentReference)
        • 상위 스코프(외부 렉시컬 환경 -> 실행컨텍스트를 생성한 소스코드를 포함하는 상위 코드의 렉시컬 환경)를 가리킴으로서 스코프 체인을 구현함

  • 실행 컨텍스트 스택 : 코드 실행순서를 관리하는 환경
    • 생성된 실행 컨텍스트를 스택 자료구조로 관리하는 환경
    • 실행 컨텍스트가 추가, 제거 되면서 관리됨
    • 실행 중인 실행 컨텍스트 : 실행 컨텍스트 스택 최상위 실행 컨텍스트는 언제나 실행 중인 코드의 실행 컨텍스트를 말함



실행 컨텍스트의 생성과 식별자 검색 과정


  • 전역 객체 생성 : 전역 코드 평가되기 이전에 생성 (빌트인 전역프로퍼티, 빌트인 전역 함수, 표준 빌트인 객체, 호스트 객체)

  • 전역 코드 평가 : 소스코드가 로드되면 JS엔진이 전역 코드를 평가함
    • 전역 실행 컨텍스트 생성
      • -> 실행 컨텍스트 스택에 푸시 (실행 중인 실행 컨텍스트)
    • 전역 렉시컬 환경 생성 : 전역 실행 컨텍스트에 바인딩
      • 전역 환경 레코드 생성 : 전역 스코프, 전역 객체의 빌트인 전역프로퍼티, 빌트인 전역함수, 표준 빌트인 객체를 제공
        • 객체 환경 레코드 생성 :
          • var으로 선언한 전역변수 관리, 함수 선언문, 빌트인 전역 프로퍼터, 빌트인 전역 함수, 표준 빌트인 객체 관리
          • BindingObject를 통해서 전역객체(window)의 프로퍼티와 메서드가 됨
            • var로 선언한 변수의 경우 undefined로 할당(초기화) 되어 전역객체로 전달 -> 변수 호이스팅
            • 함수 선언문으로 정의한 함수의 경우 생성된 함수 객체 생성 및 즉시 할당(초기화)되어 전역 객체로 전달 -> 함수 호이스팅
        • 선언적 환경 레코드 생성 :
          • const, let으로 선언한 전역 변수 관리
          • 전역객체의 프로퍼티가 되지 않음
          • 선언시 undefined가 할당(초기화)되지 않음으로 일시적 사각지대(TDZ) 현상 발생
        • this 바인딩 : [[GlobalThisValue]]라는 내부 슬롯에 this가 바인딩 됨
          • 현재는 전역객체가 바인딩 됨
          • 전역 환경 레코드, 함수 환경 레코드에만 존재
      • 외부 렉시컬 환경에 대한 참조 결정 : 현재 평가 중인 소스코드를 포함하는 외부 소스코드의 렉시컬 환경으로 상위 스코프를 가리킴 -> 스코프 체인 구현
        • 전역코드를 포함하는 소스코드는 없음으로 null

  • 전역 코드 실행 : 전역 코드가 순차적으로 실행
    • 할당문, 함수 호출문 실행 단계 -> 실행중인 실행 컨텍스트에서 식별자 검색 - (찾지 못하면 상위스코프에서 검색) -> 식별자 결정 -> 검색된 식별자에 값 바인딩

  • 함수 코드 평가 : 함수호출되어 전역 코드 실행을 일시 정지하고 함수 코드를 평가하기 시작함

    • 함수 실행 컨텍스트 생성
      • 함수 렉시컬 환경 완성후 실행 컨텍스트 스택에 push (실행 중인 실행 컨텍스트가 됨)
    • 함수 렉시컬 환경 생성 : 생성후 함수 실행 컨텍스트에 바인딩 됨
      • 함수 환경 레코드 생성 : 매개변수, arguments객체, 함수 내부에서 선언한 지역 변수와 중첩 함수를 등록하고 관리
      • this 바인딩 : 함수 환경 레코드의 [[ThisValue]] 내부슬롯에 this가 바인딩 됨
        • 함수 호출 방식에 따라 달라짐
      • 외부 렉시컬 환경에 대한 참조 결정 : 함수 정의가 평가된 시점에 실행중인 실행 컨텍스트의 렉시컬 환경의 참조가 할당됨 (상위 스코프는 어디에서 정의했는가가 결정함, 어디서 호출이 아니라)
        • 함수 정의 평가하여 함수객체 생성시 함수의 상위 스코프를 함수 객체 내부 슬롯 [[Environment]]에 저장, 이것을 외부 렉시컬 환경에 대한 참조가 가르키게 됨

  • 함수 코드 실행 : 런타임이 시작되어 함수의 소스코드가 순차적으로 실행
    • 매개변수에 인수가 할당되고, 변수 할당문이 실행되어 지역 변수에 값이 할당
    • 할당문, 함수 호출문 실행 단계 -> 실행중인 실행 컨텍스트에서 식별자 검색 - (찾지 못하면 상위스코프에서 검색) -> 식별자 결정 -> 검색된 식별자에 값 바인딩

  • 객체.메서드 실행 :
    • 실행중인 실행 컨텍스트의 해당 렉시컬 환경에서 객체의 식별자 검색 시작
    • 식별자가 없으면, 외부 렉시컬 환경에 대한 참조가 가르키는 상위 스코프로 이동하여 식별자 검색
    • 식별자를 찾으면 해당 식별자에 바인딩된 객체에서 프로토타입 체인을 통해 메서드를 검색
    • 메서드를 찾았으면, 메서드에 전달하는 인수인 표현식을 평가하기 위해 변수 식별자를 스코프 체인에서 검색
    • 해당 식별자에 바인딩 된 값을 전달하여 표현식을 평가하여 만들어진 값을 메서드에 전달하여 호출

  • 함수 코드 실행 종료 : 더이상 실행할 코드가 없으므로 함수 코드는 실행 종료
    • 실행 컨텍스트 스택에서 함수 실행 컨텍스트가 pop되어 제거
      • 만약, 함수 렉시컬 환경을 누가 참조하고 있다면, 함수 렉시컬 환경은 소멸하지 않음, 아무도 참조하지 않아야 가비지 컬렉터에 의해 소멸(실행 컨텍스트와 렉시컬 환경은 독립적이기 때문)
    • 실행중인 실행 컨텍스트는 전역 코드인 전역 실행 컨텍스트가 됨

  • 전역 코드 실행 종료 : 더이상 전역 코드가 없으면 실행 종료되고, 전역 실행 컨텍스트도 실행 컨텍스트 스택에서 pop되어 제거됨



실행 컨텍스트와 블록 레벨 스코프


  • if문, for문, while문, try/catch 문등의 코드 블록을 지역 스코프로 인정하는 let, const는 블록 레벨 스코프를 따름
  • 해당 블록레벨 스코프(if, for, while 등...)가 실행되면 실행 컨텍스트의 경우 함수 처럼 실행 컨텍스트를 쌓는게 아닌, 해당 실행중인 컨텍스트에서 새로운 렉시컬 환경을 생성하여 교체함
  • 새롭게 생성한 렉시컬 환경은 선언적 환경 레코드와 외부 렉시컬 환경 참조로 구성됨
    • 지역변수는 선언적 환경 레코드에 저장
    • 외부 렉시컬 환경 참조는 이전에 있던 렉시컬 환경을 가르킴
  • 해당 블록의 실행이 끝나면 이전에 있던 렉시컬 환경으로 교체됨
  • for문의 경우 해당 코드가 반복될 때 마다 독립적인 렉시컬 환경을 생성하여 식별자의 값을 유지함




클로저


  • 렉시컬 스코프 : 함수를 어디에 정의 했는지에 따라 상위 스코프를 결정함

    • 렉시컬 환경의 '외부 렉시컬 환경에 대한 참조'에 저장할 참조값
  • 함수 객체의 내부 슬롯[[Environment]] : 함수객체가 가진 내부 슬롯으로 함수가 정의된 렉시컬 환경을 저장하고 있음

    • 상위 스코프 => [[Environment]] => 외부 렉시컬 환경에 대한 참조
  • 클로저 :

    • 자바스크립트 교유 개념이 아닌 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 특성임
    • 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합
    • 자유변수에 묶여있는 함수
      • 자유변수 : 클로저에 의해 참조되는 상위 스코프의 변수
    • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩함수는 이미 생명주기가 종료한 외부 함수의 변수를 참조할수 있는 중첩함수를 말함
      • 조건01: 외부 함수보다 중첩 함수가 더 오래 유지되어야 한다.
      • 조건02: 중첩함수가 외부 함수(상위 스코프)의 변수를 참조하고 있어야 한다. (디버깅시 클로저로 인식하긴 함)
      • 위 2가지 조건을 만족해야, 클로저 임
      • 브라우저 또는 JS엔진은 최적화를 통해서 참조하고 있는 식별자만 기억하고 참조하지 않으면, 메모리 낭비이므로 기억하지 않음

const x = 1;

function outer() {
  const x = 10;
  const inner = function () {
    console.log(x);
  };
  return inner;
}

const innerFunc = outer(); // outer함수가 종료 되어 실행 컨텍스트가 사라졌음에도, 안에 있는 중첩함수(inner)가 외부 함수의 지역변수를 참조함
innerFunc(); // 10

  • 클로저가 외부 함수의 변수를 기억하는 이유
    • 외부 함수 종료시 해당 외부함수의 실행 컨텍스트는 제거되지만, 외부함수의 렉시컬 환경은 사라지지 않음 (참조 되고 있기 때문에)
      • 외부함수 렉시컬 환경의 함수 환경 레코드에 저장된 중첩함수 객체의 [[Environment]] 슬롯이 외부함수 렉시컬 환경을 참조하고 있고,
      • 중첩함수 객체는 외부함수가 종료된 전역 렉시컬 환경의 선언적 환경 레코드에 변수에 의해 참조 되고 있음
    • 전역 렉시컬 환경에서 중첩 함수를 호출하게 되면, 중첩함수 실행 컨텍스트가 생성되면서 실행 컨텍스트 스텍에 올라가고
      • 중첩함수 렉시컬 환경이 생성
      • 외부 렉시컬 환경에 대한 참조 컴포넌트에 중첩 함수객체가 가지고 있던 [[Environment]] 내부 슬롯의 값이 할당되어 외부함수 렉시컬 환경과 연결됨
    • 그리하여, 모든 렉시컬 환경이 연결되어 외부함수 변수를 변경할 수 있음
    • 중요 핵심 :
      • 중첩함수의 렉시컬 환경이 사라지지 않는다는 것
      • 중첩함수가 호출될 때 중첩함수 렉시컬 환경과 다른 렉시컬 환경이 모두 연결된다는 것

클로저의 활용


  • 상태를 안전하게 변경하고 유지하기 위해 사용함
  • 상태를 안전하게 은닉하여 특정 함수에게만 상태 변경을 허용하기 위해 사용됨



  • 객체 리터럴 방식의 클로저
    • counter 객체의 increase, decrease 메서드로만 변경 가능
    • 즉시 실행 함수 스코프의 렉서스 환경에 있는 num 변수는 은닉됨
    • 즉시 실행 함수의 num 변수를 참조하고 있는 클로저 increase, decrease
    • 클로저를 가르키고 있는 전역 변수 counter
const counter = (function () {
  let num = 0;

  return {
    // 객체 리터럴 중괄호는 코드 블럭이 아님 -> 별도 스코프 X
    increase() {
      return ++num;
    },
    decrease() {
      return num > 0 ? --num : 0;
    },
  };
})();

console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0



  • 생성자 함수 방식의 클로저
    • 즉시 실행 함수 스코프의 렉서스 환경에 있는 num 변수는 은닉됨
    • Counter 생성자 함수에 의해 만들어진 인스턴스의 increase, decrease 메서드로만 변경 가능
    • 즉시 실행 함수의 num 변수를 참조하고 있는 increase, decrease 클로저는 Counter 생성자 함수의 프로토타입으로 지정되어 Counter 생성자로 생성된 인스턴스는 이를 프로토타입의 클로저를 참조하여 사용할 수 있음
    • Counter 생성자 함수를 가르키는 Counter 전역 변수
    • Counter 생성자 함수로 만들어진 인스턴스 counter
    • 인스턴스인 counter를 통해 클로져를 참조하여 사용 가능함
const Counter = (function () {
  let num = 0;

  function Counter() {}

  Counter.prototype.increase = function () {
    return ++num;
  };
  Counter.prototype.decrease = function () {
    return num > 0 ? --num : 0;
  };

  return Counter;
})();

const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0



  • 고차함수를 활용한 클로저01: 독립된 환경 클로저
    • 고차함수는 함수를 인수로 받아서 사용하는 함수
    • 여기서 만들어지는 클로저는 고차함수의 매개변수와 지역변수를 기억함
    • 해당 클로저는 makeCounter 고차함수 렉시컬 환경을 상위 스코프로 기억함
    • 그리고 makeCounter 고차함수가 호출되어 클로저가 밖으로 나오면서 활용할 수 있게 됨
    • 특징은, 고차함수가 호출될 때마다 새로운 실행 컨텍스트가 생성되고, 사라지면서 정작 렉시컬 환경은 사라지지 않아 계속 새로운 독립된 환경이 만들어 짐
// 일반 함수 형태에 하나의 함수 인수를 넣는 형태라서
// 자유 변수를 변형하는 함수는 클로저에 고정되어 하나 밖에 존재하지 않게됨
function makeCounter(predicate) {
  let counter = 0;

  // 클로저 반환
  // 클로저가 고차함수가 아니기 때문에 이미 상위 스코프에서 전달한 함수만 사용하게
  // 클로저가 하는 일이 정해져 있음
  return function () {
    counter = predicate(counter);
    return counter;
  };
}

// 보조함수
function increase(n) {
  return ++n;
}
function decrease(n) {
  return --n;
}

// 클로저1 - 독립된 환경1 num
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// 클로저2 - 독립된 환경2 num
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2



  • 고차함수를 활용한 클로저02: 공유된 환경 클로저
    • return 되는 클로저가 함수를 받아 사용할 수 있게 하는 고차함수 구조로 만들어서 클로저에 동적으로 함수들이 사용되면서 즉시 실행 함수의 렉시컬 환경을 공유할 수 있게함
const counter = (function () {
  let counter = 0;

  // 클로저 반환
  // 클로저 자체에 인수를 받음으로서 동적으로 다양한 함수가 상위 스코프의 환경을 공유하게 됨
  return function (predicate) {
    counter = predicate(counter);
    return counter;
  };
})();

// 보조함수
function increase(n) {
  return ++n;
}
function decrease(n) {
  return --n;
}

// 자유 변수를 공유함
console.log(counter(increase)); // 1
console.log(counter(increase)); // 2

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



  • 캡슐화와 정보 은닉
    • 캡슐화 : 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것으로 정보 은닉을 위해서 사용하기도 함
    • 정보 은닉 : 외부에 공개할 필요가 없는 구현의 일부를 감추어 적절치 못한 접근으로 부터 객체의 상태가 변경되는 것을 방지해 정보 보호, 객체간 상호 의존성을 낮추는 효과가 있음
    • 접근 제한자 : 다른 객체지향 프로그래밍 언어의 클래스를 구성하는 멤버(프로퍼티와 메서드)에 대하여 public, private, protected 같은 접근 제한자를 통해 공개 범위를 한정 할 수 있음
      • JS는 접근 제한자를 제공하지 않아 객체의 모든 프로퍼티와 메서드는 public으로 공개 되어 있음
      • 대신 생성자 함수의 지역변수를 활용해서 private를 구현하여 인스턴스가 사용할수 없게 함 (this가 없는 변수)
      • 메서드 중복을 피하기 위해서 prototype에 메서드를 할당하여 저장하는데, 이렇게 되면 생성자 함수의 지역변수를 메서드가 변경하지 못하게 됨 -> 클로져를 활용해서 해결

  • 프로토타입 메서드 방식의 클로저 문제점
const Person = (function () {
  let _age = 0;

  // 클로저인 생성자 함수
  function Person(name, age) {
    // 인스턴스의 프로퍼티
    this.name = name;
    // 생성자 함수의 지역변수
    _age = age;
  }

  // 클로저인 프로토타입 메서드
  // 프로토타입에 메서드 설정(중복 제거)
  Person.prototype.sayHi = function () {
    console.log(`Hi, I'm ${this.name} and ${_age}`);
  };

  // 클로저 return
  return Person;
})();

const me = new Person("kim", 25);
me.sayHi(); // Hi, I'm kim and 25
console.log(me.name); // kim
console.log(me._age); // undefined (생성자 함수의 지역변수 이므로)

const you = new Person("park", 30);
you.sayHi(); // Hi, I'm park and 30

// 문제 발생) 생성자 함수의 지역변수가 프로토타입 메서드에 의해서 공유되어 변경됨
// 프로토타입 메서드의 경우 단한번 생성되는 클로저라서 상위 스코프가 한번만 만들어지며, 모든 인스턴스가 공유하기 때문임 -> 아직까지
me.sayHi(); // Hi, I'm kim and 30

  • 자주 발생하는 실수
    • 함수레벨 스코프와 블록레벨 스코프를 생각하지 못하고 var를 사용하는 경우
      • for의 변수선언문에서 var를 사용하게 되면 함수레벨 스코프라는 것을 기억하고, 클로저의 스코프가 어떻게 결정되는지 잘 확인해야 한다. (자유 변수의 스코프를 잘 확인할 것, 전역 스코프인 경우에는 언제든지 변경이 됨으로)
      • 이때, 즉시 실행 함수로 묶어 즉시 실행 함수를 클로저의 스코프로 만들어 독립된 스코프를 활용하게 해야 함
      • 아니면, let을 사용할 것
    • let, const 키워드를 사용하는 반복문은 코드 블록을 반복 실행할 때마다 새로운 렉시컬 환경을 생성하여 반복할 당시의 상태를 스냅샷을 찍는 것 처럼 저장함
    • 함수를 정의하는 코드가 반복문에 있을 때 의미가 있음, 함수 정의가 없는 반복문은 실행 종료후 가비지 컬렉터에 의해 없어짐