Published on

자바스크립트의 스코프

Authors
  • avatar
    Name
    박준형
    Twitter

스코프

스코프는 유효범위다. 스코프는 Js를 포함한 모든 프로그래밍 언어의 기본개념이다. Js의 스코프는 다른 언어의 스코프와 구별되는 특징이 있다.

var 키워드로 만든 변수와 let, const 키워드로 만든 변수의 스코프는 다르게 동작한다. 함수의 매개변수는 함수 내부에서만 참조가 가능하다. 외부에서 함수의 매개변수에 참조할수는 없다.

변수는 코드의 최상위 레벨뿐만 아니라 코드 블럭 혹은 함수 내부에서도 선언이 가능하다. 자신이 선언된 위치에 의해 자신이 유효한 범위, 즉 다른 코드가 자신을 참조할 수 있는 범위가 결정된다. 변수뿐만이 아니라 모든 식별자가 그렇다

모든 식별자는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효범위가 결정된다.
이를 스코프라하고 식별자가 유효한 범위를 말한다.

var scope = 'global'

function foo() {
  var x = 'local'
  console.log(x) // 1번
}

foo()

console.log(x) // 2번

코드 최상위 레벨과 foo 함수 내부에 같은 이름을 갖는 x 변수를 선언했고, 1번과 2번에서 변수를 참조하고있다. 이때 JS Engine은 이름이 같은 두 개의 변수 중에서 어떤 변수를 참조해야하는지 결정해야한다. 이를 **식별자 결정(identifier resolution)**이라 부른다

JS Engine은 코드를 실행할때 코드의 컨텍스트를 고려한다. 코드가 어디서 실행되며 주변에 어떤 코드가 있는지에 따라 위 예제의 1번과 2번처럼 동일한 코드도 다른 결과를 만들어 낸다.

예제에서 최상위 레벨에 선언된 x 변수는 어디서든 참조가 가능하다. 하지만 함수 내부에서 선언된 x 변수는 함수 내부에서만 참조할 수 있고 함수 외부에서는 참조할 수 없다.

            메모리주소      값
전역스코프 x   0x000000F1  'global'
함수의내부 x   0x000000F2  'local'

동일한 이름을 가진 식별자이지만 메모리주소는 다른 주소를 가리키고있다.

식별자에 대해서 다시 생각해보면 변수나 함수의 이름이 같은 식별자는 어떤 값을 구별하여 식별해낼 수 있는 고유한 이름을 말한다.
그러므로 유일해야한다. 식별자인 변수 이름은 중복될수 없으므로 하나의 값은 유일한 식별자에 연결(name binding) 되어야한다.

종류

코드는 전역과 지역으로 구분지을 수 있다.

전역이란 코드의 최상위 영역을 말한다. 전역은 전역스코프를 만든다. 전역변수는 어디서든 참조할 수 있다.
지역이란 함수 내부의 영역을 말한다. 지역은 지역스코프를 만든다. 지역에 변수를 선언하면 지역스코프를 갖는 지연변수가 된다.

지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효하다

스코프 체인

함수는 전역에서 정의할 수도 있고 함수 내부에서 정의할 수도 있다. 함수 내부에서 정의된 함수는 함수의 중첩이라고 한다.
함수는 중첩될 수 있으므로 함수의 지역 스코프도 중첩될 수 있다.

이를 스코프가 함수의 중첩에 의해 계층적 구조를 갖는다 라고 말하며 중첩 함수의 지역 스코프는 중첩 함수를 포함하는 외부 함수 지역 스코프와 게층적 구조를 갖는다

var x = '글로벌x'
var y = '글로벌y'

function 외부함수() {
  var z = '외부함수z'

  console.log(x) // 글로벌x - 1번
  console.log(y) // 글로벌y - 2번
  console.log(z) // 외부함수z - 3번

  function 내부함수() {
    var x = '내부함수x'

    console.log(x) // 내부함수x - 4번
    console.log(y) // 글로벌y - 5 번
    console.log(z) // 외부함수z - 6번
  }

  내부함수()
}

외부함수()

console.log(x) // 글로벌x - 7번
console.log(z) // ReferenceError: z is not defined - 8번

위의 예제에서 외부 함수의 지역과 내부 함수의 지역이 있다. 내부 함수는 외부 함수의 중첩함수다. 이때 외부 함수가 만든 지역스코프는 내부 함수가 만든 지역 스코프의 상위 스코프다.

그리고 외부함수의 지역스코프의 상위 스코프는 전역 스코프다.

내부함수 지역스코프 -> 외부함수 지역스코프 -> 전역스코프

이처럼 모든 스코프는 하나의 계층적 구조로 연결되며 모든 지역 스코프의 최상위 스코프는 전역 스코프다. 이렇게 계층적 연결을 스코프 체인이라고한다.

변수를 참조할 때 JS Engine은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다. 이를 통해 상위 스코프에서 선언한 변수를 하위 스코프에서도 참조할 수 있다.

스코프 체인은 물리적으로 존재한다. JS Engine은 코드를 실행하기에 앞서 스코프체인과 같은 자료구조인 렉시컬환경을 실제로 생성한다. 변수 선언이 실행되면 변수 식별자가 렉시컬 환경에 키로 등록되고 변수 할당이 일어나면 렉시컬환경의 변수 식별자에 해당하는 값을 변경한다. 변수의 검색도 렉시컬환경에서 이뤄진다.

렉시컬 환경(Lexical Environment)

렉시컬 환경(Lexical Environment)은 코드 Block, function, script를 실행하기 앞서 생성되는 특별한 자료구조다. 실행할 스코프 범위 안에있는 변수와 함수를 프로퍼티로 저장하는 객체다.

코드를 실행하며 참조가 필요한 변수의 값을 렉시켤환경이라는 객체에서 식별자 이름을 키로 사용해 값을 찾는다고 생각하면된다.

스코프 체인에 의한 변수 검색

위의예제 코드를 다시보자

4번 변수(x) 를 참조하는 코드의 스코프인 내부함수의 지역 스코프에서 x가 선언되었는지 검색한다. 내부 함수 내에는 선언된 x가 존재한다. 따라서 변수를 참조하고 검색을 종료한다.

5번 변수(y) 를 참조한는 코드의 스코프인 내부함수의 지역스코프에서 y가 선언되었는지 검색한다. 내부 함수 내에는 y변수 선언이 존재하지 않으므로 상위스코프인 외부함수의 지역 스코프로 이동한다. 여기서도 y변수 선언이 존재하지않으므로 상위스코프인 전역스코프로 이동한다. 전역 스코프에는 y변수 선언이 존재하므로 변수를 참조하고 검색을 종료한다.

6번 변수(z) 를 참조한느 코드의 스코프인 내부함수의 지역스코프에서 z가 선언되었는지 검색한다. 내부 함수 내에는 z변수 선언이 존재하지 않으므로 상위스코프인 외부함수의 지역 스코프로 이동한다. 외부함수내에는 z변수 선언이 존재하므로 변수를 참조하고 검색을 종료한다.

JS Engine은 스코프 체인을 따라 변수를 참조한느 코드의 스코프에서 시작하여 상위 스코프방향으로 이동하며 선언된 변수를 탐색한다. 상위스코프에서 하위스코프로 내려가며 식별자를 검색하는게 아니다.

상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조할 수 있지만, 하위 스코프에서 유효한 변수를 상위 스코프에서 참조할 수 없다

스코프 체인에 의한 함수 검색

function foo() {
  console.log('global function foo')
}

function bar() {
  function foo() {
    console.log('local function foo')
  }
  foo() // 1번
}

bar()

함수 선언문으로 함수를 정의하면 런타임 이전에 함수 객체가 먼저 생성된다. JS Engine은 함수 이름과 동일한 이름의 식별자를 암묵적으로 선언하고 생성된 함수 객체를 할당한다.

모든 함수는 함수 이름과 동일한 이름의 식별자에 할당된다. 1번에서 foo 함수를 호출하면 JS Engine은 함수를 호출하기 위해 먼저 함수를 가리키는 식별자 foo를 검색하게된다.

함수라고 크게 다르지않다. 암묵적으로 함수이름으로 식별자를 부여받은 변수라고 생각하면된다. 전역스코프에서 bar함수를 호출하였다. bar함수의 지역스코프에서 foo 함수를 호출하였다. 그럼 bar함수 지역스코프내에서 foo식별자를 갖는 함수를 찾고 foo라는 함수를 발견하면 해당함수를 참조하여 호출하게된다.

스코프는 변수를 검색할때 사용하는 규칙보다는 식별자를 검색하는 규칙라고 표현하는편이 적합하다.

함수레벨 스코프

지역은 함수 내부를 뜻하고 지역은 지역스코프를 만든다고 했다. 이는 코드 블럭이 아닌 함수에 의해서만 지역 스코프가 생성된다는것이다.

언어는 함수 몸체뿐만 아니라 모든 코드 블럭(if, for, while ...)이 지역 스코프를 만든다. 이를 블럭 레벨 스코프라고한다.

하지만 var 키워드로 선언된 변수는 오로지 함수의 코드블럭만을 지역 스코프로 인정한다. 이를 함수 레벨 스코프라고한다

var score = 100

if (true) {
  var score = 0
}

console.log(score) // 0

방금 var 키워드로 선언한 변수는 오로지 함수의 코드블럭만을 지역스코프로 인정 한다고했고, 그 결과 if 코드블럭은 지역스코프로 인정하지 않아 전역변수 score가 이미 존재하지만 또 다시 전역변수 score를 선언하게 된다. 이는 의도치않게 변수 값이 변경되는 부작용을 발생시킬 위험이 있다.

렉시컬 스코프

var score = 100

function foo() {
  var score = 0
  bar()
}

function bar() {
  console.log(score)
}

foo() // ? - 1번
bar() // ? - 2번

위 코드의 실행결과는 bar함수의 상위 스코프가 무엇인지에 따라 결정된다. 두가지 방법으로 예측해보자

  • 함수를 어디서 호출했는지에 따라 함수의 상위스코프를 결정한다.
  • 함수를 어디서 정의했는지에 따라 함수의 상위스코프를 결정한다.

첫번째 방법으로 결정된다면 bar의 상위 스코프는 foo함수의 지역스코프전역스코프가 된다.
두번째 방법으로 결정된다면 bar의 상위 스코프는 전역스코프가 된다.

첫번째 방법을 동적스코프라 말한다. 함수를 정의하는 시점에는 함수가 어디서 호출될지 모르니 함수가 호출된 시점에 동적으로 상위 스코프를 결정해야하기에 동적스코프라 한다.

두번째 방법을 렉시컬스코프 혹은 정적스코프라 말한다. 동적 스코프처럼 상위 스코프가 호출시점에 변경되는게아닌 함수 정의가 평가되는 시점에 상위스코프가 정적으로 결정되기 때문에 정적 스코프라고 말한다.

JS는 렉시컬 스코프를 따른다. 함수를 어디서 호출했는지가 아니라 어디서 정의되었는지에 따라 상위스코프가 결정된다.

JS의 스코프에대해서 알아보았으니 위의 예제의 결과를 알아보자

var 키워드로 선언한 변수는 함수레벨 스코프를 가지고, JS는 렉시컬 스코프를 따른다.

전역스코프에서 정의한 foo함수와 bar 함수는 바로 위 상위스코프가 전역스코프다.

1번에서 foo가 호출되었다. foo 함수내부의 score라는 변수가 선언되었고 bar 함수를 호출한다.
bar 함수가 실행되며 bar 함수 내부의 score라는 변수가 없으므로 상위스코프로 이동해 변수를 탐색한다.
상위 스코프인 전역스코프에는 score라는 변수가 있어 해당 변수를 참조하게되어 100 이라는 결과가 도출된다.

2번에서 bar가 호출되었다. bar 함수내부에 score라는 변수가 없으므로 상위스코프로 이동해 변수를 탐색한다.
상위 스코프인 전역스코프에는 score라는 변수가 있어 해당 변수를 참조하게되어 100이라는 결과가 도출된다.


참조 레퍼런스