본문 바로가기
Language/JavaScript

[JavaScript] 렉시컬 스코프, 클로저

by 며루치꽃 2020. 9. 3.

렉시컬 스코프

 

자바스크립트에서는 코드가 적힌 순간 변수의 유효범위가 정해진다다. 이것을 정적 유효범위, 렉시컬(어휘적) 스코프라고 부릅니다.

function main(){
    var name = 'skykchmin';
}

var로 선언한 name변수의 유효범위는 함수를 절대로 벗어날 수 없다. 즉 함수안에서 선언한 변수는 함수 밖에서 접근할 수 없다.

var name = 'kchmin';
function log(){
    console.log(name); 
}

function main(){
    var name = 'skykchmin'; 
    log();
}
main();

위 예의 실행 결과는 skykchmin 이다. main() 메서드가 실행하면서,  variable name이 있는지 찾아보고, 없으면 전체에서 찾아본다. 이때, 전역변수인 name의 값을 skykchmin로 변경하였기 때문에 print()메서드에서 전역변수인 name을 출력하므로 skykchmin를 출력한다.

 

var name = 'kchmin';
function log(){
    console.log(name); 
}

function main(){
    name = 'skykchmin'; 
    log();
}
main();

위 예의 실행 결과는 kchmin 이다. main()메서드의 name 변수는 전역변수인 name변수가 아니고 main()함수안에서 선언되어 함수 밖으로 벗어날 수 없기 때문이다. 

이렇게 코드가 적힌 순간 스코프가 정해지게 되는데 이것을 렉시컬 스코프라고 부른다. 

 

이러한 스코프의 유효범위는 변수 뿐만아니라 함수에도 동일하게 적용된다.

 

function print(){
    console.log('global');
}
function main(){
    function print(){
        console.log('local');
    }
    print();
}
main();

위의 예에서는, main()메서드에서는 지역함수 print()를 실행하므로, local을 출력한다.

자바스크립트 언어는 동적언어지만, 유효범위는 코드가 작성되는 순간 정해지는 정적인 특성을 가진다. 

 

클로져

 

for (var i=0; i<100; i++){
    setTimeout(function(){
        console.log(i);
    }, i*100);
}

setTimeout메소드는 첫번째 인자로 비동기콜백함수, 두번째인자로 실행할 시간(ms)으로 받는 함수이다. 0.1초마다 0에서 99까지 출력을 하게하려고 작성을 하였다. 

하지만 의도와는 다르게 100이라는 결과가 100번 출력된다. 그 이유는 반복문이 아래와 같이 실행되기 때문아다.

 

setTimeout(function(){
    console.log(i);
}, 0 * 1000)
setTimeout(function(){
    console.log(i);
}, 1 * 1000)
setTimeout(function(){
    console.log(i);
}, 2 * 1000)

setTimeout(function(){
    console.log(i);
}, 3 * 1000)

setTimeout(function(){
    console.log(i);
}, 4 * 1000)

...


setTimeout(function(){
    console.log(i);
}, 99 * 1000)

함수 안의 변수는 '실행'될 때 값이 결정된다. 위의 코드를 보면 0초이고 밑의 함수는 1초 뒤에 실행되는데, 1초 뒤에 실행되는 순간 그제서야 i를 찾는다. 처음부터 1로 고정되어 있는게 아니라, i를 찾는다. ++연산을 0.001초만에 하니까 100이다. 그래서 처음부터 100으로 출력이 된다. setTimeout비동기 함수가 0.1초 마다 생성되고, 변수 i 를 출력한다. 

 

바깥쪽 i는 올라가는데 안쪽은 그대로이다. 왜냐하면, 함수는 실행되기 전까지 그대로 가지고 있기 때문이다. 

 

자바스크립트는 단일 스레드언어이기 때문에 콜스택에 작업을 push하고 처리할때마다 하나씩 pop합니다. setTimeout과 같은 비동기작업은, 콜스택이 아닌 이벤트큐에 저장해놓는다. 그 후 이벤트 loop가 콜스택이 비어있는 순간에 이벤트 큐에 쌓여있던 작업을 push하여 콜백함수를 실행시켜줍니다. 위의 예에서는 콜백함수를 실행할때 i는 이미 100이 되어있기 때문에 0이아니라 100이 출력된다.

 

이문제를 해결하기 위해서는 아래와 같이 코드를 작성한다. 

 

for (var i = 0; i < 100; i++) {
    function call(j) {
        setTimeout(function () {
            console.log(j);
        }, j * 100);
    }
    call(i);
}

변수의 정적 유효범위를 활용하여 비동기함수에서 처리할 변수의 값을 함수로 감싸준다.

그러면 i 값이 100이 되어도 비동기 함수안에서 사용하는 j값은 고정되어 변하지 않습니다. 마치 함수로 변수를 고정(폐쇄)한다고 하여 이를 클로져(closure)라고 부릅니다.

 

 

위의 반복문 결과는 위의 결과와 다르게 아래와 같이 실행된다.

function call(j) {
    setTimeout(function () {
        console.log(j);
    }, j * 100);
}
call(0);
function call(j) {
    setTimeout(function () {
        console.log(j);
    }, j * 100);
}
call(1);
...
function call(j) {
    setTimeout(function () {
        console.log(j);
    }, j * 100);
}
call(99);

매개변수 또한 지역변수이기 때문에 지역변수가 call함수안에서 고정되어있어 변하지 않습니다.

 

많은 경우 반복문과 비동기 함수가 만날 때 클로져 이슈가 자주 발생하기 때문에 신경써서 해결을 해야한다. 

 

참고자료

  •  인프런 - 제로초 자바스크립트 강의

댓글