Scope 스코프

Sangwook Park·2022년 2월 9일
0
post-custom-banner

해당 시리즈는 Leonardomso의 33 Concepts Every JavaScript Developer Should Know 를 보고 공부, 정리한 시리즈이며, 자세한 내용은 링크를 확인하길 바란다.

이번 장에서는 기초적인 개념인 스코프 Scope 에 대해서 얘기보자.

스코프란?

식별자의 유효 범위이자 이름 공간

스코프는 식별자가 유효한 범위를 의미한다.

하나씩 살펴보자.

먼저 식별자란, 변수 이름, 함수 이름, 클래스 이름 등 메모리 공간에 저장되어 있는 값을 구별할 수 있는 이름을 의미한다. 즉, 식별자는 메모리 주소에 붙인 이름이다.

다음으로 유효한 범위란, 다른 코드가 자신을 참조할 수 있는 범위를 가리킨다. 유효한 범위에 대해서는 차근차근 알아가보자.


스코프의 가장 핵심적인 역할은 유효 범위를 만드는 것이다.

아래 코드를 보면서 유효 범위에 대해 좀 더 쉽게 알아보자.

var x = 'apple'

function a() {
  var x = 'banana'
  console.log(x) // 1
}

a()
console.log(x) // 2

코드에서 보면 같은 x 라는 변수 2개를 선언했으며, 1번과 2번 코드에서 각각 어떤 x 식별자를 참조할지 헷갈릴 수 있다.

이 때, 판단의 기준이 되는 것이 어떤 스코프에서 선언되었는지이다.

위 코드에서는 가장 처음에 선언된 x 는 전역 스코프를 갖기에 어디서든 참조를 할 수 있다. 반면에 함수 a 안에 선언된 x 는 함수 내부에서만 참조를 할 수 있고 외부에서 참조할 수 없다.

그렇기에 같은 x 라는 이름을 갖지만 두 변수는 다른 스코프에 있는 다른 식별자인 것이다.

아래의 그림처럼 2개의 다른 공간에 있다고 생각하자.

스코프1

이처럼 변수가 어떤 스코프, 어떤 유효 범위 안에 선언되었는지 파악하는 것은 중요하다.

왜 그럼 스코프가 필요할까?

만약 스코프가 없다면, 하나의 프로그램에는 같은 변수명을 쓸 수 없고 매번 각기 다른 변수명을 갖는 변수를 사용해야 할 것이다.

즉, 하나의 코드를 짜는데 변수의 이름을 중복없이 계속 짜야한다면 매우 비효율적일 것이다.

컴퓨터에 저장된 파일 시스템봐도 그렇다.

우리가 파일이름을 정할 때 항상 다른 이름으로 정하지 않는다. 문서1 이라는 이름의 파일이 컴퓨터 전체에 딱 하나만 존재하는 것이 아니라 각기 다른 폴더에 존재할 수 있다. 이는 네임스페이스가 폴더마다 개별적으로 존재하기 때문이다.

이처럼 스코프도 개별적인 네임스페이스를 제공하여 좀 더 효율적인 코드 작성을 가능케 해준다.

스코프의 분류

스코프는 크게 전역 스코프와 지역 스코프로 구분된다.

변수는 자신이 선언된 위치에 의해 자신의 스코프가 결정되며 위치에 따라 크게 전역 스코프 (global scope) 과 지역 스코프 (local scope) 로 나눌 수 있다.

전역 스코프 global scope

  • 전역 스코프는 코드의 가장 바깥쪽 영역을 가리킨다.
  • 전역 스코프에 선언된 모든 변수는 전역 변수라 하며, 전역 변수는 어디서든 참조될 수 있다.

지역 스코프 local scope

  • 지역 스코프는 { } 에 둘러싸여 있는 영역을 가리킨다.
  • 크게 블록 레벨 스코프와 함수 레벨 스코프로 나눌 수 있으며, 이는 뒤에서 알아보자.
  • 지역 스코프에서 선언된 변수는 지역 변수, 로컬 변수라 하며, 지역 변수는 자신의 스코프와 하위 지역 스코프(자신의 스코프 내부에 형성된 지역 스코프) 에서만 참조 가능하다.

아래 그림을 보면서 좀 더 알아보자.

스코프2

<그림 스코프2>

가장 바깥에 있는 영역이 전역 스코프이며, local_one 함수 내부는 지역 스코프, 그리고 local_two 함수 내부의 영역은 지역 스코프이자 local_one 함수 스코프의 하위 지역 스코프라 할 수 있다.

이처럼 중첩된 함수를 통해, 스코프는 계층적인 구조를 갖으며, 이를 연결하는 개념인 스코프 체인에 대해서 알아보자.

스코프 체인

스코프는 계층적인 구조로 연결되어 있다.

스코프 체인은 말 그대로, 스코프가 계층적으로 연결된 것을 의미한다.

스코프2 그림의 스코프 체인을 그리면 아래와 같다.

스코프3

스코프 체인이 중요한 이유는 자바스크립트 엔진이 스코프 체인을 통해 스코프를 상위 이동하면서 선언된 변수를 찾기 때문이다.

변수 탐색

스코프 체인을 통해 자바스크립트 엔진은 현재 자신의 위치에서 상위 스코프로 이동하며 선언된 변수를 찾는다.

스코프2 그림에서 4, 5, 6 번 코드의 출력값을 찾아보면서 스코프 체인을 통해 어떻게 변수 탐색을 하는지 알아보자.

  • 4번 : 변수 a 출력하기

    • 변수 a 를 찾아보기 위해 먼저 현재 4번 코드가 위치한 스코프(local two 지역 스코프)부터 탐색을 한다.
    • local_two 함수 내부에 선언한 변수 a 가 존재하기에 a 를 출력한다.
    • 출력값 : local 2 a
  • 5번 : 변수 b 출력하기

    • 변수 b 는 현재 5번 코드가 위치한 스코프(local two 지역 스코프)에 존재하지 않는다.
    • 그렇기에 상위 스코프인 local one 지역 스코프 를 탐색한다.
    • local one 지역 스코프 에도 변수 b 는 선언되어 있지 않기에 다시 상위 스코프를 탐색한다.
    • local one 지역 스코프 의 상위 스코프는 전역 스코프 이며, 전역 스코프에서는 선언된 변수 b 가 존재하기에 b 를 출력한다.
    • 출력값 : global b
  • 6번 : 변수 c 출력하기

    • 변수 c 는 현재 6번 코드가 위치한 스코프(local two 지역 스코프)에 존재하지 않는다.
    • 그렇기에 상위 스코프인 local one 지역 스코프 를 탐색한다.
    • local one 지역 스코프 에는 선언된 변수 c 가 존재하기에 c 를 출력한다.
    • 출력값 : local one c

지금까지 봤던 것처럼 스코프 체인을 통해서 변수를 탐색할 때 해당 변수를 참조하는 코드가 위치한 스코프에서 시작하여 상위 스코프로 이동하며 찾는다!

절대 하위 스코프로 내려가서 변수를 탐색하지 않는다.

정리하자면,

상위 스코프에서 선언된 변수를 하위 스코프에서 참조할 수 있지만, 하위 스코프에서 선언된 변수를 상위 스코프에서는 참조할 수 없다.

++

지금까지 그림의 예시를 따라가면서 설명했기에 변수를 기준으로 스코프 체인을 설명했지만, 앞서 스코프에서 정의한 것처럼 모든 식별자는 다 스코프 체인을 따른다.

// 함수 스코프 체인
function one() {
  console.log('global function one')
}

function two() {
  function one() {
    console.log('local function one')
  }

  one() // local function one
}

two()
one() // global function one



앞서, 지역 스코프는 크게 함수 레벨 스코프와 블록 레벨 스코프로 나뉜다고 했다. 하나씩 살펴보자.

Function Level Scope

함수에 의해서 생성된 지역 스코프

함수를 선언할 때 생성되는 스코프로 함수 선언 시 작성하는 { } 블록 내의 범위를 가리킨다.

함수 내에서 선언된 변수는 함수 내에서만 유효하며 함수 외부에서는 참조할 수 없다.

아래 코드를 보자.

function abc() {
  var c = 'wow'
  console.log(c)
}

abc() // wow
console.log(c) // ReferenceError: c is not defined

함수 abc 스코프에서 선언된 변수인 c 는 함수 내부에 호출할 수 있지만, 외부에서 호출할 수 없기에 ReferenceError 가 발생하는 것을 알 수 있다.

var

기본적으로 자바스크립트는 함수 레벨 스코프 기반 언어이다.

그 이유는 ES6 전까지 자바스크립트에서 변수를 선언할 수 있는 키워드는 var 하나였고, var 는 함수의 코드 블록만 지역 스코프로 인정하였기 때문이다.

즉, var 를 사용하면 블록 레벨 스코프를 지역 스코프로 인정하지 않았다. 아래 코드를 보자.

var x = 1

if (ture) {
    var x = 3
}

console.log(x) // 3

if 문의 블록 스코프를 지역 스코프로 인정하지 않기 때문에, if 문 안에서 재선언한 x 라는 변수는 지역 변수가 아닌 전역 변수 취급을 받아 3 을 출력한다.

이러한 성격 때문에 의도치 않는 문제를 발생시킬 수 있다.

다시 코드를 한번 보자.

var i = 0

for (var i = 0; i < 5; i++) {
    console.log(i) // 0 1 2 3 4
}

console.log(i) // 5

다른 프로그래밍 언어를 사용해보셨던 분들은 보통 for 문 내에서 사용하는 i 와 같은 인덱스 변수는 보통 그 for 문 안에서만 유효하게 사용해왔을 것이다.

하지만 위 코드처럼 for 문 안에서 var 를 통해 i 변수를 선언하면, 이 i 변수는 함수 스코프 안에 있는 것이 아니기에 전역 변수로 재선언된 것이며, for 문을 진행함에 따라 마지막 코드에서 5 를 출력한다.

이처럼 var 는 의도치 않게 변수의 값을 변경하거나 변수를 설정하는데 있어서 제약을 준다.

이에 ES6 부터는 블록 스코프를 사용할 수 있는 letconst 키워드가 도입되었다.



Block Level Scope

블록에 의해서 생성된 지역 스코프

Block Level Scope는 말그대로 { } 에 둘러싸여 있는 범위를 의미한다.

이 때 { } 는 함수를 포함하여 if, for, while, try/catch 문 등 모든 코드 블록을 가리킨다.

블록 스코프를 따르며 블록 스코프 내에서 선언된 변수들은 블록 내에서만 유효한 지역 변수이다.

앞서 var 를 통해 선언된 변수들은 함수 스코프만 따르며, 블록 스코프는 전역 스코프 취급을 하는 것을 보았다.

이에 ES6 부터 블록 스코프를 따르는 letconst 키워드가 추가 되었다.

let i = 0

for (let i = 0; i < 5; i++) {
    console.log(i) // 0 1 2 3 4
}

console.log(i) // 0

앞서 var 를 봤을 때 변수 ivar 키워드를 통해 선언한 경우, 블록 스코프를 따르지 않기에 마지막 출력이 5 가 되는 것을 보았다.

하지만, 블록 스코프를 따르는 let 을 통해 변수 i 를 선언할 경우, for 문 내에 있는 i 와 전역에 있는 i 는 별개의 변수로 처리되기 때문에 마지막 출력 0 이 되는 것을 볼 수 있다.

마찬가지로 함수도 블록 스코프에 포함되기에 var 와 같은 기능을 하는 let 이나 const 를 볼 수 있다.

function abc() {
  let c = 'wow'
  console.log(c)
}

abc() // wow
console.log(c) // ReferenceError: c is not defined



Lexical Scope

식별자를 어디서 호출하는지가 아니라 어떤 스코프에 선언하였는지에 따라 스코프를 결정한다.

앞서 스코프의 정의에 대해서 설명할 때, 식별자가 선언된 위치에 의해 유효 범위가 결정된다고 설명하였다.

이는 자바스크립트가 Lexical Scope 를 따르기 때문이다. (정적 스코프라고도 한다.)

렉시컬 스코프 Lexical Scope 란,

선언된 시점을 기준으로 스코프를 결정하는 것을 의미한다.

즉, 정의된 변수나 함수가 호출된 위치가 아니라 선언된 위치를 기준으로 스코프가 선언되고 계층이 짜여지는 것이다.

지금까지 살펴본 모든 설명과 예시 코드는 렉시컬 스코프에 기반으로 두고 진행되어 왔다.

렉시컬 스코프와 반대로 선언된 위치가 아니라 호출된 위치를 기준으로 스코프를 결정할 수 있다.

이는 동적 스코프 Dynamic Scope 라고 부른다.

아래 코드를 보면서 둘의 차이를 확인해보자.

let hi = 'hi';

function morning() {
  let hi = 'good morning'
  hello()
}

function hello() {
  console.log(hi);
}

morning()

함수 morning 을 실행했을 때,hello() 를 통해 얻는 출력값은 다음과 같다.

렉시컬 스코프의 경우 : hi

동적 스코프의 경우 : good moring


정리하자면,

렉시컬 스코프는 함수나 변수가 선언된 시점을 기준으로,

동적 스코프는 함수나 변수가 실행되는 시점을 기준으로 스코프를 결정한다.



정리

스코프는 식별자의 유효한 범위를 의미하며, 스코프에 따라 식별자의 위치가 결정된다.

자바스크립트는 선언된 시점을 기준으로 스코프를 결정하는 렉시컬 스코프를 따른다.

스코프는 크게 전역 스코프와 지역 스코프로 나뉜다.

전역 스코프 : 코드의 가장 바깥쪽 영역

지역 스코프 : { } 에 둘러싸인 영역

지역 스코프는 함수 레벨 스코프와 블록 레벨 스코프로 나눌 수 있다.

함수 레벨 스코프 : 함수 선언 시 { } 에 둘러싸인 영역을 가리키며 var 키워드로 선언된 변수가 해당 스코프를 따른다.

블록 레벨 스코프 : 모든 { } 에 둘러싸인 영역을 가리키며 letconst 키워드로 선언된 변수가 해당 스코프를 따른다.



참고 자료

모던 자바스크립트 Deep Dive

노마드 코더 유튜브

MDN 문서 - variable scope

위키백과 - Scope

Understanding Scope in JavaScript

profile
자신감 충전중..
post-custom-banner

0개의 댓글