ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 호이스팅(Hoisting)과 클로저(Closure)
    frontend/javascript&web 2022. 2. 8. 23:18

    Concept By 호이스팅과 클로저(Concept By Hoisting & Closure)

     

    호이스팅(Hoisting)

    • 변수 및 함수 선언 부가 유효 범위 최상단으로 올라가는 현상

     

    앞서 우리는 실행 컨텍스트와 콜 스택 관련 글에서 함수와 변수 등이 메모리에 적재되는 과정을 볼 수 있었습니다. 이러한 적재과정에서 벌어지는 유효범위 최상단으로 선언부가 끌어올려지는 특성을 호이스팅이라고 부릅니다. 예시와 함께 알아 보겠습니다.

     

    예시

    console.log(writer);
    var writer = "류호진";

    이 코드를 바탕으로 우리는 이제 실제 동작 과정을 한번 보겠습니다.

    var writer = "류호진"; // (1)
    전역 컨텍스트의 메모리
    writer : 류호진 (1)

    스크립트가 실행되면 전역 실행 컨텍스트가 생기고 위와 같이 변수가 메모리에 올라 갈 것입니다. 이 메모리가 올라가는 과정을 3단계로 세분화 해보겠습니다.

    var writer; // (1)
    writer = undefined;
    console.log(writer);
    writer = "류호진"
    전역 컨텍스트의 메모리
     

    첫번째 단계는 변수를 실행컨텍스트의 변수객체에 등록하는 것입니다.

    var writer; // (1)
    writer = undefined; // (2)
    console.log(writer);
    writer = "류호진"
    전역 컨텍스트의 메모리
    writer : undefined (2)

    두번째 단계는 변수객체에 대한 메모리를 할당하고 변수를 undefined로 초기화 하는 과정입니다.

     

    자바스크립트는 함수 유효범위를 가진다고 했는데 그 말은 즉, 함수 내부에선 어디서든 변수에 접근이 가능하다는 것입니다. 예시코드는 내부에선 위와 같이 선언부가 유효범위 최상단으로 올라가기 때문에 접근이 가능한 것입니다. 이것이 호이스팅 입니다.

     

    실제 코드는 console.log보다 var writer = "류호진"이 아래에 있지만, 호이스팅이 일어났기 때문에 콘솔을 찍으면 출력 값으로는 undefinded가 출력이 될 것입니다.

    const writer;
    console.log(writer);
    writer = "류호진";

    여기서 잠깐 집고 넘어가겠습니다. var은 1단계와 2단계가 동시에 일어나고 const와 let은 1단계만 진행되면서 호이스팅이 진행됩니다. 그래서 만약 var가 아닌 let이나 const 였다면 ReferenceError(참조 에러)를 발생시켰을 것입니다.

     

    var writer; // (1)
    writer = undefined; // (2)
    console.log(writer);
    writer = "류호진" // (3)
    전역 컨텍스트의 메모리
    writer : 류호진 (3)

    세번째 단계undefined로 초기화 된 변수에 값을 할당하는 과정입니다. 1장과 2장에서 숱하게 보던 메모리에 등록되는 과정은 위와 같은 과정을 거치게 됩니다.

     

    함수에서도 호이스팅이 발생하게 됩니다. 함수에서의 호이스팅은 함수를 작성하는 두가지 방법에 따라 상이하게 동작합니다.

     

    함수 선언식

    function guide(){
    	console.log('함수 선언식');
    }

    함수 표현식

    var guide = function() {
    	console.log('함수 표현식');
    }

    함수선언식은 호이스팅이 발생하게 되고 함수의 식까지 함께 호이스팅 됩니다. 하지만 함수 표현식은 다릅니다.

    function() {
    	console.log('함수 표현식');
    }

    위의 부분이 함수 표현식의 함수 부분이라고 할 수 있습니다. 함수가 할당되는 변수 var guide는 호이스팅이 되지만, 표현식으로 정의된 함수는 변수에 할당되기 전 까지 참조할 수가 없음으로 호이스팅 되지 않습니다. 

    var guide = function fact(){
    	fact()
    }

    또한, 함수표현식에는 이름을 보통 생략하는데 위와 같이 이름을 지정하여 재귀호출 할 때 사용할 수 있습니다. 미세꿀팁입니다.

     

    정리하자면, 변수 및 함수 선언부가 유효범위 최상단으로 올라가는 현상을 말하며, 함수의 선언 방식과 변수 선언부의 scope 범위에 따라 다르게 동작하빈다.

     

     

    클로저(Closure)

    • 함수가 함수를 반환할 때 반환되는 함수가 자신을 둘러싼 메모리 환경을 가지고 반환하는 것

     

    실제로 코드의 동작과정을 보며 클로저에 대해 좀 더 명확하게 알아보도록 하겠습니다.

     

    예시

    function outer(){ // (1)
    	var n = 0;
    	function increase(){
    		return n += 1;
        }
    	return increase;
    }
    
    var closureFx = outer(); // (2)
    closureFx();
    closureFx();
    전역 실행 컨텍스트 전역 메모리
      outer:  fn  (1)
    closureFx : (2)

    먼저, outer 함수를 전역메모리에 등록하고 outer()의 리턴을 값으로 가지는 closureFx변수를 만듭니다.

    function outer(){
    	var n = 0; // (2)
    	function increase(){ // (3)
    		return n += 1;
        }
    	return increase;
    }
    
    var closureFx = outer(); // (1)
    closureFx();
    closureFx();
    전역 실행 컨텍스트 전역 메모리
    outer() (1) outer:  fn 
    closureFx :
    함수 실행 컨텍스트 지역 메모리
      n : 0 (2)
    increase :  fn1  (3)

    그 후, outer()를 호출하고 outer()의 실행컨텍스트가 만들어지고 콜스택에 outer()가 push 됩니다. 매개변수는 없으니 바로 코드가 실행될 것입니다. n이 0으로 할당되고 increase라는 함수 이름에 함수 식이 할당됩니다.

    function outer(){
    	var n = 0;
    	function increase(){
    		return n += 1;
        }
    	return increase; // (1)
    }
    
    var closureFx = outer();
    closureFx();
    closureFx();
    전역 실행 컨텍스트 전역 메모리
    outer() outer:  fn 
    closureFx :  fn1  (1)
    함수 실행 컨텍스트 지역 메모리
      n : 0 
    increase :  fn1  (1)

    그리고 increase라는 함수 본체 자체를 반환하면 closureFx에 fn1이 반환됩니다.

    function outer(){
    	var n = 0;
    	function increase(){
    		return n += 1;
        }
    	return increase;
    }
    
    var closureFx = outer(); // (1)
    closureFx();
    closureFx();
    전역 실행 컨텍스트 전역 메모리
      outer:  fn 
    closureFx :  fn1  

    그 다음 outer()함수가 종료되면서 실행컨텍스트가 사라지며 outer 콜스택이 pop() 됩니다.

    function outer(){
    	var n = 0;
    	function increase(){
    		return n += 1; // (2) n이 어딧지?
        }
    	return increase;
    }
    
    var closureFx = outer();
    closureFx(); // (1)
    closureFx();
    전역 실행 컨텍스트 전역 메모리
    closureFx() (1) outer:  fn 
    closureFx :  fn1 
    함수 실행 컨텍스트 지역 메모리
    n += 1 (2) ? (2)

    그 후, ClosureFx() 함수를 실행시킵니다. closureFx에는 함수 바디에서 n = n + 1 연산을 하고 있습니다. 하지만 함수 실행컨텍스트의 지역메모리에는 n이 없습니다. 실행컨텍스트의 마지막 쯤에 배운 스코프 체이닝에 따라 상위 컨텍스트를 찾겠지만 n은 outer가 종료되면서 실행컨텍스트가 사라질 때 사라졌다고 생각하실 겁니다.

    function outer(){
    /**
    	var n = 0;
    	function increase(){
    		return n += 1; // (2)
        }
    	return increase;
    */
    }
    
    var closureFx = outer();
    closureFx(); // (1)
    closureFx();
    전역 실행 컨텍스트 전역 메모리
    closureFx() outer:  fn 
    closureFx :  fn1 - ( n : 0) 
    함수 실행 컨텍스트 지역 메모리
    n += 1  

    사실 함수가 함수를 반환할 때 반환할 함수를 둘러싸고 있는 메모리 환경(주석 부분)을 가지고 나옵니다. 이것으 우리는 클로저라고 합니다. 자바스크립트 엔진은 지역메모리에서 원하는 값을 찾지 못하면 상위 컨텍스트로 스코프체이닝을 이어나가는데 그 전에 클로저라는 것을 먼저 확인합니다.

    function outer(){
    	var n = 0;
    	function increase(){
    		return n += 1; // (1) 클로저의 N : 0 -> 1
        }
    	return increase;
    }
    
    var closureFx = outer();
    closureFx(); // (1)
    closureFx();
    전역 실행 컨텍스트 전역 메모리
    closureFx() outer:  fn 
    closureFx :  fn1 - ( n : 0)  (1)
    함수 실행 컨텍스트 지역 메모리
    n += 1 (1)  

    이렇게 클로저에 있는 n을 +=1 연산을 통해 증가시켜 줍니다.

    function outer(){
    	var n = 0;
    	function increase(){
    		return n += 1;
        }
    	return increase;
    }
    
    var closureFx = outer();
    closureFx(); // (1)
    closureFx();
    전역 실행 컨텍스트 전역 메모리
      outer:  fn 
    closureFx :  fn1 - ( N : 1)  (1)

    그 후 함수가 종료되면 실행컨텍스트가 날라가고 콜 스택이 pop() 됩니다. 이 다음 작업도 위의 동작을 반복하여 n이 2가 됩니다.

     

    정리하자면, 클로저는 함수가 함수를 반환할 때 함수는 자신을 둘러싼 메모리 환경을 가지고 반환하는 것을 말합니다. 클로저는 함수의 호출이 아닌 정의된 위치에 따라 결정되는 렉시컬 스코프를 가지고 있습니다. 또한, 자바스크립트 인터프리터가 상위 실행 컨텍스트로 스코프체이닝을 이어나가기 전 클로저에 변수가 있는지 먼저 확인합니다. 그리고 클로저 변수는 함수가 호출되어야만 접근이 가능하기 때문에 정보은닉에도 활용됩니다.

     

    이렇게 우리는 호이스팅과 클로저에 대한 전체적인 컨셉에 대해서 완벽하게 이해하셨을 거라고 생각합니다. 이제 다음은 좀 더 세부적인 내용과 부가적인 설명에 대해 해보겠습니다.

     

    DeepDive By 호이스팅과 클로저(DeepDive By Hoisting & Closure)

     

    호이스팅(Hoisting)

    • 대부분의 프로그래밍언어에서 블록 안에 있는 코드는 자신만의 유효범위를 가지며, 변수는 해당 변수가 선언되지 않는 블록 밖에서는 보이지 않습니다. 이를 블록 유효범위라고 부르는데, 자바스크립트는 기본적으로 함수 유효범위를 사용하고 있습니다. 변수는 해당 변수가 정의된 함수 뿐만 아니라 중첩된 함수 안에서도 보입니다.(스코프체이닝) 결론적으로, 자바스크립트는 함수 안에서 선언된 모든 변수는 함수 전체에 걸쳐서 유효합니다. 이는 코드 순서상 선언되기 전에도 유효 하다는 뜻인데 이런 자바스크립트 특징을 우리는 호이스팅 이라고 합니다.

     

    사실 이제 호이스팅에 대하여 어느정도 이해하셨을 것이라고 생각합니다. DeepDive에서는 예시와 함꼐 동작을 살펴 보고 조금 더 이해를 돕는 시간을 가지도록 하겠습니다.

     

    예시

    var company = "IT회사";
    
    function company() {
    	console.log("IS NOT WORKING COMPANY");
    }
    
    console.log(typeof company);

    위의 코드는 샘플 코드 입니다. 위의 코드는 어떨까요? 자칫하면 company는 function 이라고 오해하기 쉽습니다. 하지만 내부적으로는 어떨가요? 순서대로 보겠습니다.

    var company; // (1)
    
    function company(){ // (1)
    	console.log("IS NOT WORKING COMPANY");
    }
    company = "IT회사";
    
    console.log(typeof company);
    전역 컨텍스트의 메모리
    company :  fn  (1) 

    우선 호이스팅이 발생하여 변수의 선언부와 함수의 선언식이 최상단으로 올라가게 됩니다. 함수 선언식이 기존의 변수객체 company를 덮어 메모리에 함수 company가 등록이 됩니다.

    var company; 
    
    function company(){
    	console.log("IS NOT WORKING COMPANY");
    }
    company = "IT회사"; // (1)
    
    console.log(typeof company); // (1)
    전역 컨텍스트의 메모리
    company : IT회사 (1) 

    이후, 메모리에 등록된 company 변수에 문자열 개발회사를 할당하여 갚을 덮어씁니다. 그럼 결과 값으로는 String이 출력되게 됩니다.

     

    이처럼 호이스팅으로 인해 값이 예상한 결과랑 다르게 나오는 경우도 있으니 이를 참고하여 스크립트를 작성한다면 더 나은 개발을 할 수 있을 것이라고 생각합니다.

     

     

    클로저(Closure)

    • 클로저는 왜 사용하는가? 에 대한 부분에서 우리는 많은 의문을 가지고 있을 수 밖에 없습니다. 그거 없어도 개발 잘하고 있는데? 라는 분들도 계실 것이라고 생각합니다. 이유를 찾아보자면, 전역 변수의 남용을 막고 캡슐화와 은닉화를 자바스크립트로 구현하기 위해 존재한다고 볼 수 있습니다.

     

    100번말하는 것보다 한번 눈으로 따라 가는게 훨씬 많은 이해를 가질 것이라고 생각됩니다. 물론 100번 읽는 것보다 한번 직접해보시는 것이 좋습니다.

     

    예시

    function counter(value){
    	var _value = 0;
    	function count(value) {
    		_value = value || _value;
    		return _value;
    	}
    	return {
    		value: value,
    		plus:function(){
    			return count(count() + 1);
    		},
    		minus:function(){
    			return count(count() - 1);
    		}
    	}
    }
    
    var count = counter();
    count.plus();
    count.minus();

    위의 예시 코드는 NHN 기술 블로그에 올라 와있는 클로저에 대한 코드입니다. 실제로 이 코드 내부가 어떻게 생겻나 한번 보겠습니다. 위에서 동작하는 과정은 눈으로 보았으니 실제 내부를 보겠습니다.

     

    우의 코드에서 count.plus()나 count.minus()는 실제로 counter내부의 count함수를 반환하고 있습니다. 우리는 어떻게 배웠나요? 함수가 함수를 반환할 때 반환되는 함수 주변의 환경을 들고 올라간다고 했습니다. 자 한번 보겠습니다.

     

    실행 코드 - 클로저

    함수 내부를 들여다보니 스코프가 보입니다. 해당 스코프를 열어보면 우리가 계속 부르고 불렀던 클로저가 있는 것이 보입니다. 스코프 내부에는 위의 소스코드에서 var _value = 0에 해당하는 부분도 볼 수 있습니다. 정말 주변의 환경을 함께 반환하고 있는것을 실제로 보았습니다.

     

    또한, 스코프 체이닝에서 전역을 탐색하기 전에 클로저를 먼저 탐색하여 값을 찾는 다는 것을 위의 화면에서 실제로 볼 수 있습니다.

     

    우리는 이처럼 코어 자바스크립트를 배워 나가게 된다면 뭔 갈 찍어보긴 했는데 이게 뭔지는 알겠는데 내부에서는 어떻게 돌아가는지 어떤 사이드이펙트가 발생할 수 있는지 소스를 일일이 돌려보지 않아도 알 수 있을 것입니다. 이렇게 우리가 내부로직을 잘 알게 되면 무슨 프레임워크를 배우든 그 내부동작 과정을 남들보다 100배는 더 잘 이해할 수 있을 것이라고 생각합니다. 그게 우리가 돌고 돌아 코어 자바스크립트를 다시 배우는 이유이기도 합니다.

     

    참고 : NHN BLOG

    반응형

    'frontend > javascript&web' 카테고리의 다른 글

    프로토타입(Prototype)  (0) 2022.02.16
    이벤트 루프(Event Loop)  (1) 2022.02.15
    스코프(Scope)  (0) 2022.02.09
    콜 스택(Call Stack)  (0) 2022.02.06
    실행 컨텍스트(Excution Context)  (0) 2022.02.04

    댓글

Designed by Tistory.