[Javascript] 스코프(scope)와 스코프 체인(scope chain)

박기영·2022년 12월 8일
0

Javascript

목록 보기
26/45

스코프(scope)란 무엇인가?

스코프(scope)란 참조 대상 식별자(identifier, 변수, 함수의 이름과 같이 어떤 대상을 다른 대상과 구분하여 식별할 수 있는 유일한 이름)를 찾아내기 위한 규칙이다. 자바스크립트는 이 규칙대로 식별자를 찾는다.
- 모던 자바스크립트 Deep Dive -

아래 예시를 보자.

var x = "global";

function foo() {
  var x = "function scope";
  console.log(x);
}

foo(); // function scope
console.log(x); // global

전역에 선언된 변수 x는 어디에든 참조할 수 있다. 하지만 함수 foo 내에서 선언된 변수 x는 함수 foo 내부에서만 참조할 수 있고 함수 외부에서는 참조할 수 없다. 이러한 규칙을 스코프라고 한다.

스코프라는 개념이 없다면,
같은 식별자 이름은 충돌을 일으키므로 프로그램 전체에서 하나밖에 사용할 수 없다.

스코프의 구분

스코프는 크게 둘로 나눌 수 있다.

  1. 전역 스코프(Global scope)
  • 코드 어디에서든지 참조할 수 있다.
  1. 지역 스코프(Local scope or Function-level scope)
  • 함수 코드 블록이 만든 스코프로 함수 자신과 하위 함수에서만 참조할 수 있다.

변수 관점에는 다음과 같이 해석 할 수 있겠다.
보통 코딩 공부 처음 시작할 때 많이 들었던 내용일 것이다.(필자는 대학 1학년에 그랬던 것 같다)

  1. 전역 변수(Global variable)
  • 전역에서 선언된 변수이며 어디에든 참조할 수 있다.
  1. 지역 변수(Local variable)
  • 지역(함수) 내에서 선언된 변수이며 그 지역과 그 지역의 하부 지역에서만 참조할 수 있다.

즉, 전역에서 선언된 변수는 전역 스코프를 가지는 전역 변수라는 것이고,
지역에서 선언된 변수는 지역 스코프를 가지는 지역 변수라는 것이다.

자바스크립트에서의 스코프

자바스크립트에서는 함수 레벨 스코프(Function-level scope)를 따른다.
함수 레벨 스코프란 함수 코드 블록 내에서 선언된 변수는 함수 코드 블록 내에서만 유효하고 함수 외부에서는 유효하지 않다(참조할 수 없다)는 것이다.
- 모던 자바스크립트 Deep Dive -

아까 살펴봤던 예시를 다시 끌어와보자.

var x = "global";

function foo() {
  var x = "function scope";
  console.log(x);
}

foo(); // function scope
console.log(x); // global

함수 레벨 스코프를 따르기 때문에, 함수 foo 내에서의 xfoo 내에서만 유효한 것을 볼 수 있다.

그러나, 여기선 var가 사용되고 있다.
필자는 let이 익숙하므로, let에 대해서도 살펴보자.

var x = 0;
{
  var x = 1;
  console.log(x); // 1
}
console.log(x); // 1

let y = 0;
{
  let y = 1;
  console.log(y); // 1
}
console.log(y); // 0

엥? 뭔가 다르다.
var의 경우에는 변경된 값이 그대로 블록 밖으로 빠져나오는 반면 let은 블록 내에서만 유효하다.

이는 let블록 레벨 스코프(Block-level scope)를 따르기 때문이다.

블록 레벨 스코프란 코드 블록({…})내에서 유효한 스코프를 의미한다. 여기서 “유효하다”라는 것은 “참조(접근)할 수 있다”라는 뜻이다.
- 모던 자바스크립트 Deep Dive -

지금까지 본 함수 레벨 스코프블록 레벨 스코프에서의 예시를 보면 사실 약간 헷갈릴 수 있다.
아까는 함수 밖에서는 접근 안된다고 하지않았었나? 왜 여기서는 변경된 값이 전역에 반영된거지?

이건 함수 레벨 스코프에 대한 설명을 보면 좀 더 명확하게 이해할 수 있다.
함수 레벨 스코프는 위 설명에 적혀있듯 함수 코드 블록 내에서 선언된 변수에 대한 것이다.
두 예시를 보면 하나는 함수에서, 다른 하나는 일반 블록에서 선언된다.
이 차이로 인해 변경의 반영 여부가 달라지는 것이다.
개념에 대한 설명을 잘 읽어보자!(필자는 헷갈렸다 ㅠ)

전역 스코프(Global scope)

전역에 변수를 선언하면 이 변수는 어디서든지 참조할 수 있는 전역 스코프를 갖는전역 변수가 된다.

var global = "global";

function foo() {
  var local = "local";
  console.log(global);
  console.log(local);
}

foo();

console.log(global);
console.log(local); // Uncaught ReferenceError: local is not defined

변수 global의 경우에는 전역에 선언되었기 때문에,
함수 foo 내부에서도, 밖에서도 전부 접근이 가능하다.

그러나 변수 local의 경우에는 지역에 선언되었기 때문에,
함수 foo 내부에서만 접근이 가능하다.

즉, 변수 global전역 스코프를 갖는 전역 변수라는 것을 알 수 있다.

참고로, var 키워드로 선언한 변수는 전역 객체(Global object)window의 프로퍼티가 된다.
이 내용은 다른 게시글에서 더 살펴볼 예정이다.

자바스크립트에서는 특별한 시작점(Entry point)가 없어서 아무데서나 변수를 선언 할 수 있는데,

전역 변수의 사용은 변수 이름이 중복될 수 있고, 의도치 않은 재할당에 의한 상태 변화로 코드를 예측하기 어렵게 만드므로 사용을 억제하여야 한다.
- 모던 자바스크립트 Deep Dive -

그렇다. 아무데서나 막 선언하면 코드를 살펴보는 개발자 입장에서는 독이 된다. 주의하도록 하자.

비 블록 레벨 스코프(Non block-level scope)

if (true) {
  var x = 5;
}
console.log(x); // 5

앞서 말했듯, 자바스크립트는 함수 레벨 스코프를 따른다.
그렇지만, 위 예시는 함수 내에서 선언된 것이 아니다.
따라서, 함수 밖에서 선언된 변수는 코드 블록 내에서 선언되었다할지라도 모두 전역 스코프를 갖게된다. 따라서 변수 x전역 변수이다.

위에서 헷갈렸다고 말한 부분이 바로 이 부분인데, 이제 명확하게 이해가 된다.

함수 레벨 스코프(Function-level scope)

var a = 10; // 전역변수

function foo() {
  var b = 20; // 지역변수
}

console.log(a); // 10
console.log(b); // "b" is not defined

다시 한번 말하지만, 자바스크립트는 함수 레벨 스코프를 따른다.
함수 내에서 선언된 매개변수와 변수는 함수 외부에서는 유효하지 않다.
따라서 변수 b지역 변수이다.

그런데...아까 살펴봤던 예시들 중 아래와 같은게 있었다.

var x = "global";

function foo() {
  var x = "local";
  console.log(x);
}

foo(); // local
console.log(x); // global

이렇게 중복되는 선언이 있을 경우에는 어떻게 될까?
전역 영역에서는 전역변수만이 참조 가능하고 함수 내 지역 영역에서는 전역과 지역 변수 모두 참조 가능하나 위 예제와 같이 변수명이 중복된 경우, 지역변수를 우선하여 참조한다.

함수 내에 있는 함수에 대해서도 살펴보자.

var x = "global";

function foo() {
  var x = "local";
  console.log(x); // local

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

  bar();
}

foo();
console.log(x); // global

내부함수는 자신을 포함하고 있는 외부함수의 변수에 접근할 수 있다.
따라서 함수 bar는 함수 foo에서 선언된 변수 xlocal을 출력한다.

이유를 더 명확하게 설명하자면 다음과 같다.
실행 컨텍스트스코프 체인에 의해 참조 순위에서 전역변수 x가 뒤로 밀렸기 때문이다.
이 부분도 다른 게시글에서 다뤄볼 예정이다.

만약 전역 변수를 함수 내부에서 수정하게 된다면 어떻게 될까?

var x = 10;

function foo() {
  x = 100;
  console.log(x); // 100
}

foo();

console.log(x); // 100

변경이 적용되는 것을 볼 수 있다.
이는 함수(지역) 영역에서 전역변수를 참조할 수 있으므로 전역변수의 값도 변경할 수 있기 때문이다.

내부 함수의 경우, 전역변수는 물론 상위 함수에서 선언한 변수에 접근/변경이 가능하다.

var x = 10;

function foo() {
  var x = 100;
  console.log(x); // 100

  function bar() {
    x = 1000;
    console.log(x); // 1000
  }

  bar();
}

foo();

console.log(x); // 10

내부 함수에서 수정된 전역 변수가 반영되어 출력이 된 것을 확인 할 수 있다.
그런데 직전 예시와 약간 다른 점은 var를 사용해서 재선언을 했다는 점이다.
따라서 함수 foo 내에서는 var x = 100을 사용하고,
함수 foo 밖에서는 var x = 10을 사용한다.

이처럼 중첩 스코프는 가장 인접한 지역을 우선하여 참조한다.
아래 예시를 추가로 살펴보자.

var foo = function () {
  var a = 3;
  var b = 5;

  var bar = function () {
    var b = 7;
    var c = 11;

    // 이 시점에서 a는 3, b는 7, c는 11

    a += b + c;

    // 이 시점에서 a는 21, b는 7, c는 11
  };

  // 이 시점에서 a는 3, b는 5, c는 not defined

  bar();

  // 이 시점에서 a는 21, b는 5
};

변수 b가 재선언이 되는 것을 볼 수 있다.
각 위치에서의 변수 상태를 보면 중첩 스코프가 가장 인접한 지역을 우선하여 참조한다는 것을 알 수 있다.

만약, 함수 내에서 var로 재선언을 하는게 아니라, 그냥 재할당만 하는 경우는 아래와 같다.

var x = 10;

function foo() {
  x = 100;
  console.log(x); // 100

  function bar() {
    x = 1000;
    console.log(x); // 1000
  }

  bar();
}

foo();

console.log(x); // 1000

렉시컬 스코프

아래 예시가 어떻게 동작할지 예측해보자.

var x = 1;

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

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

foo(); // 1
bar(); // 1

예상한대로 결과가 나왔나요? ㅎㅎ
아마 예상 결과는 두 가지의 경우로 나뉠 것 같다.

첫 번째는 함수를 어디서 호출하였는지에 따라 상위 스코프를 결정하는 것이고,
두 번째는 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정하는 것이다.

즉, 첫 번째 방식으로는 함수 bar의 상위 스코프는 함수 foo전역일 것이고,
두 번째 방식으로는 함수 bar의 스코프는 전역일 것이다

프로그래밍 언어는 이 두 가지 방식 중 하나의 방식으로 함수의 상위 스코프를 결정한다.
첫 번째 방식은 동적 스코프(Dynamic scope)라고 하며,
두 번째는 방식은 렉시컬 스코프(Lexical scope) 또는 정적 스코프(Static scope)라고 한다.

자바스크립트는 렉시컬 스코프를 따른다.
즉, 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정한다는 것이다.

따라서, 함수 bar전역에서 선언되었으므로 1을 출력하는 것이다.

암묵적 전역

아래 코드를 보자.

var x = 10; // 전역 변수

function foo() {
  y = 20; // 선언하지 않은 변수
  console.log(x + y);
}

foo(); // 30

변수 y가 선언되지 않았다. 그러나 이는 에러를 발생시키지 않는다.
이는 선언하지 않은 식별자에 값을 할당하면 전역 객체의 프로퍼티가 되기 때문이다.

함수 foo가 호출되면 자바스크립트 엔진은 변수 y에 값을 할당하기 위해 먼저 스코프 체인을 통해 선언된 변수인지 확인한다.
이때 foo 함수의 스코프와 전역 스코프 어디에서도 변수 y의 선언을 찾을 수 없다.
참조 에러가 발생해야 하지만 자바스크립트 엔진은 y = 20window.y = 20으로 해석하여 프로퍼티를 동적 생성한다.
결국 y전역 객체의 프로퍼티가 되어 마치 전역 변수처럼 동작한다.
이러한 현상을 암묵적 전역(implicit global)이라 한다.

하지만 y는 변수 선언없이 단지 전역 객체의 프로퍼티로 추가되었을 뿐이다. 따라서 y변수가 아니다.
따라서 변수가 아닌 y변수 호이스팅이 발생하지 않는다.

console.log(x); // undefined

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

var x = 10; // 전역 변수

function foo() {
  y = 20; // 선언하지 않은 변수
  console.log(x + y);
}

foo(); // 30

콘솔을 찍어보면 x는 호이스팅이 된 것에 반해, y는 호이스팅이 되지 않았다.
호이스팅에 대해서도 다른 게시글에서 다뤄볼 예정이다.

y는 변수가 아니라 단순 프로퍼티이기 때문에 delete를 통해 삭제도 가능하다.
아래 예시를 통해 확인해보자.

var x = 10; // 전역 변수

function foo() {
  y = 20; // 선언하지 않은 변수
  console.log(x + y);
}

foo(); // 30

console.log(window.x); // 10
console.log(window.y); // 20

delete x; // 전역 변수는 삭제되지 않는다.
delete y; // 프로퍼티는 삭제된다.

console.log(window.x); // 10
console.log(window.y); // undefined

스코프 체인(scope chain)

스코프 체인은 Lexical Environment와 같이 보면 이해가 잘된다.

이 부분은 Sukhjinder Arora님 블로그의 글을 의역하였습니다

자바스크립트에서 변수가 사용될 때, 자바스크립트 엔진은 현재 스코프에서 변수를 찾을 것이다.
만약 변수를 찾지 못하면, 엔진은 바깥 스코프를 찾으며,
결국에는 변수를 찾아내거나, 전역 스코프(global scope)에 도달할 것이다.

만약, 여전히 변수를 찾지못했다면,
엔진은 암묵적으로 전역 스코프에 변수를 선언하거나(엄격 모드(strict mode)가 아닌 경우)
에러를 반환한다.

아래 예시를 통해 살펴보자.

let foo = "foo";

function bar() {
  let baz = "baz";

  console.log(baz); // baz
  console.log(foo); // foo

  number = 42;
  console.log(number); // 42
}

bar();

함수 bar()가 실행될 때, 자바스크립트 엔진은 변수 baz현재 스코프에서 찾는다.
다음으로, 변수 foo를 현재 스코프에서 찾고, 거기에 없으면, 바깥 스코프에서 변수를 찾는다.
예시에서는 바깥 스코프전역 스코프이다.

그 이후, 변수 number42를 할당한다.
따라서, 자바스크립트 엔진은 변수 number현재 스코프에서 찾아본 후, 바깥 스코프에서도 찾는다.

만약, 스크립트가 엄격 모드(strict mode)가 아니라면,
엔진은 number라는 이름의 새로운 변수를 생성하고 42를 할당한다.
혹은 에러를 반환한다(엄격 모드에서)

결국, 변수가 사용될 때, 엔진은 변수를 찾을 때까지 스코프 체인을 횡단(traverse)할 것이다.

자바스크립트는 변수를 어떻게 찾는가?

여기서부터는 Lexical Environment에 대한 내용이 섞여서 나옵니다.
혹시나 해당 개념에 대해 궁금하신 분은 필자의 실행 컨텍스트에 대한 게시글을 참고해주세요

이제 자바스크립트가 스코프와 스코프 체인을 정의하기 위해 어떻게 Lexical Environment를 사용하는지 알아보자.

아래 예시를 보자.

let greeting = "Hello";

function greet() {
  let name = "Peter";
  console.log(`${greeting} ${name}`);
}

greet(); // Hello Peter

{
  let greeting = "Hello World!";
  console.log(greeting); // Hello World!
}

위의 스크립트가 불러와졌을 때(loads),
전역 스코프 내에 정의된 변수와 함수를 포함하는 global Lexical Environment가 생성된다.

예를들어, 다음과 같이 말이다.

globalLexicalEnvironment = {
  greeting: 'Hello'
  greet: <ref. to greet function>
  outer: <null>
}

외부(outer) Lexical Environmentnull로 세팅되어 있다.
전역 스코프 후에 더 이상 바깥 스코프가 없기 때문이다.

이 이후, 함수 greet()의 호출을 만난다.
따라서, 함수 greet()를 위해 새로운 Lexical Environment가 생성된다.
아래와 같이 말이다.

functionLexicalEnvironment = {
  name: 'Peter'
  outer: <globalLexicalEnvironment>
}

이번에는 외부 Lexcial EnvironmentglobalLexicalEnvironment로 되어있다.
왜냐면 greet()외부 스코프전역 스코프이기 때문이다.

이 이후, 자바스크립트 엔진은

console.log(`${greeting} ${name}`);

위 구문을 실행한다.

자바스크립트 엔진은 함수의 Lexical Environment 내에서 변수 greeting,name을 찾으려고 한다.
현재 Lexical Environment에서 변수 name은 찾았지만, greeting은 찾지 못했다.

따라서, 엔진은 외부 Lexcial Environment(외부 속성에 의해 정의된, 즉, 전역 Lexical Environment) 내에서 변수 greeting을 찾아낸다.

그 다음, 자바스크립트 엔진은 블록 내에서 코드를 실행한다.
따라서, 엔진은 블록을 위해 새로운 Lexical Environment를 생성한다.
아래와 같이 말이다.

blockLexicalEnvironment = {
  greeting: 'Hello World',
  outer: <globalLexicalEnvironment>
}

그 다음, console.log(greeting) 구문이 실행되고,
자바스크립트 엔진은 현재 스코프에서 변수를 찾고 사용한다.
따라서, 엔진은 변수를 위해서 외부 Lexical Environment(전역 Lexcial Environment)를 보지않는다.

참고로, 새로운 Lexical Environment는 오직 letconst의 선언을 위해서만 생성된다.
var에 대해서는 생성되지 않는다.
var 선언은 현재 Lexical Environment(전역 혹은 함수 Lexical Environment)에 추가된다.
이 부분은 실행 컨텍스트에 대한 내용을 보고 오시면 더 이해가 잘 됩니다 :)

따라서, 프로그램에서 변수가 사용될 때,
자바스크립트 엔진은 현재 Lexical Environment에서 변수를 찾으려고 하며,
만약 변수가 그 곳에 없는 경우, 엔진은 외부 Lexical Environment에서 변수를 찾는다.
이게 바로 자바스크립트 엔진이 변수를 찾아내는 방법이다.

전역 변수 사용의 억제

최소한의 전역 변수 사용

var MYAPP = {};

MYAPP.student = {
  name: "Lee",
  gender: "male",
};

console.log(MYAPP.student.name); // Lee

전역 변수 사용을 최소화하는 방법 중 하나는 애플리케이션에서 전역 변수 사용을 위해
전역 변수 객체 하나를 만들어 사용하는 것이다.

즉시 실행 함수 이용

(function () {
  var MYAPP = {};

  MYAPP.student = {
    name: "Lee",
    gender: "male",
  };

  console.log(MYAPP.student.name); // Lee
})();

console.log(MYAPP.student.name); // ReferenceError: MYAPP is not defined

전역 변수 사용을 억제하기 위해,
즉시 실행 함수(IIFE, Immediately-Invoked Function Expression)를 사용할 수도 있다.
이 방법을 사용하면 전역 변수를 만들지 않으므로 라이브러리 등에 자주 사용된다.
즉시 실행 함수는 즉시 실행되고 그 후 전역에서 바로 사라진다.

참고 자료

모던 자바스크립트 Deep Dive 페이지
doozi0316님 블로그
Sukhjinder Arora님 블로그

profile
나를 믿는 사람들을, 실망시키지 않도록

0개의 댓글