클로저, Closure란?
const x = 1; const outer = () => { const x = 10; let y = 20; return () => { y += 10; console.log(x, y); } } const result = outer(); result(); result(); result();
outer는 익명 함수를 반환하고 생을 마감했다. 즉 outer는 콜 스택에서 실행 컨텍스트가 제거되어 x, y에 접근할 방법은 달리 없어보인다. 하지만 실제로 실행해보면 여전히 x, y 값을 출력할 수 있었다.(이전에 저는 reference error가 발생할 것이라 봤었습니다 ㅎㅎㅎ)
외부 함수: outer
내부 함수: 익명 함수
이처럼 자신을 포함하고 있는 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부 함수의 지역 변수에 접근할 수 있는 함수를 클로저라고 한다.
MDN의 클로저 정의 A Closure is the combination of a function and the lexical environment within which that function was declared. 클로저는 함수와 그 함수가 선언되었을 때의 렉시컬 환경과의 조합이다.
Lexical Scope
자바스크립트는 Lexical Scope로 변수, 함수, 클래스와 같은 식별자들은 본인이 선언된 위치에 따라 다른 코드에서 자신이 참조될 수 있을지 없을지 결정되는 것.
- Lexical Scope: 함수와 변수의 scope는 선언되었을 때 정해진다.
- 위 코드를 예로 들자면 x는 10이 찍힌다.
- Dynamic Scope: 함수와 변수의 scope는 호출되었을 때 정해진다.
- 위 코드를 예로 들자면 x는 1이 찍힌다.
여기서 앞의 ‘함수’는 내부 함수를 의미하고, 그 ‘함수가 선언되었을 때의 렉시컬 환경’은 내부 함수가 선언되었을 때의 스코프를 의미한다. 즉 클로저는 반환된 내부 함수가 자신이 선언되었을 때의 환경인 스코프를 기억하여, 자신이 선언되었을 때의 스코프 밖에서 호출되어도 그 스코프에 접근할 수 있는 함수이다. 즉 클로저는 자신이 생성될 때의 Lexical Environment를 기억하는 함수이다.


LexicalEnvironment는 Execution Context에 담겨 있는 것이 아니라 단지 Execution Context가 Lexical Environment를 향한 포인터를 가지고 있을 뿐이다. 따라서 result(익명 함수를 반환받은 변수)가 x를 참조하고 있다면 GC에 의해 제거되지 않는다.
클로저 용도
상태 기억
const factorialWithCache = (() => { const cache = {}; return function factorial(value) { if (value <= 1) { cache[value] = 1; return 1; } if (cache[value]) return cache[value]; cache[value] = value * factorial(value - 1); return cache[value]; }; })(); const result = factorialWithCache(4); console.log(result); const result2 = factorialWithCache(3); console.log(result2);
factorialWithCache 함수가 실행될 때 마다 내부 cache 객체는 갱신된다. 4를 인자로 넘겼을 때는 1, 2, 3, 4에 대해 다음과 같이 cache가 값을 가지고 있을 것이다.
{ '1': 1, '2': 2, '3': 6, '4': 24 }
따라서 3을 인자로 넘겼을 때는 별다른 연산 혹은 재귀 과정 없이 2번째 if문에서 곧바로 함수 실행이 끝나고 6이 반환된다. 이는 클로저를 활용해, 계속해서 갱신된 cache라는 객체를 기억하고 있기 때문이다.
상태 은닉
const outer = function () { let count = 0; return function increaseCount() { count = count + 1; return count; } } const result = outer(); console.log(result()); // 1 console.log(result()); // 2 console.log(count); // Reference Error
count라는 함수는 이제 result를 통해서만 접근이 가능해진다. 밖에서 직접적으로 count에 접근할 수 없어진다.
상태 공유
const makeStudent = (function makeClass(classTeacherName) { return function inner(name) { return { name: name, getTeacherName: () => classTeacherName, setTeacherName: _name => { classTeacherName = _name; }, }; }; })('포켓몬'); const x = makeStudent('메타'); const y = makeStudent('타몽'); x.setTeacherName('메타몽'); console.log(x.getTeacherName()); // 메타몽 console.log(y.getTeacherName()); // 메타몽
즉시 실행 함수를 통해 makeStudent에는 classTeacherName을 참조하는 inner 함수가 곧바로 반환되어 할당되었다. 즉 makeStudent는 inner 함수이다. x만 setTeacherName으로 classTeacherName을 바꿔주었으나, y도 classTearcherName이 메타몽으로 찍힌다. 즉 한 쪽에서 바꾸면 다른 한 쪽에도 바뀐대로 공유가 된다는 뜻이다.
Closure on React Hooks
React에서 useState와 useEffect를 사용하면서, 자연스럽게 우리는 Closure를 활용하고 있었다.
useState, useEffect
const { useState, useEffect, render } = (function makeMyHooks() { const hooks = []; let index = 0; const useState = (initValue) => { const state = hooks[index] || initValue; hooks[index] = state; const currentIndex = index; const setState = (newValue) => { hooks[currentIndex] = newValue; render(); }; index++; return [state, setState]; }; const useEffect = (callback, deps) => { const oldDeps = hooks[index]; let hasChanged = true; if (oldDeps) { hasChanged = deps.some((dep, idx) => !Object.is(dep, oldDeps[idx])); } if (hasChanged) callback(); hooks[index] = deps; index++; } const render = () => { index = 0; root.render(<App />); }; return { useState, useEffect, render }; })();
즉시 실행 함수로 호출되면서 useState, useEffect, render를 만드는 함수의 실행 컨텍스트는 콜 스택에서 빠졌다. 하지만 여전히 hooks와 index를 useState, useEffect, render는 참조할 수 있다.
참고
closure-on-react-hooks
metacode22 • Updated Oct 7, 2022