스코프와 클로저

Y·2021년 5월 28일
0
post-thumbnail
post-custom-banner

1. 자바스크립트의 스코프(scope)

🧐 스코프의 개념 및 필요성

스코프(scope, 유효범위)는 참조 대상 식별자 (identifier, 변수, 함수의 이름과 같이 어떤 대상을 다른 대상과 구분하여 식별할 수 있는 유일한 이름)을 찾아내기 위한 규칙이다. 자바스크립트는 이 규칙대로 식별자를 찾는다.

다시말해 스코프는 현재 접근할 수 있는 변수들의 범위를 의미한다. 어떠한 변수가 스코프안에 선언되었으면 해당 스코프 안에서는 변수에 접근하여 읽거나 쓸 수 있고, 스코프 밖에서는 해당 변수에 접근할 수 없다.

만약 스코프가 없었다면?
식별자 이름은 충돌을 일으키므로 프로그램 전체에서 하나밖에 사용할 수 없다. 즉 스코프는 식별자 이름의 충돌을 방지할 수 있다.

🧐 스코프의 구분

자바스크립트에서 스코프는 크게 2가지로 나눌 수 있다.
1. Global (전역) scope
코드 어디에서든지 참조 가능
2. Local (지역) scope
해당 지역에서만 접근할 수 있어 지역을 벗어난 곳에선 접근할 수 없음

🔶 Global scope (전역 스코프)

전역으로 변수를 선언하면 이 변수는 어디서든지 참조할 수 있는 전역 스코프를 갖는 전역 변수가 된다. var 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티이다.

var global = 'global';

function foo() {
  var local = 'local';
  console.log(global);
  console.log(local);
}
foo();

console.log(global);
console.log(local); // Uncaught ReferenceError: local is not defined

변수 global은 함수 영역 밖인 전역에서 선언되었다.
자바스크립트는 C언어의 main함수와 같이 특별한 시작점이 없어서 위 코드와 같이 전역에 변수나 함수를 선언하기 쉽다.

-> 따라서 코드가 나타나는 즉시 해석되고 실행되고 이는 전역에 변수를 선언하기 쉽게 만들어 전역변수를 남발하게 하는 문제를 야기시킨다.
전역변수는 의도치 않은 재할당에 의한 상태 변화로 코드를 예측하기 어렵게 만드므로 사용을 억제하는 것이 좋다.

🔶 Local scope (=지역 스코프)

🔹 Block-level scope (블록 레벨 스코프)

ES6(ECMAScipt 6)의 let, const 키워드는 블록 레벨 스코프 변수를 만들어준다.

function foo() {
    if(true) {
        let color = 'blue';
        console.log(color); // blue
    }
    console.log(color); // ReferenceError: color is not defined
}
foo();

예시와 같이 let color='blue'if 블록 내부에서 선언했기 때문에, if 블록 내부에서 참조할 수 있고 그 밖의 영역에서는 잘못된 참조로 에러가 발생하는 것을 알 수 있다.

자바스크립트는 전통적으로 함수레벨 스코프를 지원해왔고 얼마전까지만 해도 블록 레벨 스코프는 지원하지 않았지만, 가장 최신 명세인 ES6(ECMAScipt 6)부터 블록 레벨 스코프를 지원하기 시작했다.

🔹 Function-level scope (함수 레벨 스코프)

함수 레벨 스코프란 함수 코드 블록 내에서만 유효하고 함수 외부에서는 유효하지 않다는 의미이다.
대부분의 C-family language는 블록 레벨 스코프(block-level scope)를 따르는 반면, 자바스크립트는 함수 레벨 스코프(function-level scope)를 따른다.

즉 함수 내에서 선언된 매개변수와 변수는 함수 외부에서는 유효하지 않다는 것이다. var 키워드로 선언된 변수나, 함수 선언식으로 만들어진 함수는 함수 레벨 스코프를 갖는다.

function foo() {
    if (true) {
        var color = 'blue';
    }
    console.log(color); // blue
}
foo();

var color = 'blue'로 선언한 color는 함수 레벨의 스코프이기 때문에 foo함수 내부 어디에서든 에러 발생 없이 참조할 수 있다.

var x = 'global';

function foo() {
  var x = 'local';
  console.log(x);
}

foo();          // local
console.log(x); // global

위의 예시와 같이 변수명이 중복된 경우, 지역변수를 우선하여 참조한다.

참고) 자바스크립트는 가장 가까운 지역스코프 > 상위 스코프 > 글로벌 스코프 순으로 탐색

var x = 'global';

function foo() {
  var x = 'local';
  console.log(x); //local

  function bar() {  // 내부함수
    console.log(x); //local
  }

  bar();
}
foo();
console.log(x); //global

내부함수는 자신을 포함하는 외부함수의 변수에 접근할 수 있다.

🧐 렉시컬 스코프 (lexical scope)

  • 동적 스코프(dynamic scope) : 함수가 어디서 호출했는지에 따라 상위 스코프 결정 (함수가 어디서 호출했는지에 따라 상위 스코프를 결정)
  • 렉시컬 스코프(lexical scope) : 함수가 어디서 선언되었는지에 따라 상위 스코프 결정 (JS 를 포함한 대부분의 언어가 해당 스코프 규칙을 따름)
    아래의 예시로 이해해보자
var x = 1;

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

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

foo(); 
bar(); // 1

foo()의 결과값은 무엇일까
❕ 동적 스코프를 따른다면 실행결과는 10일테고, 렉시컬 스코프라면 결과는 1일 것이다.
실제 실행해보면 렉시컬 스코프를 따라서 1이 나오는 것을 알 수 있다. bar함수를 호출한 곳은 foo함수 내부이지만, bar함수가 선언된 곳은 전역이기때문에 x는 전역으로 선언된 1의 값을 가지게 된다.

다시 정리해보자

렉시컬 스코프는 함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정된다.
자바스크립트는 렉시컬 스코프를 따른다.
따라서 함수를 선언한 시점에 상위 스코프가 결정된다.

2. 클로저 (Closure)

클로저는 자바스크립트 고유의 개념이 아니라 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다

“A closure is the combination of a function and the lexical environment within which that function was declared.”
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.

위 정의에서의 “함수” 란 반환된 내부함수를 의미하고 “그 함수가 선언될 때의 렉시컬 환경(Lexical environment)” 이란 내부 함수가 선언됐을 때의 스코프를 의미한다.
즉, 클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다.
간단히 말하면 클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수다라고 말할 수 있겠다.

그럼 이 클로저가 자바스크립트에 어떻게 녹아 들어갔는지 살펴보자

🧐 자바스크립트의 클로저

자바스크립트에서 클로저는 함수가 선언되는 시점에 생성된다.
즉, 함수가 생성될 때 그 함수의 렉시컬 환경을 포섭(closure)하여 실행될 때 이용한다.

👀 반복문 클로저 예시

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

출력결과

❓ 여기서 우리의 목표는 0 1 2...9를 1초 간격으로 출력하는 것이였는데, 결과로는 똑같은 10이 10번 출력되었다. 왜일까?

❕ 우선var로 선언한 변수는 함수 스코프를 따르기 때문에 전역에 선언하는 것과 같다.
또한 이 코드에서 보면, 자바스크립트는 for문이 먼저 돌고 비동기 콜백함수가 실행될 것 이다. 반복문이 먼저 돌면서 setTimeout 함수가 10번 만들어지고, i는 10이 되어 있는 상황이다. 따라서 콜백함수가 콜스택에 올라왔을 당시에 참조하는 i값은 이미 10이 되어버렸기에 10을 10번 출력하게 되는 것이다.

❓ 의도한대로 0~10까지 1초 간격으로 출력하려면 어떻게 해야할까?

1) ES6에서 추가된 let으로 블록 스코프를 이용하는 방식

 for (let i = 0; i < 10; i++) {
     setTimeout(function timer() {
         console.log(i);
     }, i * 1000);
 }

let을 이용하면 블록 스코프로 선언하게 되고, let 선언문이 각 반복 루프를 돌 때 블록 범위에 대한 새로운 변수를 생성한다. 따라서 결론적으로 0 1 .. 9 을 출력한다.

2) 클로저를 이용하는 방식

for(var i = 0; i < 10; i++){
    function closure(val) {
      setTimeout(function() {
        console.log(val);
      }, i * 1000);
    }
  closure(i);
}

외부 함수가 종료되어도 내부 함수가 외부 함수의 변수들에 접근할 수 있다. for문에서의 iclosure함수가 매개변수로 받았고, for문이 종료되어도 closure 함수는 외부에서 전달받은 i를 계속 가지고 있게된다. 따라서 의도한대로 0 1 .. 9를 1초 간격으로 출력하게 된다.

정리) 클로저에서 외부함수보다 중첩함수가 더 오랜 생명주기를 유지하는 경우 외부함수의 변수를 참조할 수 있다.

결국에는 메모리에 해당 값을 기억하고 있기 때문에, 너무 많은 클로저 사용은 실제 메모리의 누수를 유발할 수 있다고 하니 이 점을 유의하자.


🤔 렉시컬 스코프와 클로저는 깊게 들어가면 내용이 더 많다. 오늘은 이 정도로 간단하게 정리해보고, 나중에 더 심도 있게 공부할 예정이다.


[References]

profile
기록중
post-custom-banner

0개의 댓글