ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 3.3 JavaScript 클로저
    코드스테이츠 2023. 3. 3. 01:56

    MDN의 클로저 정의

    "함수와 함수가 선언된 어휘적(lexical) 환경의 조합을 말한다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다."

    여기서 주목할 만한 키워드는 "함수가 선언"된 "어휘적(lexical) 환경"입니다. 특이하게도 자바스크립트는 함수가 호출되는 환경과 별개로 기존에 선언되어 있던 환경, 즉 어휘적 환경을 기준으로 변수를 조회하려고 합니다. 이와 같은 이유로 "외부 함수의 변수에 접근할 수 있는 내부 함수"를 클로저 함수라

     

    1️⃣ 클로저는 무엇인가요?

    MDN은 클로저를 아래와 같이 정의합니다.

    closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). - mdn (2023)

    클로저는 함수와 그 함수 주변의 상태의 주소 조합입니다.

    조금 더 이해하기 쉽게 풀어서 설명해보겠습니다.

    클로저는 함수와 그 함수가 접근할 수 있는 변수의 조합입니다.

    2️⃣ 클로저를 어떻게 구분할 수 있나요?

    그렇다면 아래 코드에서는 무엇이 클로저일까요?

    const globalVar = '전역 변수';
    
    function outerFn() {
      const outerFnVar = 'outer 함수 내의 변수';
      const innerFn = function() { 
        return 'innerFn은 ' + outerFnVar + '와 ' + globalVar + '에 접근할 수 있습니다.';
      }
    	return innerFn;
    }

    위 코드에 있는 함수부터 찬찬히 살펴보겠습니다.

    1. 함수 outerFn에서는 변수 globalVar에 접근할 수 있습니다.
    2. 함수 innerFn에서는 변수 globalVar와 함수 outerFn 내부의 outerFnVar에 접근할 수 있습니다.

    즉, 위 코드에서 클로저는 두 조합을 찾을 수 있었습니다.

    1. 함수 outerFn과 outerFn에서 접근할 수 있는 globalVar
    2. 함수 innerFn과 innerFn에서 접근할 수 있는 globalVar, outerFnVar

    3️⃣ 클로저는 왜 중요한가요?

    변수의 접근 범위인 스코프와 비슷한 개념인데, 왜 따로 클로저만 구분을 할까요? 클로저의 함수는 어디에서 호출되느냐와 무관하게 선언된 함수 주변 환경에 따라 접근할 수 있는 변수가 정해지기 때문입니다. 아래 코드를 보며 설명하겠습니다.

     

    const globalVar = '전역 변수';
    
    function outerFn() { // outerFn()시작점
      const outerFnVar = 'outer 함수 내의 변수';
      const innerFn = function () {
        return (
          'innerFn은 ' + outerFnVar + '와 ' + globalVar + '에 접근할 수 있습니다.'
        );
      };// 여기서 끝남
      return innerFn; //여기서 리턴함
    }
    
    const innerFnOnGlobal = outerFn();
    const message = innerFnOnGlobal();
    console.log(message); // innerFn은 outer 함수 내의 변수와 전역 변수에 접근할 수 있습니다.

    innerFnOnGlobalouterFn 내부의 innerFn의 주소값을 가집니다. 그다음 줄에서 innerFnOnGlobal을 호출합니다. 이때,innerFnOnGlobalinnerFn 밖에 있기 때문에 outerFnVar에는 접근하지 못한다고 생각할 수 있는데, 실제 접근할 수 있습니다.

     

    왜 접근할 수 있을까요? innerFn 함수가 최초 선언되었던 환경에서는 outerFnVar에 접근할 수 있기 때문입니다. innerFnOnGlobalinnerFn의 주소값을 가지고 있고, innerFn은 클로저로서 outerFnVar에 접근할 수 있기 때문입니다. 이 “환경”을 어휘적 환경(Lexical Environment)라고 합니다.

     

    예제코드를보면 innerFnOnGlobal에 할당한것은 outerFn이 아닙니다. 

    실제로 innerFnOnGlobal에 할당한것은 outerFn()으로 이것이 뜻하는 것은 outerFn의 호출 결과입니다.
    outerFn()은 return문을 통해 innerFn을 반환하고있습니다.
    따라서 실제로 innerFnOnGlobal에 할당된것은 함수 innerFn입니다.

     

    또한 추가적으로 innerFnOnGlobal이 선언된 렉시컬환경은 전역입니다.
    따라서 전역에 선언된 innerFnOnGlobal은 가장 외부 스코프에 위치하게됩니다.

    스코프를 공부할 때에 내부에서 외부로의 참조는 가능하지만 외부에서 내부로의 참조는 불가하다고 배웠습니다.
    따라서 같이 전역에서 선언되었다하더라도 outerFn의 내부 코드들은 외부로부터 닫혀있기때문에
    전역스코프에서는 같이 전역에서 선언된 outerFn을 할당받을 수는 있어도  outerFn의 내부 지역변수 outerFnVar에는 접근할 수 없습니다.

    그렇기 때문에 만약 예시코드가 

    const innerFnOnGlobal = outerFn

    위와 같은 형태였다하더라도 만약 outerFn이 클로저가 아니라면 innerFnOnGlobal 이 외부에서 outerFn 의 지역변수에 접근하거나 지역변수의 값을 조작하는 행위는 불가능합니다.

    ^^^^^동기분들 💕^^^^

     

    실제 클로저를 사용할 때는 outerFn, innerFn처럼 함수가 함수를 리턴하는 패턴을 자주 사용하고, outerFn을 외부 함수, innerFn을 내부 함수라고 통칭합니다. 클로저에 대해 추가 학습 시 “외부 함수의 변수에 접근할 수 있는 내부 함수”등의 표현을 자주 접할 수 있으니 참고 바랍니다.

    // 클로저 사용 패턴 1
    function outerFn() {
      const outerFnVar = 'outer 함수 내의 변수';
      const innerFn = function() { 
        return 'innerFn은 ' + outerFnVar + '에 접근할 수 있습니다.';
      }
    	return innerFn;
    }
    • 클로저는 함수와 그 함수 주변의 상태의 주소 조합이다.
    • 클로저의 함수는 어디에서 호출되느냐와 무관하게 선언된 함수 주변 환경에 따라 접근할 수 있는 변수가 정해진다.
     

    💁‍♀️ 아래 코드에서 message의 값이 무엇인지 고르세요.

    function outerFn() {
      const innerFn = function() { 
        const message = 'outerFn은 message에 접근할 수 있습니다.';
      }
    	return message;
    }
    
    const message = outerFn();

     

    A. 'outerFn은 message에 접근할 수 있습니다.'

    B. 알 수 없음

     

    🔈 해설

    정답 B

    outerFn을 호출할 때, message를 리턴하려고 시도하지만 messageinnerFn의 스코프, 즉 내부 함수의 스코프에 있기 때문에 접근이 불가하여 ReferenceError가 납니다.

     


    💁‍♀️ 아래 코드에 대한 설명으로 옳지 않은 것을 고르세요.

    const adder = function (x) {
    	return function inner(y) {
    		return x + y;
    	}
    }

     

    A. inner와 inner 함수에서 조회할 수 있는 변수 x의 조합은 클로저다.

    B. adder에서 y에 접근 가능하다.

    C. inner에서 x에 접근 가능하다.

     

    🔈 해설

    정답 B

    adderinner의 스코프, 즉 내부 함수의 스코프에 있기 때문에 접근이 불가합니다.

     


    데이터를 보존하는 함수

    클로저를 활용하면 클로저의 함수 내에 데이터를 보존해두고 사용할 수 있습니다. 자세히 알아보겠습니다.

    일반적으로 함수 내부에 선언한 변수에는 접근할 수 없습니다. 매개변수도 마찬가지입니다.

    function getFoodRecipe (foodName) {
      let ingredient1, ingredient2;
      return `${ingredient1} + ${ingredient2} = ${foodName}!`;
    }
    
    console.log(ingredient1); // ReferenceError: ingredient1 is not defined (함수 내부에 선언한 변수에 접근 불가)
    console.log(foodName); // ReferenceError: foodName is not defined (매개변수에 접근 불가)

    클로저를 응용하면, 함수 내부에 선언한 변수에 접근할 수 있고, 매개변수에도 접근할 수 있습니다. 기존 함수 내부에서 새로운 함수를 리턴하면 클로저로서 활용할 수 있습니다. 즉, 리턴한 새로운 함수의 클로저에 데이터가 보존됩니다.

     

    데이터를 보존하는 함수를 직접 만들어보겠습니다. 레시피를 제작하는 createFoodRecipe 함수를 만들어봅시다. 아래 코드에서는getFoodRecipe가 클로저로서 foodName, ingredient1, ingredient2에 접근할 수 있습니다. 이 때, createFoodRecipe('하이볼') 으로 전달된 문자열 '하이볼' 은 recipe 함수 호출 시 계속 재사용 할 수 있습니다. createFoodRecipe 가 문자열 ‘하이볼’ 을 “보존”하고 있기 때문입니다.

     

    function createFoodRecipe (foodName) {
      let ingredient1 = '탄산수';
      let ingredient2 = '위스키';
      const getFoodRecipe = function () {
        return `${ingredient1} + ${ingredient2} = ${foodName}!`;
      }
      return getFoodRecipe;
    }
    
    const recipe = createFoodRecipe('하이볼');
    recipe(); // '탄산수 + 위스키 = 하이볼!'

    이를 더 잘 응용하기 위해 getFoodRecipe의 매개변수도 활용할 수 있게 코드를 아래와 같이 변경해봅시다.

    function createFoodRecipe (foodName) {
      const getFoodRecipe = function (ingredient1, ingredient2) {
        return `${ingredient1} + ${ingredient2} = ${foodName}!`;
      }
      return getFoodRecipe;
    }
    
    const highballRecipe = createFoodRecipe('하이볼');
    highballRecipe('콜라', '위스키'); // '콜라 + 위스키 = 하이볼!'
    highballRecipe('탄산수', '위스키'); // '탄산수 + 위스키 = 하이볼!'
    highballRecipe('토닉워터', '연태고량주'); // '토닉워터 + 연태고량주 = 하이볼!'

    highballRecipe 함수는 문자열 ‘하이볼’ 을 보존하고 있어서 전달인자를 추가로 전달할 필요가 없고, 다양한 하이볼 레시피를 하나의 함수로 제작할 수 있었습니다.

     

    커링

    커링은 여러 전달인자를 가진 함수함수를 연속적으로 리턴하는 함수로 변경하는 행위입니다. 예시를 먼저 보겠습니다.

    sum 함수는 두 전달인자(10, 20)를 덧셈하는 함수고, currySum은 첫 번째 전달인자 10을 리턴하는 함수로 전달해줍니다. sum과 currySum이 같은 값을 리턴하기 위해서는 currySum 함수에서 리턴한 함수에 두 번째 전달인자 20을 전달하여 호출하면 됩니다. 이렇게 커링을 활용한 currySum과 같은 함수를 커링 함수라고 부르기도 합니다.

     

    function sum(a, b) {
      return a + b;
    }
    
    function currySum(a) {
    	return function(b) {
    		return a + b;
    	};
    }
    
    console.log(sum(10, 20) === currySum(10)(20)) // true

    언뜻 봐서는 일반 함수와 커링 함수의 차이가 느껴지지 않지만, 커링은 전체 프로세스의 일정 부분까지만 실행하는 경우 유용합니다. 아래 makePancake 함수는 팬케이크 제작 과정을 커링 함수로 만들었습니다. 팬케이크는 팬케이크 믹스를 만들어두었다가, 나중에 다시 만들 수도 있습니다. 반면, 커링이 적용되지 않은 makePancakeAtOnce 함수는 일부 조리 과정이 생략된 모습을 표현할 수 없습니다.

     

    function makePancake(powder) {
      return function (sugar) {
    		return function (pan) {
    			return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
    		}
    	}
    }
    
    const addSugar = makePancake('팬케이크가루');
    const cookPancake = addSugar('백설탕');
    const morningPancake = cookPancake('후라이팬');
    
    // 잠깐 낮잠 자고 일어나서 ...
    const lunchPancake = cookPancake('후라이팬');
    function makePancakeAtOnce (powder, sugar, pan) {
      return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
    }
    
    const morningPancake = makePancakeAtOnce('팬케이크가루', '백설탕', '후라이팬')
    // 잠깐 낮잠 자고 일어나서 만든 팬케이크를 표현할 방법이 없다.

    이와 같이 커링은 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장하기에 용이합니다.

     

    모듈 패턴

    JavaScript에 class 키워드가 없던 시절 모듈 패턴을 구현하기 위해서 클로저를 사용했습니다. 모듈은 하나의 기능을 온전히 수행하기 위한 모든 코드를 가지고 있는 코드 모음으로, 하나의 단위로서 역할을 합니다. 모듈은 다른 모듈에 의존적이지 않고 독립적이어야 합니다.

    다른 모듈에 의존적이지 않고 독립적이라면 기능 수행을 위한 모든 기능을 갖추고 있어야 하고, 또한 외부 코드 실행을 통해서 모듈의 속성이 훼손 받지 않아야 합니다. 모듈의 속성을 꼭 변경해야 할 필요가 있는 경우에는 제한적으로 노출된 인터페이스에 의해 변경되어야 합니다. 이 특징은 클로저와 유사합니다. 자세히 알아보겠습니다.

    아래 코드는 계산기의 최소한의 기능을 모듈 패턴으로 구현했습니다. displayValue는 makeCalculator의 코드 블록 외에 다른 곳에서는 접근이 불가능하지만, cal의 메서드는 모두 클로저의 함수로서 displayValue에 접근할 수 있습니다. 이렇게 데이터를 다른 코드 실행으로부터 보호하는 개념을 정보 은닉(information hiding)이라고 합니다. 이는 캡슐화(encapsulation)의 큰 특징이기도 합니다.

    function makeCalculator() {
      let displayValue = 0;
    
      return {
        add: function(num) {
          displayValue = displayValue + num;
        },
        subtract: function(num) {
          displayValue = displayValue - num;
        },
        multiply: function(num) {
          displayValue = displayValue * num;
        },
        divide: function(num) {
          displayValue = displayValue / num;
        },
        reset: function() {
          displayValue = 0;
        },
        display: function() {
          return displayValue
        }
      }
    }
    
    const cal = makeCalculator();
    cal.display(); // 0
    cal.add(1);
    cal.display(); // 1
    console.log(displayValue) // ReferenceError: displayValue is not defined

    이와 같이 클로저는 특정 데이터를 다른 코드의 실행으로부터 보호해야 할 때 용이합니다.

    📝 Summary

    • 클로저는 주로 데이터를 보존하는 함수, 커링, 모듈 패턴으로 활용한다.
    • 클로저를 이용하면 특정 함수가 데이터를 보존할 수 있다.
    • 커링을 이용하면 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장할 수 있다.
    • 모듈 패턴을 이용하면 특정 데이터를 다른 코드의 실행으로부터 보호할 수 있다.

     

    💁‍♀️ '<span>section</span>' 을 콘솔에 출력하기 위한 방법으로 틀린 것을 고르세요.

    const tagMaker = function (tag) {
      return function (content) {
        return `<${tag}>${content}</${tag}>`
      }
    }

     

    A.

    console.log(tagMaker('span')('section'));

    B.

    console.log(tagMaker('section')('span'));

    C.

    const spanMaker = tagMaker('span');
    console.log(spanMaker('section'));

     

    🔈 해설

    정답 B

    console.log(tagMaker('section')('span')); 전달인자의 전달 순서가 잘못되었습니다.

     


     

     

    '코드스테이츠' 카테고리의 다른 글

    4.3 part1  (0) 2023.04.04
    3.11 개인복습  (0) 2023.03.11
    3.2 JavaScript 스코프  (0) 2023.03.02
    3.2 section1 unit 9 원시 자료형과 참조 자료형 종합퀴즈  (0) 2023.03.02
    3.2 JavaScript 원시자료형, 참조자료형  (0) 2023.03.02
Designed by Tistory.