😎 클로저... 그기.. 돈이 됩니까?
실은 이 글을 작성하게 된 이유 중 가장 큰 이유는
난 클로저에 대해 꽤 이해를 잘 하고 있다고 생각했지만
막상 누군가가
"클로저가 뭐야??" 라고 물어보면
😅😅😅엄... 잠시만.. 기다려봐... (내 블로그에서 클로저를 황급히 검색) 엄.. 그러니까..
클로저라는건 엄밀히 말하면 모든 함수는 클로저이긴 한데...
가 되어버리는 걸 몇번 겪어봤기 때문이다.
그래서 오늘은 누군가가 당신에게
"혹시.. 클로저가 뭡니까?" 라고 물어보는 아주 흔하고 일상적인 일이 발생했을때
당황하지 않고 쉽게 답변하는 사람이 되는 방법에 대하여 이야기를 해보겠다.
최대한 크로스체킹을 통해 틀린 내용 없이 작성하도록 노력은 하겠지만
솔직히 말해서 나의 자바스크립트 이해도는 추상적인 개념의 영역을 벗어나지 못하고 있는 상태다.
따라서 실제 동작과는 차이가 있을 수 있다.
원래는 챗지피티한테 물어보면서 하려고 했는데 이제 무료사용자는 번호표 뽑고 기다리라더라..
😎 그래서 클로저가 뭡니까? 클로저의 정의를 말해줘요
예... 엄밀히말하면 자바스크립트에서 모든 함수는 클로저입니다.
그런데 언어의 사회성이라는 게 존재하고 우리는 사회적으로 암묵적인 약속을 하지 않습니까?
1. 생명주기가 종료한 외부함수의 변수를 참조하면서
2. 외부함수보다 오래 생존하는 함수
위 두가지 조건을 모두 만족하는 함수를 자바스크립트에서는 "클로저"라고 부릅니다.
물론 MDN에서는
"A Closure is the combination of a function and the lexical environment within which that function was declared"
라고 정의하고 있습니다.
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
이게 무슨 소리냐 생각이 들지만 클로저를 자세히 살펴보고 난 뒤의 소감은
달라질 수도 있지 않을까요?
😎 클로저는 함수가 선언된 렉시컬 환경과의 조합이란게 뭔데요
먼저 렉시컬 환경이란 것을 짚고 넘어가야할 것입니다.
우선 알아야할것은 다음과 같습니다.
1. 자바스크립트에서 함수는 호출된 곳을 기준으로하는것이아닌 정의된 곳을 기준으로 상위스코프를 기억한다.
2. 함수가 상위스코프를 기억하는 방법은 [[environment]]내부 슬롯을 이용한 것이다.
3. 스코프체인은 Outer Lexical Environment Reference(외부 렉시컬 환경에 대한 참조)를 통해 구현된다.
위 내용들은 다 간단하게나마 짚고 넘어가도록 하겠습니다.
😎 렉시컬 환경이 뭡니까?
렉시컬 환경은 외부 렉시컬 환경에 대한 참조에 저장할 참조값입니다.
또한 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에
함수가 정의된 환경에 의해 결정되며 이것을 렉시컬 스코프라고 부릅니다.
어디에서 함수를 호출 하든 함수의 스코프는 정적(항상 정의된 장소)를 가리키기 때문입니다.
😎[[environment]] 내부 슬롯
함수는 자신의 내부 슬롯 [[environment]]를 가집니다.
이 [[environment]] 내부 슬롯에 저장되는 것은 자신이 정의된 환경입니다.
자신이 호출되었을 때 생성될 함수 렉시컬 환경의
"외부 렉시컬 환경에 대한 참조"에 저장될 참조값은 [[environment]] 내부 슬롯에 저장되어있습니다.
😎스코프 체인은 또 뭔데요??
함수의 상위 스코프는 렉시컬 환경의 외부 렉시컬 환경에 대한 참조를 따릅니다.
그리고 앞서 외부 렉시컬 환경에 대한 참조의 값은 [[environment]] 내부 슬롯과 관련이 있다하였습니다.
😎클로저의 기본적인 구성
클로저는 앞서 정의에서 살펴보았듯이
1. 생명주기가 종료한 외부함수의 변수를 참조하면서
2. 외부함수보다 오래 생존하는 함수
를 말한다고 했습니다.
뭐... 예.. 그러시군요..? 잘 알겠습니다.라는 생각이 들지만
저걸 어떻게 실제로 만드냐에 대한 논의는 빠져있는 개념적 정의에 가까운 정의라는 생각이 들지 않나요?
자바스크립트로 구현하는 클로저의 기본적인 구성은
모던 자바스크립트 딥다이브의 예제를 살펴보았을 때 다음과 같습니다.
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x); // 콘솔창엔 10이 찍힙니다.
};
return inner;
}
const innerFunc = outer();
innerFunc();
예.. 뭔소리냐고요? 코드를 한 줄씩 뜯어봅시다.
그런데!!그 이전에 일반적으로 자바스크립트에서 클로저를 구현하기 위해 필요한 조건은 다음과 같습니다.
1. return 문을 통해 함수를 반환하는 함수를 만든다.(편의상 이 함수를 반환하는 함수를 외부 함수라고 하겠습니다.)
2. return 문을 통해 반환되는 함수(편의상 중첩함수라고 하겠습니다.)는 외부 함수의 변수를 참조한다.
이점에 유의해서 inner 함수를 보면 inner 함수는 클로저가 되기 위한 조건을 모두 갖추고 있는걸 알 수 있습니다.
return문으로 함수를 반환하는가 ? -> 🐬YES! 우리가 외부함수에서 정의한 변수 inner는 함수입니다!
return문으로 반환된 함수는 외부 함수의 변수를 참조하는가? -> 🐬YES! 외부 함수의 변수 x를 참조합니다!
const x = 1;
// 전역환경에서 const를 통해 x = 1이라는 선언과 할당을 진행합니다.
function outer() {
// 클로저를 반환할 outer 함수를 만들어줬습니다.
const x = 10;
// 함수 내부에서 선언한 변수는 지역변수로 취급됩니다.
// 따라서 전역환경에서 이미 x를 선언했지만 함수 내부에서 또 x를 선언할 수 있습니다.
const inner = function () {
console.log(x);
};
// inner 함수를 선언해주었습니다. 선언 방식은 크게 중요하지 않습니다.
return inner;
// outer 함수는 inner 함수를 return 문을 통해 반환하고
// 모든 코드의 실행을 마친 outer 함수는 생명주기를 종료합니다.
// 따라서 outer 함수의 실행컨텍스트는 실행컨텍스트 스택에서 제거됩니다.
}
const innerFunc = outer();
// innerFunc이라는 변수를 선언하고 outer()함수의 호출 결과를 변수에 할당했습니다.
// outer()함수는 반환문으로 inner()함수를 반환합니다.
// 따라서 innerFunc 변수에는 inner() 함수가 담겨있습니다.
innerFunc();
// 전역에서 innerFunc() 함수를 호출하여 줍니다.
// 전역에서 innerFunc()을 호출했고 outer함수는 이미 소멸한 상태이니
// 1이 콘솔에 찍혀야할 것 같지만 실제로 콘솔에 찍히는 x의 값은 10입니다.
console.log(x) // 1
// 전역에서 찍어본 console.log(x)는 1입니다.
자 이제 우리는 우리가 만든 함수가 클로저라는 것을 잘 알고 있습니다.
근데 이 클로저가 대체 뭐가 대단하길래요..?
그 강력함은 인내심을 가지고 글을 읽어보시면 언젠가 나옵니다.
지금은 일단 이 클로저의 동작에 주목해서 보자구요!
뭔가 이상하지 않나요..?
전역 환경에서 innerFunc을 호출해서 콘솔에 찍히는 x의 값은 10인데
똑같이 전역 환경에서 사용한 console.log(x)의 값은 1이 반환됩니다.
innerFunc은 이미 생명주기가 종료된 outer() 함수의 지역변수 x를 참조하고있는거군요!
근데 이게 왜 신기한걸까요?
😎 자바스크립트의 가비지 컬렉터
https://ko.javascript.info/garbage-collection
https://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection
자바스크립트의 v8 엔진의 가비지 컬렉터에 대한 내용이 담긴 포스트입니다
자바스크립트에는 가비지 컬렉터라는 것이 존재합니다.
그리고 우리는 가비지 컬렉터가 뭔지는 모르지만 가비지 컬렉터는 항상 우리를 위해 일해주고있어요
이 가비지컬렉터는 변수 또는 데이터가 더이상 필요하지 않을 때 그것들을 버려주는 역할을 수행합니다.
우리가 가비지 컬렉터를 모르는 이유는 다음과 같습니다.
가비지 컬렉션의 매커니즘은 크게 두가지로 나눠볼 수 있기 때문인데
1. C++ , C로 대표되는 수동 정리 메커니즘을 채택한 프로그래밍 언어
2. 자바스크립트와 같은 자동 정리 메커니즘을 채택한 프로그래밍 언어
따라서 C 언어에서 찾아볼 수 있는 malloc과 같이
메모리를 직접 조작하는 함수를 자바스크립트에서는 찾아 볼 수 없는 것입니다.
왜 우리는 가비지 컬렉터가 필요할까요?
이미 제 역할을 수행하고 더이상 필요없어진 데이터들도 계속 메모리에서 공간을 차지하게 내버려둔다면
우리의 하드웨어는 한정된 공간을 가지고 있기에 언젠가는 더이상 빈 공간이 없어져
새로운 데이터를 넣을 수 없어질 수도 있고
성능이 저하될 가능성도 생길 것입니다.
그렇기 때문에 이제 더이상 필요없는 데이터들은 버려주어서 공간을 확보하는 행위가 필요한것이죠!
그리고 이것을 사람이 명시적으로 지정해줄 수도 있지만
자바스크립트는 좀 더 쉽게.. 알아서 처리를 해준다! 라고 생각을 하시면 될 것 같습니다.
근데 뭘 버려야할지를 어떻게 정하냐구요?
엄청나게 똑똑한 사람이 그때그때 상황에 맞춰서 버릴 걸 정하는게 더 성능이 좋지않냐구요?
맞습니다.. 그래서 자바스크립트의 가비지 컬렉터의 알고리즘은 정교하고 잘 만들어졌지만
모든 환경에서 최상의 성능을 보여준다고 하기엔 무리가 있습니다.
그래서 개발자들은 가비지콜렉터가 가비지콜렉팅을 제대로 못하는 경우를 알아두고
메모리누수를 주의하면서 코드를 작성하고있어요
While garbage collection may have some overhead and occasionally odd behavior
가비지 콜렉션은 약간의 오버헤드를 갖고 있을 수 있다.라고 하는군요
하지만 덕분에 코드를 짜는 우리는 복잡하고 지저분한 메모리 관리에 대한 세부사항을
신경쓰지 않아도 되는 편안한 환경을 얻었잖아요?
앞으로 가비지컬렉터님한테 하루 한번 감사인사 올리십시오
😎 그래서 가비지 컬렉터 얘기를 왜 해야하나요?
왜 가비지 컬렉터 얘기를 했냐면.. 클로저는 가비지 컬렉터와 아주 밀접한 연관이 있기 때문입니다.
위 사진은 자바스크립트 가비지 컬렉션의 콜렉팅 기준입니다.
중요한 대전제는 다음과 같습니다.
1. 도달 가능한 값은 메모리에서 삭제당하지 않는다.
2. 태생부터 도달 가능한 값에는 전역변수가 있다.
3. 중첩 함수의 체인에 있는 함수에서 "사용되는" 변수와 매개변수는 도달가능한 값이다.
4. 현재 함수의 지역 변수와 매개 변수
정리하자면 다음과 같습니다.
자바스크립트의 가비지 콜렉터는 실행이 끝난 함수의 지역변수와 매개변수를 콜렉션 대상으로 삼으며
함수의 실행이 끝나면 그 함수의 지역변수,매개변수를 가비지 콜렉터에 넣어 완전히 삭제한다는 것입니다.
가비지 컬렉터가 내부적으로 어떻게 동작하는지는 우선 차치해두고
러프하게 생각한다면 다음과 같이 동작한다고 말 할 수 있어요
1. 자동으로 실행된다. (가비지 컬렉터가 실행되는 걸 개발자가 막기는 힘들다.)
2. 지역 스코프를 떠난 후 해당 스코프의 변수가 외부 스코프에서 참조 되지 않으면 가비지 콜렉터는 그 변수를 제거한다.
😎 그래서 가비지 콜렉터 얘기는 언제까지... 하실건가요?
가비지 콜렉터는 기본적으로 사용되지 않거나 / 다 사용한 변수들을 버립니다.
근데 이 가비지 콜렉터도 내가 이 변수를 안쓰는게 아니라 쓰고있다는걸 보여주면
쓰고 있는 변수는 버리지 않는다는 거에요
hooh... 그렇구나 근데 내가 어떤 변수를 쓰고있는걸 어떻게 증명할수있지..?
놀랍게도 그 "내가 이 변수를 쓰고있어 버리지마!" 라는 암묵적 선언은
클로저를 통해 할 수 있다는 것입니다
3. 중첩 함수의 체인에 있는 함수에서 "사용되는" 변수와 매개변수는 도달가능한 값이다.
바로 이 부분이죠!
😎 자...이제..
슬슬 퍼즐이 맞아떨어져 가는게 느껴지시나요?
그런데 그 이전에 뭔가 하나가 삐걱 거리는 느낌이 드시지는 않나요?
우리는 콜스택을 살펴볼것입니다. 실행 컨텍스트 스택이라고도 부르지만
편의상 더 짧은 이름인 콜스택으로 이름을 통일하도록 하겠습니다.
실행컨텍스트는 굉장히 강력한 개념이고
그 분량도 굉장...하지만 지금은 필요한 부분만 추출해서 간단히 보도록 해요
실행 컨텍스트에 대해 궁금하신 분들은 아래에 제가 쓴 글을 링크하도록 하겠습니다.
https://xionwcfm.tistory.com/106
https://xionwcfm.tistory.com/107
😎 실행컨텍스트와 콜 스택
간단히 필요한 개념만 요약해보자면 콜스택에는 실행 컨텍스트가 들어갑니다. 그 순서는 다음과 같아요
그리고 이걸 가비지 콜렉터와 연관지어 생각하면
저 call stack에 들어갔다가 실행을 마치고 제거된 함수는 따로 그 함수의 지역변수를 참조하는 게 없는 이상
그 함수가 가지고 있는 지역변수는 근시일 내에 소멸한다는 소리입니다.
또한 저 실행컨텍스트에 들어갈 자격을 얻는건 대략 함수여야한다. 라고 생각하시면 되겠습니다.
전역 컨텍스트까지 실행을 마치고 pop되어서 콜스택이 완전히 비워지고
이벤트 루프에 의해 push될 함수도 없는 상태가 되면 비로소 자바스크립트는 모든 코드를 실행 완료했다 볼 수 있습니다.
🥵여기서 잠깐 콜스택에서 pop되는 기준은 뭘까요?
function x() {
console.log("전 x입니다.");
function y() {
console.log("전 y입니다.");
}
y();
}
x();
위 예제 코드를 보겠습니다. console에 출력을 해주는 일만 하는 아주 간단한 함수입니다.
1. 먼저 전역실행 컨텍스트가 콜스택에 push됩니다.
2. 함수x의 실행컨텍스트가 콜스택에 들어갑니다.
우린 전역 컨텍스트에서 함수들을 정의했고 x()를 호출해줬습니다.
함수의 호출이 발생하면 코드 실행을 멈추고 호출된 함수의 코드를 실행한다는 것에 유의하세요
함수 x가 호출되었으니 함수 x가 콜스택에 들어가고 함수 x의 코드들을 순차적으로 실행합니다.
엥? 그런데 함수 x는 함수 y를 정의하고 y()를 통해 호출하네요?
함수 x의 실행은 전역 실행 컨텍스트와 마찬가지로 y()호출이 끝난 후 종료되어야 합니다.
3. 함수 y의 함수 실행 컨텍스트가 콜스택에 들어갑니다.
그리고 함수 y는 자신의 코드인
console.log("전 y입니다.")를 실행한 뒤 자신의 모든 코드를 실행 완료하여
콜스택에서 제거됩니다.
이렇게 같은 원리로 쭉쭉 콜스택은 비워지게됩니다.
😎 근데 클로저는 뭔가...뭔가 좀 이상하지 않음?
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x); // 콘솔창엔 10이 찍힙니다.
};
return inner;
}
const innerFunc = outer();
innerFunc();
우린 위에서 콜스택이 어떻게 동작하는지에 대한 생각을 통해 대충 감을 잡았습니다.
콜스택에 들어간 컨텍스트들은 자신의 코드를 다 실행하고 난 뒤에 콜스택에서 제거된다!
네.. 근데 이 클로저 코드는 그걸 알고 보니까 뭔가 이상하잖아요
outer() 함수는 자기 코드의 실행을 마치고 결과를 반환한 뒤 콜스택에서 제거되었는데.
outer함수가 반환한 inner 함수는 이미 제거된 함수의 변수를 참조하고 있어요
자신이 참조할 스코프의 실행컨텍스트가 소멸했는데도
그 소멸한 실행컨텍스트의 변수를 참조하고 있다는 것입니다.
이것이 가능한 이유는 콜스택에서 어떤 함수의 실행컨텍스트가 제거되었다고해서
반드시 그 함수의 실행컨텍스트의 렉시컬 환경까지 소멸하는 것은 아니기 때문입니다.
이 outer()함수의 렉시컬 환경은 inner 함수의 [[environment]] 내부 슬롯에 의해 참조되고 있고
inner 함수는 전역 변수 innerFunc에 의해 참조되고 있기 때문에
가비지 콜렉션의 대상에서 벗어나게 되기 때문입니다.
우리는 앞서 클로저의 정의를 이렇게 봤습니다.
1. 생명주기가 종료한 외부함수의 변수를 참조하면서
2. 외부함수보다 오래 생존하는 함수
생명주기가 종료되었다는 건 콜스택에서 제거되었다는 것을 뜻하고
외부함수보다 오래 생존하는 함수라는 건
외부함수가 반환해준 함수이기 때문에 더 오래생존할 수 있다는 것이군요
이제와서 보면
"A Closure is the combination of a function and the lexical environment within
which that function was declared"
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
라는 말도 조금 다르게 보입니다.
자신이 정의된 위치의 렉시컬 환경을 참조하면서 그 환경의 변수를 써먹는 것이 클로저..
클로저는.. 함수가 선언된 렉시컬 환경과의 조합이다
😎근데 그래서 클로저가 뭐가 좋은건데 장점이 뭔데
클로저는 캡슐화를 구현하기에 매우 적합한 장치입니다.
클로저를 통해 억지로 살려놓은 변수는 외부에서는 조작할 수 없고
오로지 내부에서만 조작 가능하다는 특징이 있습니다.
이는 의도치않은 변경을 막아주고 의도한 변경만을 할 수 있는 환경을 가지게된다는 것을 뜻합니다.
자바스크립트에서는 객체지향 프로그래밍을 구현하는 키워드인
private, public등의 키워드가 존재하지 않지만
이 클로저를 통해 public과 private를 어느정도 비슷하게 구현해낼 수 있다는거죠
😎와 개쩐다 클로저 바로 쓸게요
근데 이런 클로저도 장점만 있는 건 아닙니다.
가비지 컬렉터의 콜렉션 대상이 되지 않는다는 의미는
불필요하게 사용된 클로저는 메모리 누수를 일으킬 가능성을 높인다는 문제점을 가지고 있습니다.
물론 적재적소에 잘 활용하는 클로저는 참 좋겠지만..
이런 위험부담도 있다는 점 기억해두시면 간지나요
'best' 카테고리의 다른 글
husky와 commitlint로 jira 이슈번호 자동화 시키기 (2) | 2023.11.27 |
---|---|
toss/slash의 use-funnel 훅 내부 구현 탐구하고 직접 구현하기(2) (0) | 2023.11.03 |
toss/slash의 use-funnel 훅 내부 구현 탐구하고 직접 구현하기(1) (1) | 2023.11.03 |
TypeScript , JavaScript의 접근 제한자 '#' Deep Dive (1) | 2023.08.29 |
자바스크립트의 호이스팅에 Deep Dive 해보자 (3) | 2023.04.11 |