Web/JavaScript

[JavaScript] 스코프(Scope)와 클로져(Closure)

Hov 2021. 5. 25. 14:28

스코프(Scope)

호이스팅을 설명하기 전에 스코프에 대한 설명을 먼저 했어야 했는데, 지금이라도 한번 정리 해 봐야겠다. 자바스크립트에서 스코프라는 단어를 많이 접해봤을 것이다. 스코프 해당 변수에 접근 할 수 있는 범위를 일컫는 말인데, 자바스크립트에서는 Global(전역)과 Local(지역) 두 종류의 스코프가 있다.

var name = 'hov';

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

print(); // 출력: lee
console.log(name); // 출력: hov

위에서 보면 알 수 있듯, name이라는 동일한 이름을 가진 변수가 각각 독립적으로 불리는 것을 알 수 있다. print() 함수 안에 있는 변수(name)는 함수 몸체 안에서만 접근을 할 수 있는데, 이를 함수 스코프(function-scoped)라고 하고, 이 함수 스코프가 지역 스코프의 대표적인 예다.

함수 레벨 스코프

블록레벨 스코프를 갖는 C++와 달리, 자바스크립트에서 var 키워드로 선언 된 변수나, 함수 선언식으로 만들어진 함수는 모두 함수 레벨 스코프를 갖는다. 때문에 아래 코드는 아무런 문제가 없이 hov를 출력한다.

function print() {
  if (true) {
    var name = 'hov';
  }
  console.log(name); //if문 안에서 name이 선언 되었더라도, 동일한 함수 내에서는 접근이 가능
}

print();  // 정상 출력: hov

블록 레벨 스코프

ES6 부터 let, const 키워드가 나오면서 블록 레벨 스코프 변수를 만들 수 있게 되었다. 때문에 위의 코드에서 var 대신 let 이나 const로 변수를 선언 한다면 if문을 빠져나가는 순간 해당 변수는 파괴 되고 블록 밖에서 참조 할 수 없게된다.

function print() {
  if (true) {
    let name = 'hov';
  }
  console.log(name); // Reference Error. name is not defined
}

print();  // 에러

일반적으로 함수 레벨의 스코프를 가지는 var보다 블록 레벨 스코프를 갖는 let, const가 더 안정적인 프로그래밍을 할 수 있게 도와주기 때문에 ES6 이상의 코드에서는 대부분 var를 사용하지 않는다.

렉시컬 스코프 (Lexical Scope)

렉시컬 스코프란 함수가 선언이 되는 위치에 따라서 상위 스코프가 결정되는 스코프다.
자바스크립트를 포함해 C, Java, Python 등 현대 프로그래밍의 대부분의 언어에서는 렉시컬 스코프 규칙을 따르고 있다.

이해가 잘 안된다면 아래 코드를 보자.

let x = 'global';

function foo(){
  let x ='local';
  bar();
}

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

foo(); //global
bar(); //global

foo 함수 내부에서 bar를 호출 했지만,bar가 선언된 곳은 전역이기 때문에 x가 'global'의 값을 갖는다.

클로져(Closure)?

var x = 100;

function outer() {
  var x = 0;
  return function inner() {
    return x;
  }
}

var foo = outer();
console.log(foo()); // 출력: 0

위의 코드를 보면, outer 함수 내부에서 inner 함수를 반환하는데, 렉시컬 스코프에 따라 inner함수에 있는 x는 outer에서 선언한 x를 가리킨다. 그렇게 outer에 있던 innerg함수가 foo라는 변수에 담긴다. outer 함수는 이미 종료되어 콜스택에서 빠져 나갔지만, foo()를 실행하면 여전이 outer에 있는 변수 x에 접근하고 있다는 것을 알 수 있다.

이처럼 어떤 함수를 렉시컬 스코프 밖에서 호출해도, 원래 선언 되었던 스코프를 기억하고 접근할 수 있도록 하는 특성을
클로져라고 한다. 조금 더 정리해서 말하자면 내부함수가 외부 함수의 맥락에 접근할 수 있는 것을 클로져(Closure)라고 한다. 그렇다면 이 클로져는 과연 언제 사용되고, 장단점은 무엇일까?

1. 은닉화

클로져의 가장 큰 특징은 외부함수가 소멸 된 이후에도 외부함수 변수에 접근이 가능하다는 점인데, 이를 이용해서 전역 변수를 최소화 하거나, 변수 접근을 제한하는 프라이빗 함수를 만들 수 있다.

const btn = document.querySelector('button')

//전역변수로 count를 쓰지 않고 handleClick만을 위해 쓰이는 count가 생겼다..!

btn.addEventListener('click',handleClick())

function handleCilck(){
  let count = 0
  return function (){
    count++
    return count
  }
}

2.  반복문 클로져

함수특성때문에 이전값을 기억해야하는경우, 예를 들어서 전자시계 00: 00:00 시계에서 시간을 기억했다가 시 분 초가 올라가는것
그리고 게임에서 카운트가 올라가는경우등에 자주 쓰인다고 한다.

다음은 클로져를 설명할 때 자주 등장하는 코드다.

var arr = []
for(var i = 0; i < 5; i++){
    arr[i] = function(){
        return i;
    }
}

for(var index in arr) {
    console.log(arr[index]());
}

//출력: 55555

함수가 함수 외부의 컨텍스트에 접근할 수 있을 것으로 기대하겠지만 위의 결과는 5만 출력을 한다.

var arr = []
for(var i = 0; i < 5; i++){
    arr[i] = function(id) {
        return function(){
            return id;
        }
    }(i);
}
for(var index in arr) {
    console.log(arr[index]());
}
//01234

주의할 점

두 개의 클로져가 상위 스코프를 공유하게 되는 상황에서 메모리 누수가 발생하니 주의해야 한다!

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 'originalThing'에 대한 참조
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000)

 

클로저는 자바스크립트를 이용한 고난이도의 테크닉을 구사하는데 필수적인 개념으로 활용된다고 한다. C를 처음 배울 때 포인터를 접했던 너낌과 비슷 한 듯 하다. 잘못쓰면 메모리 누수 같은 큰 결함을 유도 할 수도 있으니 개념에 대한 깊은 이해와 사용법을 연습해야겠다.

 

참고

 

자바스크립트의 스코프와 클로저 : NHN Cloud Meetup

자바스크립트의 스코프와 클로저

meetup.toast.com

 

An interesting kind of JavaScript memory leak

Recently, Avi and David tracked down a surprising JavaScript memory leak in Meteor’s live HTML template rendering system. The fix will be…

blog.meteor.com