LHJ

I'm a FE developer.

14.2.2 스코프와 비동기적 실행

20 May 2020 » js_lj

14.2.2 스코프와 비동기적 실행

비동기적 실행에서 혼란스럽고 에러도 자주 일어나는 부분은 스코프와 클로저가 비동기적 실행에 영향을 미치는 부분입니다.
함수를 호출하면 항상 클로저가 만들어집니다.
매개변수를 포함해 함수 안에서 만든 변수는 모두 무언가가 자신에 접근할 수 있는 한 계속 존재합니다.

이 예제는 이미 봤었지만, 중요한 사실을 배울 수 있으니 반복해서 볼 가치가 있습니다.
countdown 함수에 대해 생각해 봅시다.
이 함수의 목적은 5초 카운트다운을 만드는 것입니다.

function countdown() {
    let i;              // i를 for 루프 밖에서 선언했습니다.
    console.log("Countdown");
    for (i=5; i>=0; i--) {
        setTimeout(function() {
            console.log(i===0 ? "GO!" : i);
        }, (5-i) * 1000)
    }
}

countdown();

이 예제를 머릿속에서 한 번 실행해 보십시오.
같은 예제를 테스트했을 때 예상대로 실행되지 않았던 것을 기억할 겁니다.
코드를 보면 5에서부터 카운트다운 할 것처럼 보입니다.
하지만 결과는 -1이 여섯번 반복될 뿐이고, “GO!”는 나타나지 않습니다.
이 예제를 처음 봤을 때는 var를 사용했습니다.
이번에는 let을 사용하긴 했지만, 변수를 for 루프 밖에서 선언했으므로 같은 문제가 벌어집니다.
for 루프가 실행을 마치고 i의 값이 -1이 된 다음에서야 콜백이 실행되기 시작합니다.
문제는, 콜백이 실행될 때 i의 값은 이미 -1이란 겁니다.

스코프와 비동기적 실행이 어떻게 연관되는지 이해하는 것이 중요합니다.
countdown을 호출하면 변수 i가 들어있는 클로저가 만들어집니다.
for 루프 안에서 만드는 콜백은 모두 i에 접근할 수 있고, 그들이 접근하는 i는 똑같은 i입니다.

이 예제에서 눈여겨 볼 것이 하나 더 있습니다.
for 루프 안에서 i를 두 가지 방법으로 사용했습니다.
i를 써서 타임아웃을 계산하는 (5-i)*1000 부분은 예상대로 동작합니다.
첫 번째 타임아웃은 0, 두번째 타임아웃은, 1000, 세번째 타임아웃은 2000입니다.
이 계산이 예상대로 동작한 것은 동기적으로 실행됐기 때문입니다.
사실 setTimeout을 호출하는 것 역시 동기적입니다.
setTimeout을 동기적으로 호출해야만 콜백을 언제 호출할지 계산할 수 있습니다.
비동기적인 부분은 setTimeout에 전달된 함수이고, 문제는 여기서부터 복잡해집니다.

이 문제는 즉시 호출하는 함수 표현식(IIFE)으로 해결했고, 좀 더 간단하게 i를 for 루프 선언부에서 선언하는 방식으로도 해결할 수 있었습니다.

function countdown() {
    console.log("Countdown:");
    for (let i=5; i>=0; i--) {  // 이제 i는 블록 스코프 변수입니다.
        setTimeout(function() {
            console.log(i===0 ? "GO!" : i);
        }, (5-i)*1000)
    }
}

여기서 주의할 부분은 콜백이 어느 스코프에서 선언됐느냐입니다.
콜백은 자신을 선언한 스코프(클로저)에 있는 것에 접근할 수 있습니다.
따라서 i의 값은 콜백이 실제 실행되는 순간마다 다를 수 있습니다.
이 원칙은 콜백뿐만 아니라 모든 비동기적 테크닉에 적용됩니다.