자바스크립트 심화편 - 2. 클로저

Seoyong Lee·2023년 10월 6일
0

개발 공부

목록 보기
14/21
post-thumbnail
  • 자바스크립트를 어느 정도 공부하다 보면 클로저라는 장벽을 만나게 될 것이다.

자바스크립트를 사용해봤지만 단 한 번도 클로저 개념을 완전히 이해한 적이 없는 이들에게는 클로저가 열반에 드는 것처럼 고된 노력을 들여야 이해할 수 있는 것일지도 모르겠다.

…깨달음의 순간이 이럴 것이다. “아, 클로저는 내 코드 전반에서 이미 일어나고 있었구나! 이제 난 클로저를 볼 수 있어.”

카일 심슨 , 『You Don’t Know JS - 타입과 문법, 스코프와 클로저』, 한빛미디어(2017), p240.

  • 클로저를 모르고 넘어가기엔 너무나 많은 사람들이 중요성을 강조해왔다.

클로저는 프로그래밍 언어의 긴 역사 속에서도 중요한 발견 중 하나입니다. 스킴(Scheme) 언어에서 처음 발견되었죠. 그리고 자바스크립트의 주류에까지 이르렀습니다. 클로저 덕분에 자바스크립트가 더 흥미로운 언어가 되었습니다.

더글러스 크락포드 , 『자바스크립트는 왜 그 모양일까?』, 인사이트(2020), p140-141.

  • 앞서 스코프에 대해 이해했다면 이제 클로저에 대해 자세히 살펴볼 차례이다.

클로저의 정의

  • 클로저란 정확히 무엇일까?

클로저는 주변 상태(렉시컬 환경)에 대한 참조와 함께 묶인(포함된) 함수의 조합입니다.

MDN, 클로저

  • MDN의 클로저에 대한 정의는 신기하게도 한글이지만 이해가 되지 않는다.

클로저는 함수가 속한 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능을 뜻한다.

카일 심슨 , 『You Don’t Know JS - 타입과 문법, 스코프와 클로저』, 한빛미디어(2017), p240.

  • 렉시컬 스코프를 기억했다 접근한다? 이는 코드를 통해 확인하면 더 쉽게 이해할 수 있다.

코드로 살펴보기

const increase = function() {
	let num = 0; // 상태 변수

	return ++num; // 1 증가
}

console.log(num); // num is not defined
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
  • 위 코드를 스코프 관점에서 살펴보면 let은 블록 레벨 스코프를 가지므로 num이라는 지역 변수는 increase 함수 밖에서 참조할 수 없다.
  • 지역 변수 num의 상태를 변경할 수 있는 유일한 방법은 increase 함수를 호출하는 것이다. 그러나 의도한 대로 숫자가 증가하지 않는다. 그 이유는 increase 함수 호출 시마다 num이 다시 선언되기 때문이다. 다시 말해, 상태가 변경되기 이전 상태를 유지하지 못한다.
const increase = (function() {
	let num = 0; // 상태 변수
	// 클로저
	return function() {
		return ++num; // 1 증가
	};
}());

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
  • 위 코드는 num의 이전 상태를 기억하고 숫자가 1씩 증가한다. 단계별로 살펴보면 다음과 같다.
    1. 위 코드가 실행되면 즉시 실행 함수가 호출된다.
    2. 호출 즉시 실행 함수가 반환한 함수(클로저)가 increase 변수에 할당된다.
    3. 이때 반환된 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하고 있다. 따라서 클로저는 num을 언제 어디서 호출하든지 참조할 수 있다.
      • 정확하게 렉시컬 환경은 클로저 함수 객체의 내부 슬롯 [[Environment]]에서 참조한다.
  • 신기하게도 다음과 같이 함수가 중첩되어 있더라도 모든 스코프에 접근이 가능하다.
    // 전역 범위 (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
  • 그렇다면 왜 이런 클로저를 사용해야 할까?

변수 값은 누군가에 의해 언제든지 변경될 수 있어 오류 발생의 근본적 원인이 될 수 있다. 외부 상태 변경이나 가변(mutable) 데이터를 피하고 불변성(immutability)을 지향하는 함수형 프로그래밍에서 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.

이웅모 , 『모던 자바스크립트 Deep Dive』, 위키북스(2020), p405.


클로저의 활용

  • 클로저는 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.

    • 따라서 외부에 변수를 노출하고 싶지 않으면서 상태 변경의 통제가 필요한 상황에 적용할 수 있다.
    • ES2019에서 #가 추가되기 전에는 private class 선언을 위해 많이 사용되었다.
  • 다음은 MDN의 예시로, 글자 크기를 바꾸는 DOM 관련 예시이다. size가 그대로 글자 크기 px이 되지만 이를 특정 함수만을 사용해서 변경하도록 캡슐화 되어있다.

    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;
  • 다음과 같이 싱글톤 패턴을 응용해 간단한 로거를 구현해 볼 수 있다.

    const LoggingService = (function () {
      const infoMessage = 'Info: ';
      const warningMessage = 'Warning: ';
      const errorMessage = 'Error: ';
    
      return {
        info: function (str) {
          console.log(`${infoMessage}${str}`);
        },
        warning: function (str) {
          console.log(`${warningMessage}${str}`);
        },
        error: function (str) {
          console.log(`${errorMessage}${str}`);
        },
      };
    })();
    
    // someOtherFile.js
    
    LoggingService.info('one'); // Info: one
    LoggingService.warning('two'); // Warning: two
    LoggingService.error('three'); // Error: three
  • 고차함수에도 사용할 수 있다. 예시는 여기를 참고하였다.

    const floatingPoint = 3.456789;
    
    const someInt = Math.round(floatingPoint); // 3
    const withDecimals = Number(floatingPoint.toFixed(2)); // 3.46
    • 위 함수를 유틸 함수로 바꿔서 가독성을 높인다면 다음과 같이 클로저를 사용할 수 있다.
    function rounder(places) {
      return function (num) {
        return Number(num.toFixed(places));
      };
    }
    
    const rounder2 = rounder(2);
    const rounder3 = rounder(3);
    
    rounder2(floatingPoint); // 3.46
    rounder3(floatingPoint); // 3.457

References

더글러스 크락포드 , 『자바스크립트는 왜 그 모양일까?』, 인사이트(2020)
카일 심슨 , 『You Don’t Know JS - 타입과 문법, 스코프와 클로저』, 한빛미디어(2017)
이웅모 , 『모던 자바스크립트 Deep Dive』, 위키북스(2020)
MDN
Ilya Meerovich, 3 Use Cases for Closures (in JavaScript)

0개의 댓글