🔁

JavaScript의 클로저

Date
2021/04/28
Tags
JavaScript
Created by
책의 내용을 공부하면서 정리해 보았다.
Table of Contents

클로저

자바스크립트의 클로저는 꽤나 유명해서 많은 개발자들이 잘 알고 있을 것이다. 그래도 클로저에 대해서 다시 한번 잘 정리해 보자. 클로저에 대해 어느 정도 알고 할지라도 해당 내용을 건너뛰지 말길 바란다. 클로저는 함수형 자바스크립트에서 매우 중요하며 계속해서 활용되기 때문에 보다 정확한 이해가 필요하다.
스코프에 대한 개념을 잘 알고 있다면 이 글을 읽는 데 더욱 도움이 될 것이다. 스코프란 변수를 어디에서 어떻게 찾을지를 정한 규칙으로, 여기에서 다루는 스코프는 함수 단위의 변수 참조에 대한 것이다.
함수는 변수 참조 범위를 결정하는 중요한 기준이다. 함수가 중첩되어 있다면 스코프들 역시 중첩되어 생겨난다.
클로저는 자신이 생성될 때의 환경을 기억하는 함수다.
이 말을 보다 실용적으로 표현해 보면 '클로저는 자신의 상위 스코프의 변수를 참조할 수 있다'고 할 수 있다. 맞는 말이지만 오해의 소지가 많은 표현이다. 오해의 소지를 좀 더 줄인 정의를 만들어 보면 다음 정도이다.
클로저는 자신이 생성될 때의 스코프에서 알 수 있었던 변수를 기억하는 함수다.
자바스크립트의 모든 함수는 글로벌 스코프에 선언되거나 함수 안에서 선언된다. 자바스크립트의 모든 함수는 상위 스코프를 가지며 모든 함수는 자신이 정의되는 순간의(정의되는 곳의) 실행 컨텍스트 안에 있다. 자바스크립트의 모든 함수는 어느 곳에서 생성하든 어떤 방법으로 생성하든 자신이 생성될 때의 환경을 기억할 수 있다. 그렇다면 모든 함수는 곧 클로저일까?
관점에 따라 그렇게 해석하거나 정의하는 경우도 있다. 개인적으로는 클로저라는 용어에 담긴 속성이나 특징들을 모두 빠짐없이 가지고 있는 특별한 함수만을 클로저라고 칭하는 것이 옳다고 생각한다. 함수가 의미적으로나 실제적으로나 진짜 클로저가 되기 위한 가장 중요한 조건은 다음과 같다.
클로저로 만들 함수가 myfn이라면, myfn 내부에서 사용하고 있는 변수 중에 myfn 내부에서 선언되지 않은 변수가 있어야 한다. 그 변수를 a라고 한다면, a라는 이름의 변수가 myfn을 생성하는 스코프에서 선언되었거나 알 수 있어야 한다.
function parent() { var a = 5; function myfn() { console.log(a); } }
JavaScript
복사
parent의 myfn에서는 a라는 변수를 선언하지 않았지만 사용하고 있다. parent의 변수 a는 myfn을 생성하는 스코프에서 정의되었다.
위와 같은 조건을 충족시키지 않는다면 그 함수가 아무리 함수 안에서 선언되었다고 하더라도 일반 함수와 전혀 다를 바가 없다. 클로저가 기억할 환경이라는 것은 외부의 변수들밖에 없기 때문이다. (여기서 외부의 변수들이란 외부의 함수들도 포함한다.) 또한 자신의 상위 스코프에서 알 수 있는 변수를 자신이 사용하고 있지 않다면 환경을 기억해야 할 필요가 없다. (자바스크립트 엔진에 따라 기억하는 경우가 있지만 사용하지 않기에 의미가 없다.
글로벌 스코프를 제외한 외부 스코프에 있었던 변수 중 클로저 혹은 다른 누군가가 참조하고 있지 않는 모든 변수는 실행 컨텍스트가 끝난 후 가비지 컬렉션 대상이 된다. 어떤 함수가 외부 스코프의 변수를 사용하지 않았고, 그래서 외부 스코프의 환경이 가비지 컬렉션 대상이 된다면 그렇게 내버려 두는 함수를 클로저라고 부르기는 어렵다.
자바스크립트 엔진이 클로저와 관련하여 어떻게 동작하는가는 클로저에 대한 실제적인 관점이 될 것이다. 2016년을 기준으로 자바스크립트를 사용하는 대부분의 환경들에서는 특정 조건의 함수만 클로저가 된다. 클로저 생성에 대해서는 V8 > 파이어폭스 > 사파리 등의 순으로 최적화가 잘 되어 있다. V8은 크롬, Node.js, 오페라 등에서 사용하고 있다. 우선 V8과 파이어폭스는 내부 함수가 사용하는 변수 중 외부 스코프의 변수가 하나도 없는 경우에는 클로저가 되지 않는다. 또한 클로저가 된 경우에도 자신이 사용한 변수만 기억하며 외부 스코프의 나머지 변수는 전혀 기억하지 않는다. 2016년을 기준으로 자바스크립트가 가장 많이 사용되는 환경은 Node.js와 크롬일 텐데, V8에서 특히 클로저에 대한 최적화가 잘 되어 있어 클로저를 사용하는 데 부담이 적다. 그 외의 환경에서도 클로저를 외부로 리턴하여 지속적으로 참조해야만 메모리에 남는다. 그렇지 않다면 모두 가비지 컬렉션 대상이 된다.
앞서 정의에서는 문장이 너무 길면 이해가 어렵고 복잡할 것 같아 짧게 표현했었다. 내용을 좀 더 정확하게 정의해 보면 다음과 같다.
클로저는 자신이 생성될 때의 스코프에서 알 수 있었던 변수 중 언젠가 자신이 실행될 때 사용할 변수들만 기억하여 유지시키는 함수다.
예제들을 통해 클로저에 대해 더 자세히 확인해 보자.
var a = 10; var b = 20; function f1() { return a + b; } f1(); // 30
JavaScript
복사
f1의 실행 결과가 30으로 나오는 것으로 보아 f1에서 ab를 참조할 수 있는 것으로 보인다.
이 예제에서 f1은 클로저일까? 결론부터 말하면 클로저가 아니다. 일단 f1은 클로저처럼 외부 변수를 참조하여 결과를 만든다. 게다가 상위 스코프의 변수를 사용하고 있으므로 앞서 강조했던 조건을 모두 충족시키고 있다. 그런데 왜 클로저가 아니라는 걸까?
글로벌 스코프에서 선언된 모든 변수는 그 변수를 사용하는 함수가 있는지 없는지와 관계없이 유지된다. ab 변수가 f1에 의해 사라지지 못하는 상황이 아니므로 f1은 클로저가 아니다.
그렇다면 클로저는 '함수 안에서 함수가 생성될 때'만 생성된다고 할 수 있을까? 그렇지 않다. 웹 브라우저에서는 함수 내부가 아니라면 모두 글로벌 스코프지만, 요즘 자바스크립트에서는 함수 내부가 아니면서 글로벌 스코프도 아닌 경우가 있다. Node.js가 그렇다. Node.js에서 사용하는 js 파일 하나의 스코프는 글로벌 스코프가 아니다. 그러므로 만일 해당 예제와 동일한 코드가 브라우저가 아닌 Node.js에서 사용할 특정 js 파일에 작성되어 있었다면 f1은 클로저다.
function f2() { var a = 10; var b = 20; function f3(c, d) { return c + d; } return f3; } var f4 = f2(); f4(5, 7); // 12
JavaScript
복사
위 코드에 클로저가 있을까? 특히 f3처럼 함수 안에서 함수를 리턴하면 클로저처럼 보인다. 하지만 이 코드의 f4에 담긴 f3도 클로저가 아니다.
f3f2 안에서 생성되었고 f3 바로 위에는 a, b라는 지역 변수도 있다. 하지만 f3 안에서 사용하고 있는 변수는 c, d이고 두 변수는 모두 f3에서 정의되었다. 자신이 생성될 때의 스코프가 알고 있는 변수 a, b는 사용하지 않았다. 그러므로 f3이 기억해야 할 변수는 하나도 없다. 자신이 스스로 정의한 c, df3이 실행되고 나면 없어진다. 다시 실행되면 c, d를 다시 생성하고 리턴 후에 변수는 사라진다. f3은 기억해 두는 환경도 변수도 없다. 그러므로 클로저가 아니다. f2에서 정의된 abf2에서만 쓰였을 뿐이다. f2 안에 f3이 있지만 f3에는 a, b가 없다. 이 점이 중요하다. ab는 기억될 필요가 없으므로 f2가 실행되고 나면 사라진다.
f3이 클로저가 아닌 것은 자바스크립트로 프로그래밍을 하는 데 있어서 너무나도 다행이고 당연한 일이다. 만일 f3이 클로저라면 거의 모든 함수가 클로저일 것이고, 가비지 컬렉터가 메모리를 해제할 수 있는 대상도 없을 것이다.
function f4() { var a = 10; var b = 20; function f5() { return a + b; } return f5(); } f4(); // 30
JavaScript
복사
위 코드에는 클로저가 있을까? 정확한 표현은 '있었다'가 되겠다. 결과적으로 클로저는 없다고 볼 수 있다.
f4가 실행되고 a, b가 할당된 후 f5가 정의된다. 그리고 f5에서는 a, b가 사용되고 있으므로 f5는 자신이 생성된 환경을 기억하는 클로저가 된다. 그런데 f4의 마지막 라인을 보면 f5를 실행하여 리턴한다. 결국 f5를 참조하고 있는 곳이 어디에도 없기 때문에 f5는 사라지고, f5가 사라지면 a, b도 사라질 수 있기에 클로저는 f4가 실행되는 사이에만 생겼다가 사라진다.
혹시 그동안 내부에 함수를 정의하여 외부 변수를 참조하는 것만으로 클로저가 될 것 같아 함수 내부에서 함수를 정의하는 것을 꺼렸다면, 그것만으로는 클로저가 되지 않으니 마음껏 사용해도 된다. 이런 부분들을 잘 숙지해 두면 더욱 편하게 프로그래밍을 할 수 있다.
function f6() { var a = 10; function f7(b) { return a + b; } return f7; } var f8 = f6(); f8(20); // 30 f8(10); // 20
JavaScript
복사
드디어 클로저를 만났다. f7은 진짜 클로저다. 이제 a는 사라지지 않는다. f7a를 사용하기에 a를 기억해야 하고 f7f8에 담겼기 때문에 클로저가 되었다. 원래대로라면 f6의 지역 변수는 모두 사라져야 하지만 f6 실행이 끝났어도 f7a를 기억하는 클로저가 되었기 때문에 a는 사라지지 않으며, f8을 실행할 때마다 새로운 변수인 b와 함께 사용되어 결과를 만든다. (여기서도 만약 f6의 실행 결과인 f7f8에 담지 않았다면 f7은 클로저가 되지 않는다.)
혹시 위 상황에 메모리 누수가 있다고 볼 수 있을까? 그렇지 않다. 메모리가 해제되지 않는 것과 메모리 누수는 다르다. 메모리 누수는 메모리가 해제되지 않을 때 일어나는 것은 맞지만, 위 상황을 메모리 누수라고 할 수는 없다. 위 상황은 개발자가 의도한 것이기 때문이다. a는 한 번 생겨날 뿐, 계속해서 생겨나거나 하지 않는다.
메모리 누수란 개발자가 '의도하지 않았는데' 메모리가 해제되지 않고 계속 남는 것을 말하며, 메모리 누수가 지속적으로 반복될 때는 치명적인 문제를 만든다. 계속해서 모르는 사이에 새어 나가야 누수라고 할 수 있다.
어쨌던 위 코드는 f8이 아무리 많이 실행되더라도 이미 할당된 a가 그대로 유지되기 때문에 메모리 누수는 일어나지 않는다. 이와 같은 패턴도 필요한 상황에 잘 선택하여 얼마든지 사용해도 된다.

클로저의 실용 사례

클로저를 가르쳐 주는 많은 예제를 보면 은닉으로 끝나는 경우가 많다. 클로저의 강력함이나 실용성은 사실 은닉에 있지 않다. 은닉은 의미 있는 기술이자 개념이지만 은닉 자체가 달성해야 하는 과제이거나 목적은 아니다. 사실 클로저가 정말로 강력하고 실용적인 상황은 따로 있다.
이전 상황을 나중에 일어날 상황과 이어 나갈 때
함수로 함수를 만들거나 부분 적용을 할 때
이 중에서 '이전 상황을 나중에 일어날 상황과 이어 나갈 때'란 다음을 의미한다.
이벤트 리스너로 함수를 넘기기 이전에 알 수 잇었던 상황들을 변수에 담아 클로저로 만들어 기억해 두면, 이벤트가 발생되어 클로저가 실행되었을 때 기억해 두었던 변수들로 이전 상황들을 이어갈 수 있다. 콜백 패턴에서도 마찬가지로 콜백으로 함수를 넘기기 이전 상황을 클로저로 만들어 기억해 두는 식으로 이전의 상황들을 이어갈 수 있다.
아래 예제는 jQuery와 Underscore.js가 있다고 가정했다.
JavaScript
복사

클로저를 많이 사용하라!

지금까지 클로저에 대해 알아보았다. 클로저는 어려운 듯하지만 사실 별로 어렵지 않으며 오히려 간단하다. 간단한 사용법을 가지고 있는 클로저는 자바스크립트에서 절차지향 프로그래밍, 객체지향 프로그래밍, 함수형 프로그래밍 모두를 지탱하는 매우 중요한 기능이자 개념이다. 서로 분리된 컨텍스트나 객체를 이토록 쉬운 개념으로 이어줄 수 있다는 것이 너무나 강력하고 아름답게 느껴진다.
분명 클로저는 메모리 누수 같은 위험성을 가지고 있다. 그러나 메모리 누수나 성능 저하의 문제는 클로저의 단점이나 문제가 아니다. 무턱대고 성능상 문제가 있으니 조심해야 한다는 말은 클로저를 적극적으로 사용하면 안 될 것처럼 생각하게 만든다. 필자는 오히려 클로저를 마음껏 쓰라고 추천하고 싶다. 조심하라는 표현보다는 정확하게 사용해야 한다는 표현이 더욱 적합하며, 사실 이것은 생각보다 어렵지 않다. 자꾸 사용하다 보면 그리 어렵지 않게 정확한 사용법을 알게 될 것이다. 그러고 나면 클로저와 관련된 이야기들이 문제로 느껴지지 않을 것이다. 오히려 메모리 누수가 일어나지 않는 로직 설계법을 더욱 깨우치게 되고, 클로저를 활용한 아름다운 패턴들도 자연스럽게 알게 될 것이다.
무조건 많이 사용하기를 권한다. 문제가 일어나면 그때 해결하면 된다. 그리고 사실 웬만해서는 문제를 일으키지 않을 것이다.

References

함수형 자바스크립트 프로그래밍 | 유인동 저 | 인사이트