ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 클로저(Closure) - JavaScript 공식문서 정리
    JavaScript 2024. 2. 25. 21:15

    클로저란?

    주변 상태(어휘적 환경)에 대한 참조와 함께 묶인(포함된) 함수의 조합

     

    즉, 클로저는 내부함수에서 외부 함수의 범위에 대한 접근을 제공한다.

     

     

     

    예시

    function makeFunc() {
      const name = "Mozilla";
      function displayName() {
        console.log(name);
      }
      return displayName;
    }
    
    const myFunc = makeFunc();
    myFunc();
    
    

    여기서 신기한점은 displayName()이 실행되기 전에 makeFunc()에서 반환한다는 점이다.

    몇몇 프로그래밍 언어에서 함수 안의 지역변수들은 그 함수가 처리되는 동안에만 존재한다.

    displayName의 인스턴스는 변수 name이 있는 어휘적 환경에 대한 참조를 유지한다.

    이런 이유로, myFunc가 호출 될때 변수 name은 사용할 수 있는 상태로 남게 되고, “Mozilla”가 console.log에 전달된다.

     

    다음은 조금 더 흥미로운 예제이다.

    function makeAdder(x) {
      return function (y) {
        return x + y;
      };
    }
    
    const add5 = makeAdder(5);
    const add10 = makeAdder(10);
    
    console.log(add5(2)); // 7
    console.log(add10(2)); // 12
    
    

    이 예제에서, 단일 인자 x를 받아서 새 함수를 반환하는 함수 makeAdder(x)를 정의한다.

    반환되는 함수는 단일인자 y를 받아서 x와 y의 합을 반환한다.

     

    본질적으로, makeAdder는 함수를 만들어내는 팩토리이다.

    이는 makeAdder 함수가 특정한 값을 인자로 가질 수 있는 함수들을 리턴한다는 것을 의미한다.

    위의 예제에서 makeAdder 함수는 인자에 5와 10을 더하는 두개의 새로운 함수를 만들었다.

     

    add5와 add10은 전부 클로저이다. 이들은 같은 함수 본문 정의를 공유하지만 서로 다른 맥락(어휘)적 환경을 저장한다.

    함수 실행 시 add5의 어휘적 환경에서, 클로저 내부의 x는 5 이지만 add10의 어휘적 환경에서 x는 10이다.

    실용적인 클로저

    클로저는 어떤 데이터(어휘적 환경)와 그 데이터를 조작하는 함수를 연관시켜주기 때문에 유용하다.

    이것은 객체가 어떤 데이터와 하나 혹은 그 이상의 메소드들을 연관시킨다는 점에서 객체지향 프로그래밍이라고 볼 수 있다.

     

    결론적으로, 오직 하나의 메소드를 가지고 있는 객체를 일반적으로 사용하는 모든 곳에 클로저를 사용할 수 있다.

     

    프론트엔드 JavaScript에서 쓰는 많은 코드가 이벤트 기반이다.

    우리는 몇가지 동작을 정의한 다음 사용자에 의한 이벤트(클릭 혹은 키 누르기)에 연결한다.

    이 코드는 일반적으로 콜백으로 추가된다.

    // HTML
    <a href="#" id="size-12">12</a>
    <a href="#" id="size-14">14</a>
    <a href="#" id="size-16">16</a>
    

    이런 HTML 코드가 있고 CSS는 이렇다.

    // CSS
    body {
      font-family: Helvetica, Arial, sans-serif;
      font-size: 12px;
    }
    
    h1 {
      font-size: 1.5em;
    }
    
    h2 {
      font-size: 1.2em;
    }
    
    

    이런 구조로 되있는데 이 코드를 버튼을 누르면 이벤트가 발생하여 폰트 크기를 바꾸는 함수를 JavaScript로 작성하면

    // JS
    function makeSizer(size) {
      return function () {
        document.body.style.fontSize = `${size}px`;
      };
    }
    
    // 사전 정의된 함수. 글자 크기 바꾸는 함수, 클로저.
    const size12 = makeSizer(12);
    const size14 = makeSizer(14);
    const size16 = makeSizer(16);
    
    // 이벤트 발생시 콜백함수
    document.getElementById("size-12").onclick = size12;
    document.getElementById("size-14").onclick = size14;
    document.getElementById("size-16").onclick = size16;
    
    

    이렇게 해서 클로저를 사용할 수 있다.

    클로저를 이용해서 비공개 메서드(private method) 흉내내기

    자바와 같은 몇몇 언어들은 메서드를 비공개로 선언할 수 있는 기능을 제공한다.

    이는 같은 클래스 내부의 다른 메서드에서만 그 메서드를 호출할 수 있다는 의미이다.

     

    예전 JavaScript에는 비공개 메서드를 선언하는 기본 방법이 없었다.

    하지만 지금은 클로저를 사용해서 비공개 메서드를 흉내낼 수 있다.

    비공개 메서드는 코드에 대한 접근을 제한하는 데만 유용한 것이 아니다.

    전역 변수를 관리하는 것도 매우 유용하다.

     

     

    아래 코드는 비공개 함수와 변수에 접근하는 퍼블릭 함수를 정의하기 위해 클로저를 사용하는 방법을 보여준다.

    이렇게 클로저를 사용하는 것을 “모듈 디자인 패턴” 을 따른다고 한다

    const counter = (function () {
      let privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
    
      return {
        increment() {
          changeBy(1);
        },
    
        decrement() {
          changeBy(-1);
        },
    
        value() {
          return privateCounter;
        },
      };
    })();
    
    console.log(counter.value()); // 0.
    
    counter.increment();
    counter.increment();
    console.log(counter.value()); // 2.
    
    counter.decrement();
    console.log(counter.value()); // 1.
    
    

    이전 예제에서는 각 클로저들이 고유한 어휘적 환경을 가졌지만,

    여기서는 counter.increment, counter.decrement, counter.value 세 함수에 의해 공유되는 하나의 어휘적 환경을 만들어준다.

     

    공유되는 어휘적 환경은 실행되는 익명 함수 안에서 만들어진다.

    이 익명 함수는 정의되는 즉시 실행된다.

    이 어휘적 환경은 두 개의 비공개 항목을 포함한다.

    하나는 privateCounter 라는 변수이고, 나머지 하나는 changeBy 라는 함수이다.

    둘 다 익명함수 외부에서 접근될 수 없는 비공개 항목이다.

    대신에 익명 래퍼에서 반환된 세 개의 공개 함수를 통해서만 접근할 수 있다.

     

     

    위의 세 가지 공개 함수는 같은 환경을 공유하는 클로저이다.

    JavaScript의 어휘적 유효 범위 덕분에, 세 함수 각각 privateCounter 변수와 changeBy 함수에 접근할 수 있다.

     

    ℹ️ 이러한 방식으로 클로저를 사용하여 객체지향 프로그래밍의 정보 은닉과 캡슐화 같은 이점을 얻을 수 있다.

    클로저 스코프 체인

    모든 클로저에는 세가지 스코프가 있다.

    • 지역 범위
    • 포함하고 있는 범위
    • 전역 범위

    일반적으로 하는 실수는, 외부 함수 자체가 중첩된 함수인 경우,

    외부 함수의 범위에 대한 접근에 외부 함수의 둘러싸고 있는 범위가 포함된다는 사실을 깨닫지 못하는 것이다.

    즉, 효과적으로 함수 범위 체인을 생성한다.

    아래 예제를 보겠다.

    // 전역 범위 (global scope)
    const e = 10;
    function sum(a) {
      return function (b) {
        return function (c) {
          // 외부 함수 범위 (outer functions scope)
          return function (d) {
            // 지역 범위 (local scope)
            return a + b + c + d + e;
          };
        };
      };
    }
    
    console.log(sum(1)(2)(3)(4)); // 20
    
    

    익명함수 없이 작성할 수도 있다.

    // 전역 범위 (global scope)
    const e = 10;
    function sum(a){
    	return function sum2(b){
    		return function sum3(c){
    			return function sum4(d){
    				return a + b + c +d + e;
    			}
    		}
    	}
    }
    
    const sum2 = sum(1)
    const sum3 = sum2(2)
    const sum4 = sum3(3)
    const result = sum4(4);
    console.log(result) // 20
    

    위의 예제를 보면, 일련된 중첩된 함수를 확인할 수 있다.

    이 함수들은 전부 외부 함수의 범위에 접근할 수 있다.

    이 문맥에서는 클로저가 선언된 “모든” 외부 함수의 스코프에 접근한다고 말할 수 있다.

     

    클로저는 블록 범위와 모듈 범위에서도 변수를 캡처할 수 있다.

    예를 들어, 다음은 블록 범위 변수 y에 대한 클로저를 생성한다.

    function outer() {
      let getY;
      {
        const y = 6;
        getY = () => y;
      }
      console.log(typeof y); // undefined
      console.log(getY()); // 6
    }
    
    outer();
    

    모듈에 대한 클로저는 더욱 흥미롭다.

    // myModule.js
    let x = 5;
    export const getX = () => x;
    export const setX = (val) => {
      x = val;
    };
    

    여기에서, 모듈은 모듈 범위 변수 x를 닫는 한 쌍의 getter-setter 함수를 내보낸다.

    x는 다른 모듈에서 직접 접근할 수 없는 경우에도, 함수를 사용하여 읽고 쓸 수 있다.

    import { getX, setX } from "./myModule.js";
    
    console.log(getX()); // 5
    setX(6);
    console.log(getX()); // 6
    
    

    성능 관련 고려 사항

    앞에서 언급했듯이, 각 함수 인스턴스는 자체 범위와 클로저를 관리한다.

    특정 작업에 클로저가 필요하지 않는데 다른 함수 내에서 불필요하게 작성하는 것은 처리 속도와 메모리 소비 측면에서 스크립트 성능에 부정적인 영향을 미치기 때문에, 현명하지 않다.

     

    예를 들어, 새로운 객체/클래스를 생성할 때, 메소드는 일반적으로 객체 생성자에 정의되기보다는 객체의 프로토타입에 연결되어야한다.

    그 이유는 생성자가 호출될때마다 메서드가 다시 할당되기 때문이다. (즉, 모든 객체가 생성될 때마다)

    다음 예를 생각해보자.

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
      this.getName = function () {
        return this.name;
      };
    
      this.getMessage = function () {
        return this.message;
      };
    }
    
    

    앞의 코드는 특정 인스턴스에서 클로저의 이점을 활용하지 않으므로 다음과 같이 클로저를 사용하지 않도록 다시 쓸 수 있다.

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
    }
    MyObject.prototype = {
      getName() {
        return this.name;
      },
      getMessage() {
        return this.message;
      },
    };
    
    

    그러나, 프로토타입을 재정의하는 것은 권장되지 않으므로, 기존 프로토타입에 추가하는 다음 예제가 더 좋다.

    'JavaScript' 카테고리의 다른 글

    호이스팅 - JavaScript  (0) 2024.02.25
    배열에 속성할당하기  (0) 2024.02.18
Designed by Tistory.