Studying/JavaScript & Frameworks

[자바스크립트 떠먹여 주는 남자] 스코프 (Scope)

국장 지킴이 앨런 2022. 4. 7. 18:35
반응형

안녕하세요!

 

최근 며칠 동안 알고리즘과 영어 공부에 관련된 포스트를 집중적으로 올리느라 개발과 관련 된 포스트는 하나도 올리지 않았네요. 하지만 책으로 계속 공부를 하고 있었기 때문에 마냥 쉬고 있었던 것만은 아닙니다. 이번 포스트에서는 자바스크립트의 스코프 개념에 대하여 알아볼 텐데요. 중요한 개념이므로 같이 열심히 공부했으면 좋겠습니다! 그럼

 

 

스코프 (Scope)

먼저, 스코프의 정의에 대하여 알아보도록 하겠습니다. 스코프는 '범위' 라는 뜻을 가지고 있는 영어 단어이며 개발에도 많이 사용되는 용어입니다. 한가지 예를 들자면, 만약 A, B, C 업무를 이번 스프린트에서 끝내기로 했는데 나중에 D 가 추가 되었다고 가정합시다. 하지만 어떠한 사정 (인력 혹은 시간 부족) 으로 인하여 D 업무를 처리할 수 없을 때 D 업무는 out of scope 라고 말할 수 있습니다. 우리가 예상했던 범위의 바깥에 있다는 소리죠. 그냥 알아두시면 좋을 것 같아서 주저리 주저리 해봤습니다.

 

그렇다면, 자바스크립트에서 스코프는 어떠한 의미를 가질까요?

모든 식별자 (변수 이름, 함수 이름, 클라스 이름 등) 는 자신이 선언 된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다. 이를 스코프라 한다. 즉, 스코프는 식별자가 유효한 범위를 말한다.

책 모던 자바스크립트 Deep Dive 에서 발췌

 

자바스크립트에서 스코프의 종류는 크게 두 가지로 나눌 수 있습니다. 바로 전역 스코프 (global scope)  지역 스코프 (local scope) 입니다. 

 

그럼, 코드 예제를 통해 더욱 자세하게 알아보도록 하겠습니다.

이번 코드 예제에서는 let, const 키워드 대신 var 키워드를 사용하여 변수를 선언할 것입니다. 하지만 ES6 가 도입 된 이후에는 let, 그리고 const 키워드를 사용하는 것을 권장합니다. var, let, const 키워드에 관한 것은 다음 포스트에서 다루겠습니다.
var sameVariableName = 'global scope';

function testFunction() {
  var sameVariableName = 'local scope';
  console.log(sameVariableName); // "local scope"
}

testFunction();
console.log(sameVariableName); // "global scope"

자바스크립트 엔진은 이름이 같은 두 개의 변수 중에서 어떤 변수를 참조하여 출력 할 것인지 결정해야 하는데, 이를 식별자 결정 (identifier resolution) 이라고 합니다. 

* 식별자는 스코프 내에서 유일하게 존재해야 하는데 우리가 흔히 사용하는 ID 가 IDentifier 의 줄임말 입니다.

 

다시 위의 코드를 보시면 같은 이름의 변수를 선언하였지만, 에러가 발생하지 않았고 각기 다른 값이 출력 되었습니다. 그 이유는 변수가 선언 된 스코프가 다르기 때문입니다. 함수 바깥의 sameVariableName 전역 스코프 (global scope) 내에서 선언 되었고, 함수 내의 변수는 지역 스코프 (local scope) 내에서 선언이 된 것이죠.

 

한 가지 명심할 점은, 만약 전역 스코프 내에 선언 된 sameVariableName 을 지우고 지역 스코프 내에 선언한 변수만 남겨놓고 위의 코드를 다시 실행시킨다면, ReferenceError 가 발생하게 됩니다. 이를 통해 우리는 함수 내 (지역 스코프) 에서 선언 된 변수는 함수 내부에서만 사용이 가능하다는 것을 확인할 수 있습니다.

지역 스코프 내 변수만을 선언 한 출력 결과

반대로 전역 스코프 내에 선언 된 변수를 놔두고, 지역 스코프 내에 선언 된 변수를 삭제하고 프로그램을 실행 시킨다면, 'global scope' 가 두 번 출력 될 것입니다. 전역 변수는 어디에서든 참조가 가능하기 때문이죠.

전역 스코프 내 변수만을 선언 한 출력 결과

프로그래밍 언어에서는 스코프를 통해 식별자인 변수 이름의 충돌을 방지하여 같은 이름의 변수를 사용할 수 있게 하는데, 이는 각각 다른 폴더에 같은 이름의 파일을 생성하는 것과 같은 개념입니다.

 

스코프 체인

자바스크립트에서는 함수 내부에 또 다른 함수를 정의할 수 있는데, 이를 중첩 함수 (nested function) 라 하고, 중첩 함수를 포함하고 있는 함수는 외부 함수 (outer function) 라고 합니다. 함수는 중첩될 수 있으므로 함수의 지역 스코프도 중첩될 수 있습니다. 이는 스코프가 함수의 중첩에 의해 계층적 구조를 갖는다는 것을 의미합니다.

var x = "global x";
var y = "global y";

function outer() {
  var z = "outer function's local z";
  
  console.log(x); // "global x"
  console.log(y); // "global y"
  console.log(z); // "outer function's local z"
  
  function inner() {
    var x = "inner function's local x";
    
    console.log(x); // "inner function's local x"
    console.log(y); // "global y"
    console.log(z); // "outer function's local z"
  }
  
  inner();
}

outer();

console.log(x); // "global x"
console.log(z); // ReferenceError

이렇게 스코프가 계층적으로 연결 된 것을 스코프 체인이라고 합니다.

 

자바스크립트 엔진은 스코프 체인을 따라 변수를 참조하는 코드의 스코프에서 시작해서 상위 스코프 방향으로 이동하며 선언된 변수를 검색합니다. 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조할 수 있지만, 하위 스코프에서 유효한 변수를 상위 스코프에서 참조할 수 없습니다.

 

함수 레벨 스코프

ES6 가 아직 도입 되기 이전에 자바스크립트에서는 오직 var 키워드만을 이용하여 변수 선언이 가능했습니다. 또한 C 나 Java 등 다른 프로그래밍 언어와는 다르게 코드 블록 (if, for, while, try/catch 등) 이 아닌 오직 함수 블록만을 지역 스코프로 인정했습니다. 이러한 특성을 함수 레벨 스코프 (function-level scope) 라고 하는데요, 코드 예제로 확인해 보겠습니다.

var aVar = 1; // 전역 변수 aVar 선언과 초기화

while (true) {
  var aVar = 10; // 전역 변수 aVar 중복 선언과 초기화
  break;
}

function testFunction() {
  var aVar = 20; // 지역 변수 aVar 선언과 초기화
  console.log(aVar);
}

console.log(aVar); // 10
testFunction(); // 20

만약 while 코드 블록 안의 aVar 가 지역 스코프였다면, 맨 아래의 console.log(aVar) 는 1 을 출력해야 합니다. 하지만 while 코드 블록은 지역 스코프가 아닌 전역 스코프로 인식이 되기 때문에 10 이 출력이 된 것이죠. 또한 함수 안의 aVar 는 함수 블록 안에서 선언 된 변수이기 때문에 지역 스코프로 인정이 되어 함수 레벨 스코프를 갖게 됩니다. 따라서 전역 변수 값이 아닌 함수 내부에서 선언 & 초기화 된 20 이 출력 되었습니다.

 

한가지 의문을 가지실 수 있는데 (가지셔야 합니다!!), 전역 스코프 내의 while 코드 블록에서 저는 var 키워드를 이용하여 aVar 변수를 를 다시 선언하고 초기화 하였습니다. 그럼에도 불구하고 에러가 발생하지 않았죠. 이것이 var 키워드를 사용함에 있어 가장 큰 문제라고 생각하는데요, var 키워드를 사용하면 중복 선언이 가능하게 되어 의도치 않은 결과를 야기할 수 있게 됩니다. 더 자세한 이야기는 다음번에 let, const 키워드와 함께 다뤄보도록 하겠습니다.

 

렉시컬 스코프 (Lexical Scope)
var x = 1;

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

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

foo(); // ?
bar(); // ?

위 예제의 실행 결과는 bar 함수의 상위 스코프가 무엇인지에 따라 결정 되는데, 두 가지 패턴을 예측할 수 있습니다.

  1. 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정. 이 경우에 bar 함수의 상위 스코프는 'foo 함수의 지역 스코프''전역 스코프' 이다. 이를 동적 스코프 (dynamic scope) 라 한다.
  2. 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정. 이 경우에 bar 함수의 상위 스코프는 '전역 스코프' 이다. 이를 정적 스코프 (static scope) 혹은 렉시컬 스코프 (lexical scope) 라 한다.

자바스크립트를 비롯한 대부분의 프로그래밍 언어는 렉시컬 스코프를 따릅니다. 따라서, 위 코드 예제의 출력값은

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

이 됩니다.

 

마무으리

이번 포스트를 작성하게 된 계기는 최근에 언급한 적이 있지만 자바스크립트 책 한권을 사서 정독을 하고 있습니다. 책이 매우 두껍고 내용도 많아 버거운 감이 없지 않아 있지만, 몰랐던 내용들이 너무나도 많기 때문에 많은 도움이 되고 있습니다 (하루가 48시간이었으면....). 저는 스코프에 대한 개념은 자세히 모르고 있기도 했고, 읽으면서 매우 중요한 개념이라 생각이 되어 꼭 포스팅 해야겠다고 생각했습니다.

 

이 포스트가 자바스크립트에서 스코프가 어떤 개념인지에 대해 확실하게 이해할 수 있도록 도움이 되었으면 합니다. 저는 개인적으로 많이 도움이 되었다고 생각하구요. 다음 포스트에서는 var, let, const 키워드에 대하여 알아보도록 하겠습니다. 긴 글 읽느라 고생하신 여러분 자신에게 잠시의 휴식을 제공하셔도 좋을 것 같네요. 그럼 안녕~

반응형