본문 바로가기
👩‍💻 Programming/JavaScript

[코어 자바스크립트] 5장 클로저

by codingBear 2022. 11. 29.
728x90
반응형

01 클로저의 의미 및 원리 이해 

🤔 클로저란?

👉 다양한 서적에서 클로저를 한 문장으로 요약 설명한 것을 소개하자면 다음과 같다.

👉 MDN(Mozilla Developer Network)에서는 클로저를 "A closure is the combination of a function and the lexical environment within which that function was declared."라고 소개한다.

👉 "선언될 당시의 lexical environment"는 실행 컨텍스트 구성 요소 중 outerEnvironmentReference에 해당한다.(LexicalEnvironment 및 실행 컨텍스트에 대해서는 다음 글 참조.)

👉 컨텍스트 A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화된 시점에서는 B의 outerEnvironmentReference가 참조하는 A의 LexicalEnvironment에 접근 가능. 즉 A에서는 B에서 선언한 변수에는 접근할 수 없지만 B에서는 A에서 선언한 변수에 접근 가능.

👉 위의 'combination'이란 '선언될 당시의 LexicalEnvironment와의 상호관계'라 할 수 있음. 이는 내부함수에서 외부 변수를 참조하는 경우에만 해당됨.

 

💡 지금까지 내용을 요약하자면, 클로저란 '어떤 함수에서 선언한 변수를 내부함수에서 참조할 때만 발생하는 현상'이라 할 수 있다.

 

👉 위 예제에서 inner 함수 내부에서는 a 변수를 선언하지 않아 environmentRecord에서 값을 찾지 못하므로 outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 LexicalEnvironment에 접근해서 a 변수를 찾아 4번째 줄에서 a의 값인 2를 출력함.

👉 outer 함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들(a, inner)에 대한 참조를 지움. 각 주소에 저장됐던 값들은 자신을 참조하는 변수가 없으므로 가비지 컬렉터의 수집 대상이 됨.

👉 위 두 예제의 공통점은 outer 함수의 실행 컨텍스트가 종료되기 이전에 inner 함수의 실행 컨텍스트가 종료되어 이후 inner 함수를 호출할 수 없다는 것이다.

 

😲 그렇다면 outer의 실행 컨텍스트가 종료된 후에도 inner 함수를 호출할 수 있게 만들면 어떨까?

👉 6번째 줄에서 inner 함수의 실행 결과가 아닌 함수 자체를 반환함. 8번째 줄에서 outer 함수의 실행 컨텍스트가 종료될 때 outer2 변수는 outer의 실행 결과인 inner 함수 자체를 참조함. 이후 outer2를 호출하면 inner 함수가 실행됨.

👉 inner함수는 outer 함수의 내부에 선언되었으므로 outer 함수의 LexicalEnvironment가 inner 함수의 outerEnvironmentReference에 담김. inner 함수의 실행 컨텍스트 내 environmentRecord에는 수집할 정보가 없음. 따라서 inner 함수를 실행하면 스코프 체이닝에 따라 outer 함수의 LexicalEnvironment에 있는 변수 a에 접근해 1만큼 증가시킨 후 그 값인 2를 반환하고 inner 함수의 실행 컨텍스트가 종료됨.

 

🤔 그런데 inner 함수의 실행 시점에 outer 함수는 이미 실행 종료된 상태인데 outer 함수의 LexicalEnvironment에 어떻게 접근 가능한 것일까? 👉 가비지 컬렉터의 동작 방식을 알아봐야 함.

 

👉 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않음.

👉 위 예제에서 outer 함수는 실행 종료 시점에 inner 함수를 반환함. 즉 외부 함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수는 outer2 함수가 실행될 때 호출될 가능성이 열려 있다는 것임. 따라서 outer2 함수가 실행되면 inner 함수의 실행 컨텍스트가 생성되고 outerEnvironmentReference에서 outer 함수의 LexicalEnvironment를 참조할 것이므로 가비지 컬렉터의 수집 대상에서 제외되는 것임.

 

🔨 이때까지 살펴본 내용을 바탕으로 클로저를 재정의해보자.

👉 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상임. 함수의 실행 컨텍스트가 종료된 후 LexicalEnvironment가 가비지 컬렉터의 수집 대상에서 제외되는 경우는 지역변수를 참조하는 내부함수가 외부로 전달되는 경우가 유일함. 즉 "어떤 함수에서 선언한 변수를 내부함수에서 참조할 때만 발생하는 현상"이란 "외부 함수의 LexicalEnvironment가 가비지 컬렉팅되지 않는 현상"을 뜻함.

👉 개념적으로 클로저는 특정 상황에서만 발생하는 특수한 '현상'이고 '함수'는 이 현상이 나타나기 위한 '조건'이긴 하나 현상을 구체화한 '대상'이라 볼 순 없음. 따라서 실제적인 클로저는 '클로저 현상에 의해 메모리에 남겨진 변수들의 집합'을 지칭하는 것이 좀더 타당함.

 

🖤 return 없이도 클로저가 발생하는 경우들

👉 (1)은 window의 메서드(setTimeout, setInterval 등)에 전달할 콜백 함수 내부에서 지역변수 참조

👉 (2)는 DOM의 메서드(addEventListener)에 등록할 handler 함수 내부에서 지역변수 참조

👉 두 상황 모두 지역변수를 참조하는 내부함수를 외부에 전달했기 때문에 클로저임.


02 클로저와 메모리 관리

👉 클로저는 의도적으로 함수의 지역변수를 위해 메모리를 소모함으로써 발생함. 클로저에 의한 메모리 누수를 막기 위해서는 필요성이 사라진 시점에 메모리를 소모하지 않게 하면 됨. 참조 카운트를 0으로 만들면 GC가 수거하면서 소모됐던 메모리가 회수될 것임. 식별자에 null이나 undefined와 같은 기본형 데이터를 할당하면 됨. 

 


03 클로저 활용 사례

5-3-1 콜백 함수 내부에서 외부데이터를 사용하고자 할 때

🖤 콜백 함수 내부에서 외부변수를 참조하기 위한 3가지 방법

👉 4번째 줄의 forEach 메서드에 넘긴 콜백 함수(A)는 내부에서 외부 변수를 참조하지 않아서 클로저가 없지만, addEventListener에 넘겨준 콜백 함수(B)에서는 fruit라는 외부 변수를 참조하므로 클로저가 있음.

👉 A가 실행될 때마다 새로운 실행 컨텍스트가 활성화됨. A의 실행 종료 여부와 무관하게 클릭 이벤트에 의해 각 컨텍스트의 B가 실행될 때는 B의 outerEnvironmentReference가 A의 LexicalEnvironment를 참조함. 따라서 B가 참조할 예정인 A의 변수 fruit에 대해서는 A 종료 후에도 GC 대상에서 제외되어 계속 참조 가능.

 

🖤 콜백 함수를 공통 함수로 꺼낸 version

👉 14번째 줄에서 alertFruit 함수는 정상적으로 실행되나 li을 클릭 시 [object MouseEvent]가 출력됨. 이는 콜백 함수에 대한 제어권을 addEventListener가 가진 상태이며 addEventListener는 콜백 함수 호출 시 첫 번째 인자에 '이벤트 객체'를 주입하기 때문임. bind 메서드를 써서 해결 가능.

👉 다만 bind 메서드를 적용하면 이벤트 객체가 인자로 넘어오는 순서가 바뀐다는 것과 함수 내부의 this가 원래와는 달라진다는 것을 감안해야 함. this의 경우 원래라면 <li></li>이지만 bind 메서드를 적용하면 첫 번째 인자에 null을 할당하여 this를 없애기 때문에 함수 호출을 할 때의 기본 this인 전역 객체가 됨.

 

🖤 고차함수를 활용한 version

👉 alertFruitBulder 함수 내부에서는 실행 결과로 익명 함수를 반환함. 이 함수가 addEventListener의 콜백 함수로 할당됨. 클릭 이벤트가 발생하면 이 함수의 실행 컨텍스트가 열리면서 인자로 넘어온 fruit를 outerEnvironmentReference에서 참조 가능. 즉 alertFruitBuilder의 실행 결과로 반환된 함수에는 클로저가 존재함.

 

5-3-2 접근 권한 제어(정보 은닉)

🤔 정보 은닉?

👉 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 개념.

👉 public, private, protected 세 종류가 있음. public은 외부에서 접근 가능, private은 내부에서만 사용 가능한 것.

 

🤔 자바스크립트에서는?

👉 자바스크립트에서는 클로저를 활용해서 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능.

👉 외부에 제공하고자 하는 정보들을 모아서 public member로서 return하고, 내부에서만 사용할 정보들은 private member로서 return하지 않는 식으로 접근 권한 제어가 가능.

 

🖤 정보 은닉을 적용하지 않은 예

👉 위 예제의 경우 기능적으로는 아무 이상 없지만 외부에서 내부 변수를 마음대로 조작할 수 있다. 따라서 예제의 객체를 함수화하여 클로저로 보호해야 한다.

 

🖤 클로저를 활용해 정보 은닉을 적용한 예

👉 fuel, power 변수는 비공개 멤버로 지정해 외부에서 접근을 제한했고 moved 변수에는 getter만 부여하여 읽기 전용으로 만들었다. 외부에서는 run 메서드를 실행하거나 현재 moved 값을 확인하는 것만 할 수 있다. 허나 run 메서드를 다른 내용으로 덮어씌우는 것은 여전히 가능한데 이를 Object.freeze() 메서드를 활용해 차단할 수 있다. 외부로 공개할 변수들을 하나의 객체로 묶어 Object.freeze() 메서드의 인자로 전달하면 외부에서 변경할 수 없게 된다.

 

🖤 클로저를 활용해 접근권한을 제어하는 방법

  1. 외부에 접근 권한을 주고자 하는 대상들로 구성된 참조형 데이터(여럿일 때는 객체 혹은 배열, 하나일 때는 함수)를 return
  2. return한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 됨.

 

5-3-3 부분 적용 함수

🤔 부분 적용 함수?

👉 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨놨다가 나중에 (n - m)개의 인자를 넘겨 원래 함수의 실행 결과를 얻는 함수. (ex. bind 메서드)

 

👉 예제의 addPartial 함수는 인자 5개를 미리 적용해놓고 추후 인자들을 전달하면 모든 인자들을 모아 원래의 함수가 실행되는 '부분 적용 함수'이다.

👉 add 함수는 this를 사용하지 않으므로 bind 메서드만으로 문제 없이 구현되었다. 허나 this에 관여하지 않는 별도의 부분 적용 함수가 있다면 범용성이 더 뛰어날 것이다.

 

👉 partial 함수의 첫 번째 인자에는 원본 함수를, 두 번째 인자 이후부터는 미리 적용할 인자들을 전달하고, 반환할 함수(부분 적용 함수)에서는 다시 나머지 인자들을 한데 모아(concat) 원본 함수를 호출(apply)한다. 또한 실행 시점의 this를 반영함으로써 this에는 아무런 영향을 주지 않는다.

👉 허나 위 예제의 경우 인자를 앞에서부터 차례대로 전달할 수밖에 없다. 이를 수정하면 다음과 같다.

 

👉 '비워놓음'을 표시하기 위해 전역객체에 _라는 프로퍼티를 설정하면서 삭제, 변경 등의 가능 여부도 프로퍼티로 설정한다.

👉 앞서 살펴본 예제와 다른 부분은 17번째 줄부터 21번째 줄까지이다. 처음에 넘겨준 인자들 중_로 비워놓은 공간마다 나중에 넘어온 인자들로 차례대로 변경되도록 구현했다.

 

😲 위 두 예제 모두 클로저를 핵심 기법으로 사용했다. 미리 저장해놓은 인자들을 필요한 시점에 활용한다는 것이 클로저의 개념에 부합한다.

 

🖤 클로저를 실무에서 활용한 예 - 디바운스

👉 디바운스는 이벤트가 여러 개 발생할 경우 처음 또는 마지막에 발생한 이벤트를 처리하는 것으로 프론트엔드 성능 최적화에 도움을 준다. scroll, wheel, mousemove, resize 등에 적용하기 좋다.

👉 위 debounce 함수 내부에서는 timeoutId 변수를 생성하고 클로저로 EventListener에 의해 호출될 함수를 반환한다. 반환될 함수 내부에서는 setTimeout을 사용하기 위해 this를 별도의 변수에 담고 clearTimeout으로 대기큐를 초기화한 뒤 setTimeout으로 func를 호출한다.

👉 최초 event가 발생하면 timeout의 대기열에 'wait 시간 뒤에 func를 실행할 것'이라는 내용이 담긴다. wait 시간 경과 이전에 동일한 event가 발생하면 clearTimeout으로 대기열을 초기화하고 다시 setTimeout으로 새로운 이벤트를 대기열에 등록한다. 이러한 과정을 거쳐 마지막에 발생한 이벤트만 초기화되지 않고 실행된다.

👉 위 예제에서 클로저로 처리되는 변수는 eventName, func, wait, timeoutId이다.

 

🖤 ES6 Symbol.for

👉 앞선 예제에서 '비워놓음'을 처리하기 위해 어쩔 수 없이 전역공간을 침범했다. 허나 ES6에서는 Symbol.for 메서드를 통해 똑같은 기능을 구현할 수 있다. Symbol.for 메서드는 전역 심볼공간에 인자로 넘어온 문자열이 이미 있으면 해당 값을 참조하고 선언돼 있지 않으면 새로만든다.

👉 어디서든 접근 가능하면서 유일무이한 상수를 만들고자 할 때 적합하다.

 

5-3-4 커링 함수

👉 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출되게끔 체인 형태로 구성한 함수. 필요한 인자 개수만큼 함수를 만들어 계속 리턴하다가 마지막에 모든 인자를 조합하여 리턴함.

👉 한 번에 하나의 인자만 전달함. 중간 과정의 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기할 뿐이므로 마지막 인자가 전달되기 전까지 원본 함수가 실행되지 않음.(부분 적용 함수는 여러 개의 인자를 전달할 수 있고 실행 결과를 재실행할 때 무조건 원본 함수가 실행됨.)

👉 각 단계에서 받은 인자들은 모두 마지막 단계에서 참조되므로 GC의 수거 대상이 되지 않고 메모리에 쌓였다가 마지막 호출로 실행 컨텍스트가 종료된 후에 한꺼번에 GC의 수거 대상이 됨.

 

🤔 커링 함수의 가독성을 좋게 만드는 방법

👉 ES6의 화살표 함수를 써서 간단히 한 줄로 나타낼 수 있다.

 

🤔 커링 함수가 유용한 경우

1. HTML5 fetch()

👉 HTML5의 fetch 함수는 url을 받아 해당 url에 HTTP 요청을 함. 매번 새로운 url을 기입하기보다 공통적인 요소는 미리 저장해두고 특정한 값만으로 서버 요청을 수행하는 함수를 만드는 편이 효율적임.

 

2. Redux의 미들웨어(middleware)

👉 위 예제에서 store와 next의 값이 결정되면 Redux 내부에서 logger 또는 thunk에 store, next를 미리 넘겨 반환된 함수를 저장해놓고 이후 action만 받아서 처리함.


06 정리

  • 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상.
  • 내부함수를 외부로 전달하는 방법에는 함수를 return하는 경우뿐 아니라 콜백으로 전달하는 경우도 포함.
  • 클로저는 메모리를 계속 차지하므로 사용하지 않는 클로저가 메모리를 차지하지 않도록 관리해야 함.
728x90
반응형

댓글