그 동안 자바스크립트 공부를 하면서 명확하게 잡히지 않았던 것들만 골라 경험담과 함께 시리즈로 정리하려한다. 카페트 밑의 먼지 마냥 급하진 않았지만 거슬리는 기본적 개념, 근본적인 동작에 대한 갈망을 이번 시리즈로 해결하고자 한다.

SCOPE

let, const 같은 키워드는 ES의 패러다임 바꾼다. 좋다고 하니까, 일단 let을 쓰라는 가이드만 따르다보니 let을 쓰지 않음으로서 생기는 오류를 접할 기회가 없었다. 결국 [지역, 전역] 그리고 [var, let, const]의 차이와 구체적인 동작을 명확히 모른채 약간 혼란스러운 상태에 있었다. JSLint 를 쓰면 문제를 바로 지적해주니 당장 문제는 회피할 수 있지만 도대체 무슨일이 벌어지는지, 어디에서 어디로 프레임이 옮겨 갔는지 알아야 한다고 생각한다.

분석 Flow

let letInGlobal = 'letInGlobal';  // (1)
function test() {
  console.log(letInGlobal, 'letInGlobal 입니다'); // (1)

  var varInLocal = 'varInLocal 입니다.';         // (2)
  const constInLocal = 'localConst 입니다.';     // (2)

  noVar = '함수안에서 지역변수가 되지 않고 전역변수가 된 noVar 입니다.'; // (3)
  debugger;
};
console.log(varInLocal, "varInLocal 출력!")       // (2) => ERROR varInLocal is not defined   (2)
console.log(constInLocal, "constInLocal 출력!");  // (2) => ERROR constInLocal is not defined (2)

console.log(noVar);         // => ERROR noVar is not defined  (3)
test();                     // 'letInGlobal 입니다'            (4)
console.log(noVar);         // 'noVar 입니다.'                 (4)

// var, let, const 같은 키워드로 선언되지 않고 바로 쓰는 변수들은 항상 전역변수
// 오작동의 원인이기 때문에 이를 방지하기 위한 strict 모드에선 에러를 일으킴

(1) ~ (4) 이슈로 쪼개어서 앞으로 설명

ISSUE (1) 지역변수와 전역변수

let을 만나고 내가 헛갈린건 대다수 언어가 ‘블록단위 스코프’를 쓰기 때문이다. 자바는 전역을 쓰고 싶다면 그냥 static 을 붙인다. 그덕에 ‘JS에서는 var, const가 static 인가?’라고 착각을 했다. 아니다. 지역과 전역은 let 같은 선언 키워드의 문제가 아니다.


ISSUE (2) 함수 스코프의 부연설명

이 자바스크립트의 맹점을 기계적으로 회피하는게 아니라 실제로 이해하려면 결국 글로벌 객체와 실행문맥을 선언 키워드와 연결해서 이해해야한다.

  • 자바스크립트 함수 스코프가 기본이다.
  • 명시되지는 않았지만 작성된 코드는 브라우저의 window객체를 기준으로 실행된다.
  • 즉 브라우저측에서 다루는 전역변수의 실체는 window 객체의 프로퍼티다. (엄밀히 따지면 같은 전역변수라도 var와 let은 다르다. let은 window프로퍼티가 아니다. 아래에서 정리한다.)

window의 자녀들

전역객체와 변수의 관계— 첫 예제의 test 외부에서 선언된건 모두 전역변수다. 다른 실행컨텍스트 즉 다른 함수에서 전역변수를 선언하는 방법은 (3)에서 설명

  • 따라서 예제의 test 함수 안에 선언된것들은 무슨 키워드를 쓰든 지역변수다. 즉 키워드가 아니라 어떤 함수스코프에서 선언이 되었느냐가 변수의 호출범위를 좌지우지한다.
  • 의미가 없다면 선언 키워드 간 차이는?
    함수 스코프를 따르고 재선언 및 변수변경이 가능한 < var >
    블록 스코프를 사용하고 재선언이 불가능한 공통점을 가진 < let >, < const >
    글로벌객체 (또는 실행함수)에 프로퍼티를 형성하는 < var >
    글로벌객체 (또는 실행함수)에 프로퍼티를 형성하지 않는 < let >, < const >
    < let >과 < const >의 차이점은 값 변경가능여부다.

var는 글로벌 객체에 붙었다. 그럼 let과 const는 어디에 있다가 호출되는걸까!

let 으로 디파인한 변수는 어디에 숨어 있는지 알기 위해. 익명함수를 실행했다. 함수는 실행될때마다 저 자신의 실행문맥을 가진다. (실행문맥은 함수의 argument, scope 등을 가지고 있다) 여기선 함수를 ( ) 으로 래핑함으로서 윈도우와 동떨어진 독립된 스코프가 되었다. 내가 콘솔에서 선언한 testLet은 글로벌객체엔 없지만 실행문맥의 Script Scope 에서 찾을 수 있기에 호출할때 Ref 에러를 발생시키지 않았다.


ISSUE (3) 어떻게 전역을 선언하나?

window.foo = 'hello';       // 윈도우 객체에 직접 입력
window['foo'] = 'hello';    // 위와 동일
var bar = function() {      // var 키워드가 바로 위와 같이 윈도우객체( 다르게 말해 실행중인 함수의 'VariableObject'에 접근하는 것임을 알자.
  foo = "hello";            // 키워드가 없어도 윈도우에 붙는다.
};
Object.defineProperty(window,"foo", {value: "hello"});    // Obejct에 내장된 함수를 통해서 선언

전역(글로벌객체)에 접근하는것 그게 전역변수를 만드는 방법이다. 그리고 이글은 3번 Line에 대한 긴 설명이었다.

전역(글로벌객체)에 접근하는것 그게 전역변수를 만드는 방법이다. 그리고 이 글은 이 사실에 대한 대한 긴 설명이었다. 그렇다면 첫 예제의 ‘noVar’ 처럼 그냥 키워드 없이 쓰는게 JAVA의 static 에 해당한다는걸 알수 있다. 특별히 정하지 않거나 var를 사용하면 변수가 죄다 전역객체에 붙어버린다. 혼자만 쓰는것도 아닌데 글로벌 객체에 덕지덕지 붙는건 문제를 일으킬것 같다. 때문에 let으로 블럭 스코프를 기준으로 변수를 관리하는 것이 의미가 있다는걸 알 수 있다.

var는 실행중인 함수의 프로퍼티에 접근하는 키워드다. 이게 이번 글의 핵심이다. 익명함수 안에서 실행된 var는 수행이 끝남과 동시에 컨텍스트가 사라져 다시 호출할 수 없다. 로컬변수는 이렇게 사라지고 사라지지 않기 위한 패턴이 클로져다.


ISSUE (4) 함수의 내용은 처음부터 실행되지 않는다.

let, var, function, class, {} 같은 선언(statement)은 코드가 읽힐때 호이스팅된다. 이때 호이스팅은 선언과 실행의 순서가 거꾸로라도 Reference 에러가 나지 않는 차이가 있는거지 내용물을 실행하는건 아니다. 리터럴 형식으로 정의된 함수도 마찬가지다. var에 undefine이 먼저 정의할뿐이지 할당은 순차적인 작업에서 시행된다.

  • 그렇기 처음 noVar의 호출은 에러를 일으키지만 호출후에는 전역변수가 되어서 스코프에 잡혀 호출에도 에러가 발생하지 않는다.
  • 하지만 바로 실행되지 않는다고해서 실시간으로 호출되는 형태에 따라 Scope가 정해진다고 생각하면 안된다. 스코프는 함수가 사용할 준비가 될때 (호이스팅 될때 정해진다.)
var P_tempVar = "tempVarAtGlobal";

function foo() {
  var P_tempVar = "tempVarAtLocal";
  
  function bar() {
    console.log(P_tempVar);
  }
  bar();
}

foo();          // "tempVarAtLocal"
bar();          // ERROR

foo() 에서 콜 스택은 global, foo, bar 순으로 쌓이겠지만 그렇다고 호출해준 함수의 스코프를 순차적으로 참고하지 않는다. bar() 에서 콜 스택은 global, bar 순으로 쌓이고 전역변수가 호출되는게 자연스러워 보이지만 원래 스코프는 global 밖에 없었다.

스코프는 바뀌지 않는다. 어떻게 호출하든 함수의 위치(글로벌에서 정의했다)만이 영향을 미친다는 거다. 다르게 표현하면 처음 정해진 근본을 따라간다는 이야기다.

로컬에 변수가 없으므로 글로벌을 참조한다. 여기서 함수 bar 는 전역객체 스코프만 가지고 있다. 콜스택은 그저 프로그램 제어순서를 보여줄뿐이다.

var P_tempVar = "tempVarAtGlobal";

function foo() {
  var P_tempVar = "tempVarAtLocal";
  
  function bar() {
    console.log(P_tempVar);
  }
  bar();
}

foo();          // "tempVarAtLocal"
bar();          // ERROR

bar 함수는 foo(); 를 통해 사용할 준비가 된다. 전과 다르게 foo의 컨텍스트에서 호이스팅되는 bar이므로 로컬변수가 출력된다.

Categories:

Updated:

Leave a comment