Programming/Javascript

[Javascript] You Don't Know JS (함수 vs 블록 스코프)

알로그 2020. 4. 6. 23:54
반응형

Chapter 3. 함수 vs 블록 스코프

3.1 함수 기반 스코프

각각의 선언된 함수는 저마다의 버블을 생성하지만 다른 어떤 자료구조도 자체적인 스코프를 생성하지 않음 -> 사실이 아님

function foo(a){
    var b = 2;
    //some code

    function bar(){
        //...
    }
    //more code
    var c = 3;
}

앞의 코드에서 foo()의 스코프 버블은 확인자 a, b, c와 bar를 포함함
따라서 foo() 바깥에서는 이들에게 접근할 수 없음

bar();
console.log(a, b, c);

하지만 이 모든 확인자 a, b, c, foo, bar는 foo() 안에서 접근할 수 있고, bar() 안에서도 이용할 수 있음

3.2 일반 스코프에 숨기

스코프를 이용해 숨기는 방식을 사용하는 이유 중 하나는 소프트웨어 디자인 원칙인 '최소 권한의 원칙과 관련이 있음
이 원칙은 모듈/객체의 API와 같은 소프트웨어를 설계할 때 필요한 것만 최소한으로 남기고 나머지는 숨겨야 한다는 것임

function doSomething(a){
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
}

function doSomethingElse(a){
    return a - 1;
}

var b;
doSomething(2); //15

더 적절하게 설계하려면 다음과 같이 비공개 부분은 doSomething() 스코프 내부에 숨겨야 함

function doSomething(a){
    function doSomethingElse(a){
        return a - 1;
    }

    var b;
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
}

doSomething(2); //15

이제 b와 doSomethingElse()는 외부에서 접근할 수 없어서 더는 바깥의 영향을 받지 않고 오직 doSomething()만이 이를 통제함

3.2.1 충돌회피

변수와 함수를 스코프 안에 숨기는 것의 또 다른 장점은 같은 이름을 가졌지만 다른 용도를 가진 두 확인자가 충동하는 것을 피할 수 있음

function foo(){
    function bar(a){
        i = 3;
        console.log(a+i);
    }

    for(var i=0; i<10; i++){
        bar(i * 2); // oops, 
    }
}

foo();

처음에만 3이 호출되고 bar()내부에서 "i = 3"으로 대입되면서 무한반복에 빠지게 됨(i<10인 상태로 머물게 됨)

bar() 내부의 대입문은 어떤 확인자 이름을 고르든 지역변수로 선언해서 사용해야 함
"var i = 3;"으로 변경하면 문제를 해결할 수 있음
또는 다른 변수명을 선택하면 됨

글로벌 '네임스페이스'

내부/비공개 함수와 변수가 적절하게 숨겨져 있지 않은 여러 라이브러리를 한 프로그램에서 불러오면 라이브러리들은 쉽게 충돌할 수 있음

이러한 라이브러리는 일반적으로 글로벌 스코프에 하나의 고유 이름을 가지는 객체 선언문을 생성함
이후 객체는 해당 라이브러리의 네임스페이스로 이용된다.
네임스페이스를 통해 최상위 스코프의 확인자가 아니라 속성 형태로 라이브러리의 모든 기능이 노출됨

var MyReallyCoolLibrary = {
    awesome: "stuff",
    doSomething: function(){
        //...
    },
    doAnotherThing: function(){
        //...
    }
};

모듈관리

다양한 의존성 관리자를 이용한 모듈 접근법이 있음
이 도구를 사용하면 어떤 라이브러리도 확인자를 글로벌 스코프에 추가할 필요없이, 특정 스코프로부터 의존성 관리자를 이용한 다양한 명시적인 방법으로 확인자를 가져와 사용할 수 있음

기억할 것은 이런 도구를 사용한다고 렉시컬 스코프 규칙에서 벗어날 수 있는것은 아님
의존성 관리자는 스코프 규칙을 적용해 모든 확인자가 공유 스코프에 누출되는 것을 방지하고, 우발적인 스코프 충돌을 예방하기 위해 충돌 위험이 없는 비공개 스코프에 확인자를 보관함

3.3 스코프 역할을 하는 함수

지금까지 변수나 함수 선언문을 바깥 스코프로부터 함수의 스코프 안에 숨기는 것을 살펴보았음

var a = 2;

function foo(){
    var a = 3;
    console.log(a); //3
}

foo();
console.log(a);     //2

이 방식은 이상적인 방식이 아님
첫째, foo()라는 이름의 함수를 선언해야 하는데, 이는 foo라는 확인자 이름으로 둘러싸인 스코프를 오염시킨다는 의미
또한 그 함수를 호출해야만 실제 감싼 코드를 실행할 수 있음

var a = 2;
(function foo(){
    var a = 3;
    console.log(a); //3
})();

console.log(a);     //2

이 코드에서 함수는 보통의 선언문이 아니라 함수 표현식으로 취급됨

함수선언문과 함수표현식의 중요한 차이는 함수 이름이 어디의 확인자로 묶이느냐와 관련이 있음
두번째 코드의 foo는 함수 자신의 내부 스코프에 묶여 바깥 스코프에서는 발견되지 않음
함수 이름 foo를 자기 내부에 숨기면 함수를 둘러싼 스코프를 불필요하게 오염시키지 않을 수 있음

3.3.1 익명 vs 기명

setTimeout(function(){
    console.log("I waited 1 second!");
}, 1000);

이런 방식을 '익명 함수 표현식'이라 부르는데, 이는 "function () ..."에 확인자 이름이 없기 때문임

익명 함수 표현식은 빠르고 쉽게 입력할 수 있어서 많은 라이브러리와 도구가 이 자바스크립트 특유의 표현법을 권장함
하지만 몇가지 단점이 있음

  1. 익명 함수는 스택 추적시 이름이 없어 디버깅이 힘듦
  2. 이름없이 스스로 재귀 호출을 하려면 불행히도 폐기 예정인 arguments.callee 참조가 필요함
  3. 이름은 읽을 수 있는 코드 작성에 도움이 되는데, 익명 함수는 이를 생략하므로 가독성 문제가 있을 수 있음

따라서 인라인 함수 표현식은 효과적이고 유용함 (앞의 단점 해결)

setTimeout(function timeoutHandler(){
    console.log("I waited 1 second!");
}, 1000);

함수 표현식을 사용할 떄 이름을 항상 쓰는 것이 좋음

3.3.2 함수표현식 즉시 호출

var a = 2;
(function foo(){
    var a = 3;
    console.log(a); //3
})();

console.log(a);     //2

함수를 둘러싼 첫 번째 ()는 함수를 표현식으로 바꾸고, 두번째 ()는 함수를 실행시킴
즉시 호출 함수 표현식(IIFE)는 익명 함수 표현식으로 가장 흔하게 사용됨

var a =  2;

(function IIFE(global){
    var a = 3;
    console.log(a); //3
    console.log(global.a);  //2
})(window);

console.log(a); //2

window 객체 참조를 global이라는 이름 붙인 인자에 넘겨서 글로벌 참조와 비 글로벌 참조 사이에 명확한 차이를 만듦

이 패턴의 다른 예제를 통해 기본 확인자 undefined의 값이 잘못 겹쳐 쓰여 예상치 못한 결과를 야기하는 예제를 보자

undefined = true;

(function IIFE(undefined){
    var a;
    if(a === undefined){
        console.log("Undefined is safe here!");
    }
})();

함수 표현식 def는 코드 후반부에 정의되어 코드 전반부에 정의된 IIFE 함수에 인자로 넘겨짐
결국 인자 함수 def가 호출되고 window가 global 인자로 넘겨짐

var a = 2;
(function IIFE(def){
    def(window);
})(function def(global){
    var a = 3;
    console.log(a); //3
    console.log(global.a);  //2
});

3.4 스코프 역할을 하는 블록

3.4.1 with

앞서 언급했듯 지양해야 할 구조이지만 블록 스코프의 형태를 보여주는 한 예로, with 문 안에서 생성된 객체는 바깥 스코프에 영향 주는 일 없이 with문이 끝날때까지만 존재함

3.4.2 try/catch

catch 부분에서 선언된 변수는 catch 블록 스코프에 속함

try{
    undefined();
}
catch(err){
    console.log(err);   // works
}

console.log(err);       // ReferenceError : 'err' not found

3.4.3 let

var foo = true;

if(foo){
    let bar = foo * 2;
    bar = something(bar);
    console.log(bar);
}

console.log(bar);   //ReferenceError

let을 이용해 변수를 현재 블록에 붙이는 것은 비명시적임

아래처럼 명시할 경우 나중에 리팩토링하면서 if문의 의미를 변화시키지 않고도 전체 블록을 옮기기가 쉬워짐

var foo = true;

if(foo){
    {
        let bar = foo * 2;
        bar = something(bar);
        console.log(bar);
    }
}

console.log(bar);   //ReferenceError

let을 사용한 선언문은 속하는 스코프에서 호이스팅 효과를 받지 않음

{
    console.log(bar);   //ReferenceError
    let bar = 2;
}

가비지콜렉션

function process(data){
    //do something interesting
}

var someReallyBigData = { ... };

process(someReallyBigData);

var btn = document.getElementById("my_button");

btn.addEventListener("click", function click(evt){
    console.log("button clicked");
}, /*capturingPhase=*/false)

클릭을 처리하는 click 함수는 someReallyBigData 변수가 전혀 필요없음
따라서 이론적으로 process() 함수 호출 후 수거할 수 도 있지만, click 함수가 해당 스코프 전체의 클로저를 가지고 있지 않기 때문에 수거 불가능

블록 스코프는 엔진에게 someReallyBigData가 더 필요없다는 사실을 명료하게 알려줘서 해결할 수 있음

function process(data){
    //do something interesting
}

{
    var someReallyBigData = { ... };
    process(someReallyBigData);
}

var btn = document.getElementById("my_button");

btn.addEventListener("click", function click(evt){
    console.log("button clicked");
}, /*capturingPhase=*/false)

3.4.4 const

키워드 const는 블록 스코프를 생성하지만 선언된 값은 고정됨 (상수)

var foo = true;

if(foo){
    var a = 2;
    const b = 3;
    a = 3;
    b = 4;   //error
}

console.log(a); //3
console.log(b); //ReferenceError

3.5 정리

  • 자바스크립트에서 함수는 스코프를 이루는 흔한 단위
  • 다른 함수 안에서 선언된 변수, 함수는 다른 스코프로부터 숨겨진 것(좋은 소프트웨어 디자인 원칙)
  • 블록 스코프는 함수만이 아니라 임의의 코드 블록에 변수와 함수가 속하는 개념
  • catch 부분은 블록 스코프를 가짐
  • if() {let a = 2;}에서 a는 if문의 {}블록 스코프에 자신을 붙임
반응형