자바스크립트 딥다이브 - 스코프

ChoiYongHyeun·2023년 12월 10일
0

스코프

스코프 (유효범위) 는 자바스크립트를 포함한 모든 프로그래밍 언어의 기본적이며 중요한 개념이다.

특히나 자바스크립트의 스코프는 다른 언어의 스코프와 구별되는 특징이 존재한다.

이미 이전 함수 편에서 스코프에 대해서 경험했었다.

var var1 = 1;

if (true) {
  var var2 = 2;
  if (true) {
    var var3 = 3;
  }
}

console.log(var1); // 1
console.log(var2); // 2
console.log(var3); // 3

다음처럼 함수 외부에서 다양한 코드 블록으로 변수를 선언해도 접근 할 수 있지만

function f() {
  var var1 = 1;

  if (true) {
    var var2 = 2;
    if (true) {
      var var3 = 3;
    }
  }
}

console.log(var1); // ReferenceError
console.log(var2); // ReferenceError
console.log(var3); // ReferenceError

같은 로직이여도 함수 몸체 내부에 존재하는 변수는 접근 할 수 없었다.

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

이를 스코프라 한다.

var x = 'global';

function foo() {
  var x = 'local';
  console.log(x);
}
foo(); // local
console.log(x); // global

코드 내부와 외부 모두 x 에 서로 다른 값을 설정해주었다.

함수 foo 를 실행했을 땐 함수 몸체에 존재하는 x = local 을 로그했고

x 를 로그 했을 땐 함수 외부에 존재하는 x = global 을 로그했다.

두 식별자는 이름이 같음에도 불구하고 말이다.

그 이유는 자바스크립트 엔진이 코드가 실행되는 순간에 맞춰 어떤 변수를 참조해야 할지를 결정하기 때문이다.

이를 식별자 결정 이라고 한다 .

자바스크립트 엔진은 코드의 문맥을 고려한다.

코드의 문맥과 환경

코드가 어디에서 실행되며 주변에 어떤 코드가 있는지를 렉시컬 환경 (Lexical enviroment) 라 부른다. 즉 , 코드의 문맥은 레시컬 환경을 통해 이뤄진다. 이를 구현 한 것이 실행 컨텍스트(excution context) 이며 실행 컨텍스트에서 평가되고 실행된다.

식별자란 어떤 값을 가리키는 유일한 변수여야 하기 때문에 동일한 식별자 이름을 가진 다른 값이 존재하면 충돌을 일으킨다.

식별자는 어떤 값을 구별하기 위해 유일해야 하며, 하나의 값은 하나의 유일한 식별자에 연결 되어야 한다.

그렇기에 식별자는 식별자의 이름 뿐이 아니라, 어떤 문맥에서 존재하는지를 통해서 고유한 식별자를 구분한다.

var 키워드로 선언한 변수의 중복 선언

var x = 'hi';
var x = 'hello';
console.log(x); // hello

var 로 선언한 변수에는 재할당이 가능하지만

let x = 'hi';
let x = 'hello'; // SyntaxError: Identifier 'x' has already been declared
console.log(x);

let 이나 const 키워드로 선언된 변수들은 같은 스코프 내에서 중복 선언을 허용하지 않는다.

스코프의 종류

코드는 전역과 지역으로 구분 가능하다.

구분 설명 스코프 변수
전역 코드의 가장 바깥 영역 전역 스코프 전역 변수
지역 함수 몸체 내부 지역 스코프 지역 변수
  • 전역 : 전역 변수는 전역 스코프를 만들며 전역 변수느 어디서든지 참고 할 수 있다.
  • 지역 : 지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효하다.
function f1() {
  var x = 'hi';

  function f2() {
    var y = 'hi';
    console.log(x); 
  }

  f2(); // hi
  console.log(y); // ReferenceError: y is not defined
}

f1();

f1 함수 내부에 선언된 xf1 내부에 존재하는 지역 변수이므로 f1f1에서 정의된 지역 f2 에서 참조 가능하다.

하지만 f2 에서 존재하는 yf2 지역 외부에선 참조 할 수 없다.

이처럼 중첩 함수 내에 존재하는 지역 변수들은 계층적 구조를 갖는데 이를 스코프 체인 이라고 한다.

스코프 체인

var x = 'global value';

function foo() {
  var y = 'outer value';

  function innerFoo() {
    var z = 'inner value';
  }
}

foo();

다음 코드에서

변수 x 는 전역에 존재하는 전액 변수이다.
foo 안에 존재하는 변수yfoo 내부에 존재하는 지역 변수이다.
innerFoo 안에 존재하는 변수 zfoo 내부에 존재하는 innerFoo 에 존재하는 지역 변수이다.

위 해당 구조를 살펴보면

첫번째 상위 계층 (전역) 두번째 상위 계층 (foo) 세번째 상위 계층 (innerFoo)
x y z

다음 구조로 계층이 구성되어 있다.

자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.

var x = 'global value';

function foo() {
  var y = 'outer value';

  function innerFoo() {
    var z = 'inner value';
    console.log(x); // 'global value'
    console.log(y); // 'outer value'
    console.log(z); // 'inner value'
  }
  innerFoo();
}

foo();

innerFoo 에서 x,y,z 를 검색 할 때 현재 스코프인 innerFoo 영역에서 전역 계층까지 스코프 체인을 따라 전역 변수인 x 를 찾고, 상위 계층을 따라 y 를 찾고 현재 계층에서 z 를 찾는 것이 가능하다.

스코프 체인은 식별자의 스코프를 따라 상위 계층으로 단방향 이동하며 변수를 참조한다.

그런 이유로 상위 계층에 존재하는 변수를 하위 계층에서 참조하는 것은 가능하지만

하위 계층에 존재하는 변수를 상위 계층에서 참조하는 것이 불가능하다.

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

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

function bar() {
  function foo() {
    console.log('local foo');
  }
  foo();
}

bar(); // local foo
foo(); // global foo

함수도 객체라고 하였다.

그렇기 때문에 같은 함수 식별자를 가진 경우에도 코드가 실행된 문맥에 따라 함수를 참조한다.

bar 를 실행했을 땐 스코프 체인에 따라 bar 지역 내부터 상위 계층으로 올라가며 foo 를 찾기 때문에 함수 몸체 내에 존재하는 local foo 가 실행된다.

전역에서 foo 를 실행할 땐 전역 공간에서부터 foo 를 찾기 때문에 전역에서 선언된 global foo 가 실행된다.

함수 레벨 스코프

지역은 함수 몸체 내부를 정의한다고 하였다.

이는 코드 블록이 아닌 함수에 의해서만 지역 스코프가 생성된다는 의미다.

C , java 같은 경우엔 함수 몸체 외에도 코드 블록 (조건문, 반복문) 에서도 지역 스코프를 만든다.

이런 특성을 블록 레벨 스코프 라고 한다.

하지만 자바스크립트의 var 키워드로 선언된 변수는 오로지 함수 코드 블록 만을 지역 변수로 인정한다.

이러한 특성을 함수 레벨 스코프 라고 한다.

정리

블록 레벨 스코프 : 모든 코드 블록 내에서도 지역 스코프를 생성
함수 레벨 스코프 : 함수 몸체 내부에서만 지역 스코프를 생성

var i = 0;

for (var i = 0; i < 5; i++) {}

console.log(i); // 5

전역 변수와 지역 변수 모두 var 키워드로 생성 후 반복문을 시행하면 i = 5로 설정된다.

그 이유는 반복문 블록에서 i 값이 반복적으로 생성되기 때문이다.

var 키워드는 함수 몸체 내부를 제외한 모든 영역을 같은 스코프로 설정(위 예시에서 i 값이 증가 할 때 마다 전역 변수에 i 를 설정)하기 때문이다.

하지만 ES6 에서 도입된 let , const 키워드는 블록 레벨 스코프를 지원한다.

var i = 0;

for (let i = 0; i < 5; i++) {}

console.log(i); // 0

let , const 는 블록 레벨 스코프이기 때문에 반복문 내에서 설정된 i 값은 for 문 지역 스코프 내에 존재하는 지역 변수로 설정되어

반복 코드 블록 외부에서는 반복문 내부의 지역 변수인 i 에 접근 할 수 없다.

렉시컬 스코프

렉시컬

렉시컬이란 변수의 스코프가 소스코드의 문맥에 따라 결정된다는 개념

var x = 10;

function bar() {
  console.log(x);
}

function foo() {
  var x = 1;
  bar();
}

bar(); // 10
foo(); // 10

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

함수의 상위 스코프는 언제나 자신이 정의된 스코프를 의미한다.

bar 함수의 상위 스코프는 전역인 x = 10 이기 때문에

foo 내부에서 bar 을 실행하더라도 foo 내부의 지역 변수인 x =1 을 참조하는 것이 아니라

bar 함수가 선언 될 때의 상위 스코프인 전역의 x = 10 을 참조한다.

즉, 상위 스코프가 함수가 어디에서 실행되는지에 따라 동적으로 변하지 않고 함수가 선언 됐을 때 상위 스코프가 결정되는 , 정적 스코프 혹은 렉시컬 스코프 라고 한다.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글