Scope

스코프 (scope)는 변수와 상수, 매개변수와 언제 어디서 정의되는지 결정한다.
함수 매개변수가 함수 바디 안에만 존재하는 것도 스코프의 한 예이다.

  function f(x) {
    return x + 3;
  }

  f(5); // 8
  x; // ReferenceError: x is not defined

x가 잠시나마 존재했으니 x + 3을 계산할 수 있었는데 함수 바디를 벗어나면 x는 존재하지 않는 것처럼 보인다.
따라서 우리는 x의 스코프가 함수f라고 말한다.

변수의 스코프가 어떤 함수라고 말할 때는, 함수를 실제 호출할 때까지는 함수 바디의 정해진 매개변수가 존재하지 않음을 반드시 상기해야 한다. 함수는 여러 번 호출할 수 있고 함수를 호출할 때마다 매개변수가 나타나고, 함수가 제어권을 반환하면 스코프 밖으로 사라진다.

스코프와 존재

변수가 존재하지 않으면 그 변수는 스코프 안에 '있지 않음'을 직관적으로 알 수 있다.
즉, 아직 선언하지 않은 변수나 함수가 종료되면서 존재하지 않게 된 변수는 분명 스코프 안에 '있지 않다.'

스코프와 존재를 구분해야 한다.
가시성 (visivility)이라고도 불리는 스코프는 프로그램의 현재 실행 중인 부분, 실행 컨텍스트에서 현재 보이고 접근할 수 있는 식별자들을 말한다. 반면 존재한다는 말은 그 식별자가 메모리가 할당된 무언가를 가리키고 있다는 뜻이다.

정적 스코프

자바스크립트의 스코프는 정적이다. 소스 코드만 봐도 변수가 스코프에 있는지 판단할 수 있다는 뜻이다.

const x = 3;

  function f() {
    console.log(x);
    console.log(y);
  }
  // 새 스코프
  {
    const y = 5;
    f();
  }

변수 x는 함수 f를 정의할 때 존재하지만, y는 아니다. 다른 스코프에 존재하며 y를 선언하고 그 스코프에서 f를 호출하더라도, f를 호출하면 x는 그 바디안의 스코프에 있지만 y는 그렇지 않다.

자바스크립트의 정적 스코프는 전역 스코프 (global scope)블록 스코프 (block scope), 함수 스코프 (function scope)에 적용된다.

전역 스코프

프로그램을 시작할 때, 즉 어떤 함수도 호출하지 않았을 때 실행 흐름은 전역 스코프에 있다. 전역 스코프에서 선언된 것을을 전역 변수이다.

간단한 예시로 사용자의 이름과 나이를 보관하며, 그 정보를 사용하는 몇몇 함수가 있다.

전역변수 사용

let name = "Irena"; // 전역
let age = 25; // 전역

function greet() {
    console.log(`Hello ${name}`);
}
function getBirthYear() {
    return new Date().getBirthYear() - age;
}

이 방법의 문제는 함수가 호출하는 스코프에 대단히 의존적이라는 것이다. 어떤 함수든, 프로그램 어디에서든 상관없이 name 값을 바꿀 수 있기 때문이다.

그래서 위 방법 보다는 사용자 정보를 단일 객체에 보관하는 방법이 더 좋다.

let user = {
    name = "Irena";
    age = 25;
}

이제 함수를 고쳐서 전역 스코프에 의존하지 않게 만들어 보자.

function greet(user) {
    console.log(`Hello ${user.name}`);
}
function getBirthYear() {
    return new Date().getBirthYear() - user.age;
}

블록 스코프

블록은 문을 중괄호로 묶은 것 으로 그 블록의 스코프에서만 보이는 식별자를 의미한다.

console.log('before block');
{
    console.log('inside block');
    const x = 3;
    console.log(x);
}
console.log(x) // ReferenceError: x는 정의되지 않았습니다.

x는 블록 안에서 정의됐고, 블록을 나가는 즉시 x도 스코프 밖으로 사라지므로 정의되지 않은 것으로 간주합니다.

변수 숨기기

스코프가 중첩되는 경우가 있습니다.

{
    // 외부 블록
    let x = 'blue';
    console.log(x); // 'blue'
    {
        // 내부 블록
        let x = 3;
        console.log(x); // 3
    }
    console.log(x); // 'blue';
}

console.log(x); // "undefined" x는 스코프에 있지 않다.

내부 블록의 x는 외부 블록에서 정의한 x와는 이름만 같을 뿐 다른 변수이므로 외부 스코프의 x를 숨기는 효과가 있다.

{
    // 외부 블록
    let x = {color: 'blue'};
    let y = x;
    let z = 3;
    {
        // 내부 블록
        let x = 5; // 바같 x 가려짐
        console.log(x); // 5
        console.log(y.color) // 'blue'
        y.color = 'red';
        console.log(z) // 3        
    }
    console.log(x.color); // 'red'
    console.log(y.color); // 'red'
    console.log(z) // 3
}

스코프의 계층적인 성격 때문에 어떤 변수가 스코프 안에 있는지 확인하는 스코프 체인 이란 개념이 생겼다. 현재 스코프 체인에 있는 모든 변수는 스코프에 있는 것 이며, 숨겨지지 않았다면 접근할 수 있다.

함수, 클로저, 정적 스코프

함수는 여기서 정의하고 저기서 호출하는 식으로 사용하므로 함수의 스코프를 제대로 이해하려면 더 고민해야 합니다.

최신 자바스크립트에서는 함수가 필요한 곳에서 즉석으로 정의할 때가 많다. 함수를 변수나 객체 프로퍼티에 할당하고, 배열에 추가하고, 다른 함수에 전달하고, 함수가 함수를 반환하고, 심지어 이름조차 없을 때도 많다.

함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 경우가 많은데,
이런 것을 보통 클로저 라고 부른다. 스코프를 함수 주변으로 좁히는 것 (closing) 이라 생각해도 된다.

let globalFunc; // 정의되지 않은 전역 함수
{
    let blockVar = 'a'; // 블록 스코프에 있는 변수
    globalFunc = function() {
        console.log(blockVar);
    }
}

globalFunc(); // 'a';

globalFunc는 블록 안에서 값이 할당됐다. 이 블록 스코프와 그 부모인 전역 스코프가 클로저를 형성하고 globalFunc를 어디서 호출하든, 이 함수는 클로저에 들어있는 식별자에 접근이 가능하다.

즉시 호출하는 함수 표현식 IIFE

함수 표현식을 사용하면 즉시 호출하는 함수 표현식 ( IIFE )을 만들 수 있다. IIFE는 함수를 선언하고 즉시 실행하고 아래 예시와 같은 형태를 취한다.

(function() {
    // IIFE 바디
})();

함수 표현식으로 익명 함수를 만들고 그 함수를 즉시 호출했다. IIFE 장점은 내부에 있는 것들이 모두 자신만의 스코프를 가지지만, IIFE 자체는 함수이므로 그 스코프 밖으로 무언가를 내보낼 수 있다는 것이다.

ES6에서 블록 스코프 변수를 도입하면서 IIFE가 필요한 경우가 줄긴 했지만 여전히 널리 쓰이고 있다. 클로저를 만들고 클로저에서 무언가 반환받을 때에는 유용하게 쓸 수 있다.

함수 스코프와 호이스팅

let으로 변수를 선언하면, 그 변수는 선언하기 전에는 존재하지 않는다. var로 선언한 변수는 현재 스코프 안이라면 어디서든 사용할 수 있으며, 심지어 선언하기도 전에 사용할 수 있다. var로 선언한 변수는 끌어올린다는 뜻의 호이스팅 hoisting 이라는 매커니즘을 따르기 때문이다. 중요한 것은 선언만 끌어올려진다는 것이며, 할당은 끌어올려지지 않는다.

if(x !== 3) {
    console.log(y);
    var y = 5;
    if(y === 5) {
        var x = 3;
    }
    console.log(y);
}
if(x === 3) {
    console.log(y);
}

이렇게 변수를 선언하기도 전에 사용하면 불필요하게 혼란스럽고, 에러에도 취약하다.
호이스팅의 동작 방식을 보여주기 위한 예시이고 자바스크립트는 위 코드를 이렇게 해석한다.

var x;
var y;
if(x !== 3) {
    console.log(y);
    var y = 5;
    if(y === 5) {
        var x = 3;
    }
    console.log(y);
}
if(x === 3) {
    console.log(y);
}

함수 호이스팅

var로 선언된 변수와 마찬가지로, 함수 선언도 스코프 맨 위로 끌어올려진다.ㅡ 따라서 함수르 선언하기 전에 호출할 수 있다.

f();    // 'f';

function f() {
    console.log('f');
}