본문 바로가기

스터디/모던 자바스크립트 딥 다이브

[딥 다이브 스터디 13 ~ 14장] 스코프

<모던 자바스크립트 딥 다이브> (이웅모, 위키북스) 를 읽고 공부한 내용입니다. 책의 내용을 그대로 적어 놓은 것이 아니기 때문에 오류가 있을 수 있습니다. 오류가 있다면 댓글을 통해 피드백 부탁드립니다.

❓ 급 퀴즈

아래 콘솔에는 무엇이 찍힐까요? 이유는 무엇일까요?

var x = "나는 전역 변수 x"

function foo(){
    console.log(x); // ???
    var x = "나는 지역 변수 x";
}

foo();
console.log(x); // '나는 전역 변수 x'

 

<13장 스코프>

[1] 스코프란?

스코프는 프로그래밍 언어에서 매우 중요한 개념이다. 프로그래밍 언어를 처음 배울 때, 스코프에 대해서 배우지 않더라도 어느정도 직관적으로 스코프가 무엇인지 인지할 수 있을 것이다.

function any() {
  var x = 1;
  var y = 2;
  return x + y;
}

console.log(any()); // 3

console.log(x, y); // x is not defined​
  1. 🤔 any를 호출하면 3이 반환되겠군...
  2. 🤔 바깥에는 x랑 y가 없으니 에러가 날 것 같은데....?

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

[1-1] 식별자 결정과 실행 컨텍스트

만약 변수의 이름이 같다면, 자바스크립트 엔진은 어떤 변수를 참조해야 할 것인지 결정해야 한다. 이를 식별자 결정이라 한다.

var x = "나는 전역 변수 x";

function foo() {
  var x = "나는 지역 변수 x";
  return x;
}

console.log(foo()); // '나는 지역 변수 x'

console.log(x); // '나는 전역 변수 x'

이렇게 자바스크립트 엔진은 참조해야 할 식별자를 결정한다. 자바스크립트 엔진은 코드의 문맥을 고려하는데 이는 실행 컨텍스트와 관련이 있다.

[1-2] var, let, const

변수를 선언하는 키워드 또한 스코프와 관련되어 있다. 동일한 스코프 내에서 var는 중복 선언이 되고, let, const는 되지 않는다. var가 중복으로 선언된다는 점은 예상치 못한 버그를 발생시킬 수 있다.

function outer(){
    let a = "나는 outer의 a";

    function inner(){
        let a = "나는 inner의 a";
        return a;
    }

    console.log(inner()); // '나는 inner의 a'

    return a;
}

console.log(outer()); // '나는 outer의 a'

var a = 1;
console.log(a) // 1

var a = 2;
console.log(a) // 2

[2] 스코프의 종류

스코프는 전역 스코프지역 스코프, 2가지 경우로 나뉜다. 전역은 말 그대로 스크립트의 가장 최상단이다.

지역 스코프는 함수, 클래스(클래스도 내부적으로 함수이므로), 모듈 스코프도 포함한다. 또, 모듈 스코프의 최상단은 전역이 아니다.

[3] 스코프 체인

function outer(){
    let a = "나는 outer의 a";

    function inner(){
        return a;
    }

    console.log(inner()); // '나는 outer의 a'

    return a;
}

console.log(outer()); // '나는 outer의 a'

inner는 a가 없다. 그래서 자바스크립트 엔진은 더 상위 스코프에서 a가 있는지 찾아본다. 이것이 스코프 체인이다.

만약 전역 스코프까지 올라갔는데도 식별자를 찾지 못하면, 그제서야 에러가 발생한다. 따라서 이 말은,

  1. 스코프는 함수의 중첩에 의해 계층적 구조를 갖는다.
  2. 스코프 체인은 무조건 위로 향한다. 절대로 하위 스코프로 내려가서 식별자를 찾지 않는다.

변수 뿐 아니라 함수도 마찬가지이다.

function foo(){
    console.log("나는 전역 함수 foo");
}

function bar(){
    function foo(){
        console.log("나는 지역 함수 foo") // '나는 지역 함수 foo'
    }
    foo();
}

bar();

foo 함수는 전역에서 먼저 선언되었고, bar 함수 내에서 한번 선언된 후 호출됐다. 자바스크립트 엔진은 먼저 호출된 스코프 내에서 foo 함수를 찾는다.

[4] 함수 레벨 스코프

대부분 프로그래밍 언어는 함수 레벨 스코프 보다는 블록 레벨 스코프를 따른다. 함수 레벨 스코프를 따른다면 프로그래밍 흐름에 맞지 않는 문제들이 생긴다. var 키워드는 함수 레벨 스코프를 따른다. 이러한 문제를 보안하기 위해서 ES6에서 블록 레벨 스코프를 따르는 let과 const가 추가됐다.

[5] 렉시컬 스코프

함수의 스코프는

  1. 함수를 어디서 호출 했는지
  2. 함수를 어디서 정의 했는지

에 따라 상위 스코프를 결정한다.

var x = 1;

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

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

foo();
bar();
  1. foo 함수 호출
    • foo 함수를 호출하면 bar 함수가 실행된다. x를 출력하려 할 때 자바스크립트 엔진은 함수가 정의된 곳을 기준으로 스코프 체인을 통해 x를 찾는다.
    • 이 방식을 동적 스코프 라고 한다. 함수를 정의하는 시점에는 함수가 어디서 호출될 지 알수 없으므로 호출된 순간 스코프를 결정한다.
  2. bar 함수 호출
    • bar 함수 내부에서 x를 찾으려했지만 존재하지 않는다. 따라서 상위 스코프인 전역 스코프에서 x를 찾는다.
    • 이 방식을 렉시컬 스코프 또는 정적 스코프 라고 한다. 함수 정의가 평가되는 시점에 이미 상위 스코프가 정적으로 결정된다. 자바스크립트를 비롯한 대부분 프로그래밍 언어는 렉시컬 스코프를 따른다.

<14장 전역 변수의 문제점>

[1] 전역 변수의 생명주기

전역에 생성된 변수의 생명주기는 프로그램의 실행부터 종료까지 함께한다. 지역 스코프에 생성된 변수는 어떠한 참조도 되지 않는다면 가비지 컬렉션된다. (❓ 그러면 전역 스코프는 가비지 컬렉션이 되지 않는가?)

[1-1] 호이스팅은 스코프 단위로 동작한다.

이것은 아마 실행 컨텍스트가 스코프 단위로 생성된다는 것을 의미하는 것 같다. 아래 코드에서

var x = "나는 전역 변수 x"

function foo(){
    console.log(x); // undefined
    var x = "나는 지역 변수 x";
}

foo();
console.log(x); // '나는 전역 변수 x'

foo 함수 안에서 x는 호이스팅되어 undefined로 할당된 상태이다.

[2] 전역 변수의 문제점

  1. 스코프의 유효 범위는 좁을 수록 좋다. 하지만 전역에 선언한 변수는 모든 코드가 참조할 수 있으므로 가독성을 저해하고 예상치 못한 버그를 유발할 수 있다.
  2. 전역에 선언한 변수는 생명주기가 길다. 따라서 메모리 리소스를 오래 소비한다.
  3. 전역에 있는 변수를 참조하게 된다면, 스코프 체이닝이 깊어진다. 따라서 속도가 느리다.
  4. 네임스페이스 오염 - 같은 이름으로 변수를 만들게 된다거나 하면 의도치 않은 버그를 만나게 될 수 있다.

[3] 전역 변수의 사용을 억제하기

전역 변수를 반드시 사용해야하는 것이 아니라면, 지역 변수를 사용하여 스코프의 범위를 최대한 좁게 가져가는 것이 더 낫다.

  1. 즉시실행함수
  2. 네임스페이스 객체 사용
  3. 모듈 패턴으로 파일 분리

 


🔗 참고 자료

- <모던 자바스크립트 딥 다이브>

- 우테코 10분 테코톡 엘라님의 <Scope & Closure> / https://www.youtube.com/watch?v=PVYjfrgZhtU