jineecode

스코프 & 클로저 본문

JS

스코프 & 클로저

지니코딩 2021. 4. 26. 15:14

코드를 보며 이해 먼저 하기

들어가기 전에...

 

자바스크립트 ES6부터는 const let을 이용해 블록 레벨 스코프도 지원하기 시작했습니다.

따라서 if 문 안에 var 대신 const나 let으로 변수를 선언하면, 다른 언어들처럼 참조하지 못합니다.

const와 let은 블록 레벨 스코프,

var와 같은 전통적인 자바스크립트의 변수는 함수 레벨 스코프라는 사실을 꼭 기억해두세요.


 

ES5

 

function outer() {
    var a = 1;
    console.log(a);
}

outer();

// 1

 

outer

a 1

 

a에 무슨 값이 들어있는지 찾는 '곳'이 스코프.

함수단위로 스코프가 생성된다.

 

function outer() {
    var a = 1;
    function inner() {
        var a = 2;
        console.log(a);
    }
    inner();
}

outer();

// 2

 

outer

a 1
inner()  

 

inner

a 2

 

 

console.log(a)에서 a가 어떤 a 인지 찾을 때 들여다보는 곳이 '스코프'이다.

inner에서 a를 찾는다.

 

function outer() {
    var a = 1;
    var b = 'B';

    function inner() {
        var a = 2;
        console.log(b);
    }
    inner();
}

outer();

// B

 

outer

a 1
inner()  
b 'B'

 

 

console.log(b) 를 찍었을 때 먼저 inner() 에서 찾아본다.

inner() 에 b 가 없다.

 

그 다음을 찾을 때 outer()에서 찾는다. (inner()가 outer() 안에 생성됐기 때문이다)

 

var d = 'X';

function outer() {
    var a = 1;
    var b = 'B';

    function inner() {
        var a = 2;
        console.log(b);
    }
    inner();
}

outer();

// X

 

global

d X
outer()  

 

글로벌 스코프. var d = 'X'

 

 

스코프끼리 연결되어있다.

이것이 바로 스코프 체인이라고 한다.

 

var d = 'X';

function outer() {
    var a = 1;
    var b = 'B';

    function inner() {
        var a = 2;
        console.log(b);
    }
    return inner;
}

var someFun = outer();
someFun();

// B

 

생성한 시점에 스코프 체인을 계속 들고 있는다. 들고 있기 때문에 일종의 클로저이다.

클로저 때문에 outer()가 실행된 다음에도 inner가 살아있다.

 

youtu.be/MbYShFxp-j0


 

function x() {
    {
        var t = 1;
    }
    console.log(t);
}

x();

// 1

 

var는 중괄호와 아무 상관 없는 함수 단위 스코프를 가진다.

 

function x() {
    {
        let t = 1;
    }
    console.log(t);
}

x();

//Uncaught ReferenceError: t is not defined

 

let은 블록 단위 스코프. {} 때문에 접근하지 못한다.

 


 

유효 범위(Scope)란?

scope의 사전적인 의미는 범위입니다.

자바스크립트에서 스코프란 작성된 코드를 둘러싼 환경으로, 어떤 변수들에 접근할 수 있는지를 정의합니다.

스코프 전역(global) 지역(local) 스코프로 정의할 수 있습니다.

 

전역 스코프는 함수 안에 포함되지 않은 곳에 정의하는 것으로 코드 어디에서든지 참조할 수 있고, 

지역 스코프는 함수 내에 정의된 것으로 정의된 함수 내에서만 참조할 수 있습니다.

 

자바스크립트의 스코프는 다른 언어와 다른 특징을 가지고 있는데, 바로 자바스크립트는 Function-level scope(함수 레벨 스코프)를 사용한다는 것입니다.

 

대부분의 언어는 Block-level scope(블록 레벨 스코프)를 사용함으로써, 변수 선언이 코드 블록 단위로 유효합니다. 하지만 Function-level scope인 자바스크립트는 함수 블록 내에서 선언된 변수는 함수 블록 내에서만 유효하고 함수 외부에서는 참조할 수 없습니다. 

 

단, 자바스크립트 ES6부터는 constlet을 이용해 블록 레벨 스코프도 지원하기 시작했습니다.

따라서 if 문 안에 var 대신 const나 let으로 변수를 선언하면, 다른 언어들처럼 참조하지 못합니다.

const와 let은 블록 레벨 스코프,

var와 같은 전통적인 자바스크립트의 변수는 함수 레벨 스코프라는 사실을 꼭 기억해두세요.

 

전역 스코프(Global scope)와 지역 스코프(Local scope)

지역 스코프는 함수 내의 범위로, 각각의 함수마다 자신의 지역 스코프를 가지고 있습니다.

 

어떤 함수 내의 지역 스코프에 선언된 변수가 있다면, 그 함수를 벗어난 범위에서는 그 변수를 참조할 수 없습니다.

당연하게도, 각각 선언된 함수가 있다면 서로의 스코프에 있는 변수는 참조할 수 없습니다.

 

아마 함수를 벗어나면 쓰지 못하는 지역 스코프보다, 전역 스코프가 더 편하다고 생각하실 수도 있습니다. 하지만 되도록이면 전역 스코프에 변수 선언을 하지 않는 것이 좋습니다.

 

왜일까요? 그 이유는 변수의 이름이 충돌할 가능성이 있기 때문입니다. 

자바스크립트로 개발하면서 모든 것을 혼자 개발하지 않는 이상 다른 사람의 코드를 사용하거나, 팀원들과 함께 협업을 하는 일이 생깁니다. 그런데 전역 스코프에 변수를 선언하게 되면, 흔한 이름일 경우 겹치는 일이 생길 수 있겠지요? 이름이 겹치게 되면 이전에 있던 변수를 덮어 쓰게 될 수도 있습니다.

따라서 피치 못할 경우가 아닌 이상, 전역 스코프 대신 지역 스코프를 이용하는 것을 권장합니다. 

 

유효 범위 체인(Scope Chain)

자바스크립트는 위에서 말했듯이 함수 단위의 범위를 가지고 있습니다. 그래서 서로 다른 함수끼리는 참조할 수 없다고 했었는데요, 함수 안에 함수가 들어있는 경우에는 어떻게 될까요?

 

만약 아래 코드와 같이 함수를 정의하면, 제일 안쪽의 함수인 inner는 그 위쪽의 범위까지 흡수하게 됩니다. 이러한 메커니즘을 유효 범위 체인(Scope Chain) 이라고 일컫습니다. 흔히 스코프 체인이라고 말하죠.

 

다시 말해, 한 변수가 특정 함수 내부에서 정의되면 그 함수 밖에서는 존재하지 않는 것처럼 보이는 것입니다. 외부에서는 안에 있는 함수의 변수를 참조할 수 없지만, 안에 있는 함수에서는 외부의 변수를 사용할 수 있습니다.

 

var a = 1;

function outer() {
	var b = 2;
	console.log(a); // 1
	
	function inner() {
		var c = 3;
		console.log(b);
		console.log(a); 
	}
	
	inner();  // 2 1
}
outer();

console.log(c);  // c is not defined

 

정적 범위(Lexical scope)

렉시컬 스코프는 기본적인 자바스크립트의 특징입니다.

 

렉시컬 스코프를 번역할 때는 흔히 "정적 범위", "정적 스코프"라고 번역합니다.

 

렉시컬 스코프란, 함수를 어디서 호출하는지가 아니라 어떤 스코프에 선언하였는지에 따라 결정된다는 것입니다.

이렇게 말하면 무슨 뜻인지 잘 이해가 되지 않죠? 예시 코드를 보면서 얘기해보도록 하겠습니다. 실행버튼을 눌러서 답을 확인하기 전에, 어떤 것이 출력될 것인지 아래 설명을 읽기 전에 생각해보세요.

 

var text = 'global';

function foo() {
	console.log(text);
}

function bar() {
	var text = 'bar';
	foo();
}

bar(); // 무엇이 출력될까요?

text 변수가 "global"에서 "bar"로 바뀌었으니, 당연히 "bar"가 출력될 것 같지 않으신가요?

 

하지만 실행하면 "global"이 출력됩니다. foo에서 출력한 text는 bar 함수의 지역 변수 text가 아니라 전역 변수 text를 가르키고 있기 때문입니다. 위에 설명했던 스코프 체인과도 연관이 있는 개념인데요, 먼저 foo 함수에서 text를 참조할 때 자기 자신의 스코프에서 text를 먼저 찾아보고, 없기 때문에 상위 스코프인 전역 스코프에서 text를 찾아서 출력하게 됩니다. 여기까지는 이해가 금방 되실 겁니다.

 

그런데 문제는, bar 함수에서 foo 함수를 불러온다는 겁니다. 그렇다면 foo 함수는 text를 참조해야 하니까, 상위 스코프인 bar 함수에서 text를 찾을 거라고 생각할 수 있지만, 아까 위에서 얘기했듯이 "어디서 호출하는지가 아니라 처음 선언되었을 때에 어떤 스코프에 있는지"가 중요합니다. 즉, 스코프란 코드를 실행하면서 바뀌는 것이 아니라 처음 작성한 그 스코프로 결정된다는 것입니다.

 

따라서 foo는 bar에서 호출되든 어떤 함수 안에서 호출되든지 상관없이, 무조건 자기 자신의 스코프를 찾아보고 그 이후에는 전역 스코프를 찾는다는 것입니다. 이제 조금 이해가 되시나요? 이렇게 foo가 한번 선언된 이상 전역변수 text를 참조하는 것을 바꿀 수 없습니다. 만약 text를 bar로 바꾼 후에 출력하고 싶다면, 지역변수 var를 선언할 것이 아니라 전역변수 var의 값을 바꾸면 됩니다. 이렇게요.

 

var text = 'global';

function foo() {
    console.log(text);
}

function bar() {
    text = 'bar';
    foo();
}

bar(); 

 

호이스팅(hoisting)

hoisting의 사전적 의미는 끌어 올리기라는 뜻입니다. 호이스팅도 자바스크립트의 특징 중 하나인데, 말 그대로 함수 안에서 변수를 선언할 때 어떤 위치에 있든 함수의 시작 위치로 끌어올리는 현상입니다. 단, 선언 부분만 위로 끌어올리고 값을 대입하는 부분은 위치 그대로 남아있습니다. 예시 코드를 통해 좀 더 자세히 알아보겠습니다. 

 

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

foo();

위 코드는 사실 아래와 같은 코드입니다.

 

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

foo();

 함수 호이스팅도 어떻게 작동하는지 살펴보도록 하겠습니다. 

foo();

function foo() {
	console.log('출력');
}

//출력

 

위와 같은 코드의 경우, 변수 호이스팅과 마찬가지로 함수선언이 위로 끌어올려지기 때문에 제대로 동작합니다. 하지만 아래와 같은 함수 표현식의 경우에는 오류가 발생합니다.

 

foo();  // foo is not a function

var foo = function() {
	console.log('출력');
};

 위의 코드는 아래와 같기 때문입니다.

var foo;

foo();  // foo is not a function

foo = function() {
	console.log('출력');
};

 

위와 같이 foo 선언을 위로 호이스팅 해버리기 때문에, foo가 실행될 때는 아직 변수로 선언이 된 상태일 뿐인 것입니다. 따라서 foo는 함수가 아니라는 에러 메세지를 보게 됩니다.

 

이 호이스팅은 혼란스러울 수 있기 때문에, 함수를 호출하기 전에 최상단에 선언하는 습관을 들여야 합니다.

 

 

클로저

일반적으로 외부 함수의 실행이 끝나면 외부 함수가 소멸되어 내부 함수가 외부 함수의 변수에 접근할 수 없습니다.

하지만 "외부 함수의 실행이 끝나고 외부 함수가 소멸된 이후에도 내부 함수가 외부 함수의 변수에 접근할 수 있는 구조" 클로저라고 합니다.

 

자신의 고유 스코프를 가진 상태로 소멸하지 않고 외부 함수에 의해 호출되는 함수를 만드는 것이 바로 클로저입니다.

 

원래 자바스크립트에서는 함수가 호출되면 메모리에 할당되고 함수가 종료되면 메모리에서 해제되기 때문에 함수별로 선언된 지역변수들은 호출할 때마다 같은 값으로 초기화됩니다. 하지만 함수를 호출할 때 이전에 쓰던 값을 유지하고 싶다면 클로저를 사용합니다.

 

var num = 1;

function foo() {
	var num = 2;
	
	function bar() {
		console.log(num);
	}
	return bar;
}

var baz = foo();
baz();

// 2

 

"foo() 함수는 리턴되어 사라진 후에 내부 함수 bar가 생성되는 것인데, 여전히 내부함수인 bar가 외부함수인 foo의 지역변수에 접근할 수 있을까?"

 

물론 위의 코드를 보시면 아시겠지만, 가능합니다. 이렇게 외부함수가 리턴되어 사라져야 하는데 사라지지 않고 내부함수의 참조로 인해 값을 유지하게 되는 것을 클로저라고 부릅니다. 위의 코드는 간단한 편이지만, 이것도 클로저라고 할 수 있습니다. 정확히는 내부 함수를 클로저 함수라고 부릅니다. 위 코드와 같은 경우에는 bar()가 클로저 함수가 되겠지요.

 

function f(arg) {
	var n = function() {
		return arg; 
	}
	arg++;
	return n; 
}

var m = f(123); 
console.log(m());

 

언뜻 보기에는 10번 라인의 f(123)을 실행하게 되면 함수 n에서 이미 arg를 반환하였기 때문에 n이 가지는 범위에서 arg 값은 123이라고 생각할 수 있습니다.

 

하지만 함수 n은 함수 f의 범위에 있는 것을 참조하고 있기 때문에 함수 f에서 모든 처리가 끝나고 나서야 함수 n이 처리됩니다.

 

따라서 f(123)는 124가 되게 됩니다. 

 

function f() {
    var a = [];
    var i;
	
    for(i = 0; i < 3; i++){
      a[i] = function() {
        return i;
        }
    }
    return a;
  }
  
  var b = f();

  console.log( b[0]() ); 
  console.log( b[1]() ); 
  console.log( b[2]() ); 

//3

//3

//3

 

위의 코드를 실행하면 당연히 0 1 2 가 차례대로 출력될 것 같습니다. 그런데 실제로 실행해보면 3 3 3 이 출력됩니다. a[i] = function() {return i; }

는 함수 선언만 된 것이고,

 

실제로 이 함수가 실행되는 것은 console.log( b[0]() ); 줄에서인데, 

var b = f(); 문장에서 for 문의 실행이 다 끝나고 나서야 실제 참조가 이루어지게 됩니다.

따라서 i 값이 이미 3으로 증가했기 때문에 전부 3이 출력되는 것입니다. 

 

즉, 클로저는 그 순간의 값을 저장하는 것이 아니라 연결된 함수 범위에서 최종 처리된 값을 가지게 됩니다.

그럼 클로저를 이용해서 정상적으로 0 1 2 가 출력되게 하려면 어떻게 해야 하는지 아래 코드를 통해 살펴보겠습니다.

 

function f() {
    var a = [];
    var i;
	
    for(i = 0; i < 3; i++){
      a[i] = (function(x) { 
        return function() {
          return x;
        }
      })(i);
    }
    return a;
  }
  
  var b = f();

  console.log( b[0]() );
  console.log( b[1]() );
  console.log( b[2]() );

function 내부의 변수인 i를 바로 리턴하지 않고, 파라미터를 받는 function을 정의한 다음에 파라미터로 내부 변수 i를 넘겨서 클로저가 내부 변수 i가 아니라 파라미터를 리턴하도록 하는 방법입니다.

 

function f() 내부에서 a[i] = (function(x) { ... })(i); 로 파라미터를 받는 함수를 이미 실행시켰다는 것에 주의해야 합니다.

 

파라미터는 0, 1, 2를 차례로 받게 되고, 나중에 console.log( b[0]() )를 실행하더라도 파라미터를 기억하고 있다가 0, 1, 2를 차례로 리턴하게 됩니다.

 

보통 함수 내에서 사용된 지역 변수는 해당 함수의 실행이 종료되면 파기되는 것인데, 이와 같이 클로저 함수에 의해 계속 참조되고 있는 경우에는 해당 지역 변수를 파기하지 않고 계속 보관하고 있는 것입니다. 

 

클로저를 사용하면 함수를 호출할 때마다 기존에 생성했던 값을 유지할 수 있기 때문에, 전역 변수의 잘못된 사용 없이 깔끔한 코드 작성을 할 수 있습니다. 또한, 외부에 해당 변수를 노출시키지 않아서 안정성을 보장해줍니다.

 

클로저를 통해서만 해당 변수를 참조할 수 있기 때문에 외부 사용자가 값을 변경할 수 없습니다.

 

하지만 클로저로 참조하는 변수는 프로그램 종료 시까지 계속 메모리에 할당되어 있기 때문에, 메모리 누수로 인해 성능 저하의 원인이 될 수도 있으니 신중하게 사용해야 합니다.

 

 

출처:

 

edu.goorm.io/learn/lecture/557/%ED%95%9C-%EB%88%88%EC%97%90-%EB%81%9D%EB%82%B4%EB%8A%94-node-js/lesson/21731/%ED%81%B4%EB%A1%9C%EC%A0%80

 

구름EDU - 모두를 위한 맞춤형 IT교육

구름EDU는 모두를 위한 맞춤형 IT교육 플랫폼입니다. 개인/학교/기업 및 기관 별 최적화된 IT교육 솔루션을 경험해보세요. 기초부터 실무 프로그래밍 교육, 전국 초중고/대학교 온라인 강의, 기업/

edu.goorm.io

 

'JS' 카테고리의 다른 글

promise  (0) 2021.04.27
싱글스레드 자바스크립트.  (0) 2021.04.26
지역 선택 JS로 제어하기  (2) 2021.02.26
이벤트 위임  (0) 2021.02.25
깨알 기초 지식 모음  (0) 2021.02.14
Comments