Javascript

JS가 console.log('hello world')를 하는 방법

변기원 2023. 7. 30. 18:31

목적: 자바스크립트에 대해 공부하고 있는 주니어 개발자분들과 공부한 내용을 공유한다.

 

자바스크립트가 console.log()를 하는 과정을 설명하면서 여러가지 개념을 복습할 수 있을 것이다.

아래는 배운내용을 복습하기 위해 조금은 말도 안 되는..? 예제를 작성해 봤다.

var t = 10;
let u = 20;
function outerFunc (a) {
  var x = 1;
  var y = 2;
  const innerFunc = () => {
    const z = 3;
    return (x + y + z)
  }
  return innerFunc;
}

const innerRun = outerFunc();
  
const newConsole = new SuperConsole()

newConsole.log(innerRun())

1. 전역객체가 생성된다.

2. 전역 코드가 평가된다. 실행컨텍스트가 생성되고 전역 실행컨텍스트와 연결된 렉시컬 환경이 생성되어 컨텍스트 스택에 푸시된다. 그리고 var로 선언된 변수와 함수 선언문으로 선언된 함수가 정의된다. 변수 t, u와 outerFunc함수 모두 평가시점에 선언된다.

단 var로 선언된 변수는 선언과 초기화가 동시에 이루어지고, let으로 선언된 변수는 초기화가 실행시점에 이루어진다. 

랙시컬 환경의 환경레코드에 t, u와 outerFunc이 선언된다. 변수 t의 경우에는 undefined로 선언 즉시 초기화된다. outerFunc은 즉시 평가되어 함수객체로 초기화된다. 전역에 var로 선언된 변수와 함수선언문은 전역객체의 변수와 메서드가 된다. 그래서 t와 outerFunc은 객체환경 레코드에 등록되고 변수 u는 let으로 선언되었기 때문에 선언적 환경레코드에 선언된다. var와 달리 let은 선언과 즉시에 초기화되지 않는다. 아무 값도 없는 상태가 된다. 이러한 차이 때문에 런타임이 실행될 때 undefined로 초기화가 완료된 t는 호이스팅 되어 할당문보다 먼저 참조했을 때 undefined가 반환하게 되고, let이나 const로 선언된 변수는 참조 에러를 반환하게 된다. 즉, let이나 const로 선언한 변수, 함수 표현식으로 선언한 함수는 런타임에 값이 할당되기 전까지는 에러가 발생하게 된다. 이것을 TDZ이라고 한다.

아래쪽에 있는 innerRun 변수도 선언적 환경레코드에 등록되고 런타임 전까지는 아무 값도 할당되지 않는다.

초기화 전에는 변수를 참조할 수 없다는 에러가 친절하게 나온다.

3. 전역코드가 실행된다(런타임). 변수 t에 드디어 10이 할당되고 u에는 20이 할당된다. 그리고 함수 선언문으로 이미 평가시점에 완료된 outerFunc을 지나서 const innerRun = outerFunc()에 도달한다. 이번에는 outerFunc을 실행하기 위해 코드 실행의 주도권을 넘겨준다. 

4. outerFunc이 실행되기 전에 outerFunc의 실행컨텍스트와 렉시컬환경이 연결되고 실행컨텍스트 스택에 outerFunc의 실행컨텍스트가 푸시되어 실행 중인 실행컨텍스트가 된다. 이 함수 실행컨텍스트의 환경레코드에는 매개변수, 지역변수, 내부함수가 선언된다. x, y, innerFunc이 선언된다. x, y는 var로 선언되어 모두 undefined로 초기화되고 매개변수 a도 undefined로 초기화된다. innerFunc은 함수 표현식이기 때문에 변수가 선언되는 것과 똑같이 동작한다. innerFunc이라는 변수가 선언되고 const이기 때문에 초기화되지 않고 아무 값도 할당되지 않는다. 

5. outerFunc이 실행되면 드디어 값이 할당된다. x, y에 각각 1,2가 할당되고 화살표함수로 정의된 함수가 평가되어 innerFunc이라는 변수에 할당된다. 그리고 이 함수는 innerFunc 함수객체가 할당된 변수를 반환하고 종료된다.

6. 이제 innerRun변수에는 innerFunc이 할당된 상태가 되고 다시 전역실행컨텍스트가 실행 중인 실행컨텍스트가 된다. 다음줄로 내려와서 innerRun을 실행한다.

7. innerRun이 실행되면 outerFunc이 반환했던 innerFunc이 실행된다.

8. innerFunc이 실행되기 전에 평가된다. 평가시점에 innerFunc 함수의 실행컨텍스트와 렉시컬환경이 만들어지고 지역변수 z가 선언된다. 초기화는 런타임에 한다. 위에서 설명하지 않은 부분이 있는데 실행컨텍스트는 렉시컬환경 컴포넌트에 환경레코드 외에 외부환경참조값을 가진다. 외부환경참조란 해당 함수의 실행컨텍스트가 평가되기 전에 실행되던, 현재 평가 중인 소스코드를 포함하는 외부 소스코드의 렉시컬 환경, 즉 상위 스코프를 가리킨다. innerFunc의 외부환경 참조는 innerFunc의 상위 스코프를 가지는데 그것은 outerFunc의 렉시컬환경이 된다. 즉, innerFunc은 outerFunc의 렉시컬환경을 참조하기 때문에 outerFunc의 렉시컬환경은 가비지컬렉터에 의해 삭제되지 않고 존재한다. outerFunc의 실행컨텍스트는 이미 끝나서 스택에서 사라졌지만 여전히 innerFunc에 의해 참조되므로 outerFunc의 렉시컬환경은 실행컨텍스트와 상관없이 존재한다. 렉시컬환경이 실행컨텍스트와 연결되기는 하지만 따로 존재한다는 것이 바로 이 뜻이다. 그러면 innerFunc은 이미 종료된 함수의 렉시컬환경에 접근하고 값을 변경할 수도 있는 상태가 된다. 이런 함수를 클로저라고 한다. 

outerFunc은 클로저를 반환하는 함수고 이 클로저는 innerRun변수에 할당되어 실행할 수 있는 상태가 된다.

9. InnerFunc은 x, y, z를 찾기 위해 현재 실행컨텍스트의 렉시컬환경 환경레코드를 찾는다. z는 현재 실행 중인 실행컨텍스트의 환경레코드에서 찾을 수 있다. x, y를 찾기 위해 외부환경참조를 따라간다. 해당 함수를 클로저이므로 외부환경참조에 이미 실행이 종료된 outerFunc의 렉시컬환경을 참조하고 있으므로 해당 스코프에서 x, y값을 찾는다. 이것을 스코프체인이라고 한다. 

10.  innerRun이 1+2+3을 반환하면 드디어 newConsole이 SuperConsole이라는 생성자 함수의 인스턴스로 생성된다. 프로토타입체인을 설명하기 위해 console기능에 특별한 메서드를 추가한 생성자 함수라고 생각하자..

function SuperConsole(){
    this.v = 10;
}
SuperConsole.prototype = window.console

newConsole인스턴스는 log를 실행하기 위해 자신의 메서드를 찾는다. 하지만 newConsole인스턴스는 v= 10이라는 프로퍼티밖에 없다. 그래서 프로토타입을 찾는다. newConsole.__proto__는 SuperConsole의 prototype이다. 이곳에도 log메서드가 없다. 그러면 다시 SuperConsole.prototype.__proto__를 찾아간다. SuperConsole.prototype.__proto__는 window.console이다.

window.console에는 log라는 메서드가 있다. log를 상속받아서 인스턴스 자신의 메서드인 것처럼 호출한다. 이것을 프로토타입 체인이라고 한다. 즉, 식별자와 프로퍼티 메서드를 찾기 위해 스코프체인과 프로토타입체인이 협력하고 있다.

위 과정을 거쳐 드디어 6을 출력할 수 있게 되었다.

 

공부하는 과정이라 혹시 틀린 내용이 있을 수 있습니다! 틀린부분에 대해 조언해 주시면 감사히 배우겠습니다!