1. 가비지 컬렉터(Garbage Collector)
프로그래밍을 하면, 경우에 따라 메모리를 청소를 해줘야 합니다. 그리고 어떤 프로그래밍 언어를 하냐에 따라 청소하는 법도 다릅니다. C 언어는 개발자가 직접 메모리를 관리하고 청소해야 합니다. 메모리 관리를 잘 하지 못하면 어플리케이션이 죽을 수도 있습니다.
C##, Go, Java, JavaScript, TypeScript는 메모리를 자동으로 관리해주기 때문에 다 쓴 메모리를 개발자가 직접 청소할 필요도 없습니다. 왜냐하면 Garbage Collector(가비지 컬렉터, 쓰레기 수집가, GC)가 있기 때문입니다.
가비지 컬렉터가 있다고 아무렇게나 코딩해도 되는 건 아니지만, 어떤 경우에 메모리가 그대로 남아있는지, 어떤 경우에 가비지 컬렉터에 의해 메모리가 정리되는지 이해하고 있어야, 메모리 효율적인 어플리케이션을 만들 수 있습니다.

오브젝트를 변수에 할당하면, 메모리의 Heap 영역에 생성됩니다. 변수는 메모리가 만들어진 주소를 가리키기만 합니다.
1let apple = {2name: 'apple',3}45let orange = apple6// 새로운 변수 orange에 apple을 할당하면, apple과 orange가 동일한 메모리 주소를 가르킴
만약 orange와 apple에 null을 할당하면, 이제 아무것도 오브젝트를 참조하고 있지 않습니다.
이렇게 누구도 오브젝트를 참조하고 있지 않을 떄, 가비지 컬렉터가 등장합니다.
Garbage Collector는 주기적으로 오브젝트를 참조하고 있는지, 없는지 확인한 다음에,
아무도 이 오브젝트를 참조하고 있지 않다면, 쓰레기로 간주하고 메모리에서 지워줍니다.
1// 글로벌 변수는 앱이 종료될때까지 계속 메모리에 유지됨!2const global = 13{4// 블럭 내부에서만 존재하고, 블럭이 끝나면 자동으로 소멸됨5// Garbage Collector가 청소!6const local = 17}89function print() {10// 함수 내부에 블럭 안에서도 변수가 필요한 경우에는11// 필요한 곳에서만! 블럭 안에서 변수를 선언하고 사용해야 함12if (true) {13let temp = 014}15}
💡 정리
- Garbage Collector는 JS 엔진에서 제공해주며, 백그라운드에서 동작
- 메모리를 주기적으로 감시하면서 아무도 참조하고 있는 메모리가 있다면 알아서 청소
- 다만, 가비지 컬렉터가 동작하는 데에도 CPU가 동작함
- 그래서 너무 자주 빈번하게 쓰레기가 수집되면, 청소하는데 리소스를 다 쓰면 안되기 때문에,
- Garbage Collector를 너무 믿지 말고, 불필요한 메모리를 할당, 재할당하는 일을 하지 말아야 함
- cf. MDN Memory management
2. 렉시컬 환경(Lexical Environtment)

함수들을 호출할 떄, 함수 순서를 기억하는 것은 Call Stack이 기억을 합니다.
실행 컨텍스트(Execution Context)는 Call Stack과 밀접한 관련이 있는데, 코드의 실행 순서와 스코프를 기억하기 때문입니다.
JS에서는 단 하나의 싱글 컨텍스트 스택이 존재합니다. 그래서 한 번에 하나의 일만 처리합니다.
💡
스코프(Scope, 범위 or 영역): 변수에 접근할 수 있는 유효한 범위
그래서 일한 이름의 a를 전역, 내부 블록에서도 선언해도 이름 충돌을 피할 수 있습니다. 현재 블록 안에 a가 있다면 쓰고, 없다면 근접한 상위 블록을 찾아보고, 없다면 다음 블록에서도 찾아봅니다.
1// 각각의 블록은 렉시컬 환경이라는 내부 오브젝트를 갖고있다.2const a = 1 // 전역 스코프3{4const a = 2 // 블럭 스코프 15{6const a = 3 // 블럭 스코프 27}8}
각각의 블록은 렉시컬 환경(Lexical Environtment)라는 내부 오브젝트를 갖고 있습니다.
그래서 각각의 블록마다 어떤 변수들이 들어있는지, 부모는 누구인지 등의 정보를 가지고 있습니다.
그래서 렉시컬 환경 안을 살펴보면, 2가지 종류가 있습니다.
환경 레코드(Environment Record): 현재 블록의 정보를 담고 있음외부 환경 참조(Outer Lexical Environment Reference): 어떤 부모를 참조하고 있는지의 정보를 담고 있음
1// 각각의 블록은 '렉시컬 환경'이라는 내부 오브젝트를 갖고있다.2const a = 1 // 전역 스코프3{4const a = 2 // 블럭 스코프 15{6const a = 3 // 블럭 스코프 27}8}
예를 들어, 실행 컨텍스트 스택에 전역 스코프를 만나면, 전역스코프 렉시컬 환경이 실행 컨텍스트 스택에 들어옵니다.
- 전역 스코프 렉시컬 환경
환경 레코드가a=1,외부 렉시컬 환경 참조는null을 가리키고 있습니다. 왜냐하면 최상의 부모이기 때문에
- 블럭1 스코프 렉시컬 환경
환경 레코드가a=2,외부 렉시컬 환경 참조는전역 렉시컬 환경을 가리키고 있습니다.- 이렇게 스코프들이 연결되어 있는 것을
스코프 체인(scope chain)이라고 부릅니다.
- 이렇게 스코프들이 연결되어 있는 것을
- 블럭2 스코프 렉시컬 환경
환경 레코드가a=3,외부 렉시컬 환경 참조는블럭1 렉시컬 환경을 가리키고 있습니다.
여기서 블록이 모두 끝나게 되면, 실행 컨텍스트 스택에 위에서 부터 1개씩 빠지게 됩니다. 전역이 끝나면 스택이 텅텅 비게 됩니다. 그런데 현재 블록에 변수가 없는데, 접근을 하게 되면, 바로 상위에 있는 스코프 체인을 통해 부모 렉시컬 환경의 환경 레코드를 참조하게 됩니다.
렉시컬 환경을 통해 배울 수 점은 메모리 절약 뿐만 아니라, 성능을 위해서라도 변수는 최대한 필요한 곳에서 정의해야 된다는 점입니다. 이렇게 중첩된 여러 함수나 스코프가 있다면, 필요한 곳에서 선언하지 않으면, 계속 스코프 체인을 따라가면서 환경 레코드가 있는지 없는지 검사를 하니 성능에도 좋지 않습니다.
3. 클로져(Clusure)
1let cnt = 023function cntPlus() {4cnt = cnt + 15}
안에 어떤 변수 cnt 가 있는데 cnt라는 값을 무조건 cntcntPlus() 로만 바꾸고 싶은 경우를 구현하고 싶다 가정해봅시다.
- 코드를 짜면 위와 같이 구현할 수 있습니다.
- 변수 하나를 선언하고 let으로 그리고 그 cnt 값을 바꿀 수 있는 함수를 만듭니다.
1let cnt = 023function cntPlus() {4cnt = cnt + 15}67console.log(cnt) // 08cntPlus()9console.log(cnt) // 1
이렇게 만들면 cnt라는 변수값을 cntPlus()를 실행시켰을때 값이 바뀌게 되겠죠.
그렇지만 제가 말한 기능을 구현을 했는데 완벽하지가 않습니다.
1let cnt = 023function cntPlus() {4cnt = cnt + 15}67console.log(cnt) // 08cntPlus()9console.log(cnt) // 11011// 1억개의 코드12cnt = 10013// 1억개의 코드14cntPlus()15console.log(cnt) // 101
만약에 위 코드처럼 중간에 1억개의 코드가 있다고 가정을 해봅시다.
- 우리가 가정한 상황은 이
cnt라는 변수는 무조건cntPlus()로만 값을 증가시킬 수 있고, - 다른 걸로는 바꾸면 안되는 그런 상황인데,
- 이 중간에
cnt라는 변수가 접근 가능하기 때문에 우리가 생각하는 상황을 완벽하게 구현할 수가 없습니다
그러면 원래 목표인 cntPlus()로만 값을 수정하게 구현하려면 어떻게 하면 될까요?
- 이 변수를 접근 못하게 하면 되겠죠. ⭐️
- 그때 필요한게
클로저입니다.
1function closure() {2let cnt = 03function cntPlus() {4cnt = cnt + 15}6}78console.log(cnt) // 09cntPlus()10console.log(cnt) // 11112// 1억개의 코드13cnt = 10014// 1억개의 코드15cntPlus()16console.log(cnt) // 101
현재 cnt는 전역 변수이기 때문에 어디서든 접근할 수 있습니다. cnt를 접근할 수 없게 하기 위해서는 지역변수로 만들어줄겁니다.
- 이제
cnt값을 참조하려고 해도, 이cnt는 이 클로저 함수의 지역변수이기 때문에 밖에서 참조를 할 수가 없게 됩니다. - 그런 대신 지금이
cntPlus()를 실행시킬 수도 없게 되버렸죠. cntPlus()는 클로저 함수의 지역변수에 있는 함수이기 때문에 다른 곳에서 참조할 수가 없습니다.
1function closure() {2let cnt = 03function cntPlus() {4cnt = cnt + 15}6return {7cntPlus,8}9}1011console.log(cnt) // 012cntPlus()13console.log(cnt) // 11415// 1억개의 코드16cnt = 10017// 1억개의 코드18cntPlus()19console.log(cnt) // 101
그럼 이떄 cntPlus() 사용하기 위해서 해주는 방식이 cntPlus()를 리턴해 주는 겁니다. 이렇게 하고 한번 사용을 해보겠습니다.
1function closure() {2let cnt = 03function cntPlus() {4cnt = cnt + 15}6return {7cntPlus,8}9}1011const cntCloure = closure()12console.log(cntCloure) // { cntPlus: [Function: cntPlus] }
이렇게 실행하게 되면, 이 cntClosure에는 cntPlus라는 함수가 담겨있는 객체가 담기겠죠.
1function closure() {2let cnt = 03function cntPlus() {4cnt = cnt + 15}6function printCnt() {7console.log(cnt)8}9return {10cntPlus,11printCnt,12}13}1415const cntCloure = closure()16console.log(cntCloure) // { cntPlus: [Function: cntPlus] }17cntCloure.printCnt() // 018cntCloure.cntPlus()19cntCloure.printCnt() // 1
그러면 cntPlus()를 실행시키면 cnt 값이 1 증가할 겁니다.
1function closure() {2let cnt = 03function cntPlus() {4cnt = cnt + 15}6function printCnt() {7console.log(cnt)8}9return {10cntPlus,11printCnt,12}13}1415const cntCloure = closure()16console.log(cntCloure) // { cntPlus: [Function: cntPlus] }17cntCloure.printCnt() // 018cntCloure.cntPlus()19cntCloure.printCnt() // 12021// 1억개의 코드22cnt = 10023// 1억개의 코드
그러면 아까 했던 것 중에 이렇게 1억개의 코드가 있는데, 중간에 cnt=100이나 기타 다른 방법으로 cnt를 바꿀 방법이 있을까요?
절대 없습니다. 이 cnt 변수에 접근하는 방법은 2개의 함수(cntPlus, printCnt)를 통한 것 말고는 없기 때문입니다.
이런식으로 아까 말했던 기능을 구현할 수 있습니다. 그리고 추가적인 기능이 필요할 때는 역시 함수를 추가하는 방식으로 원하는 기능을 구현할 수 있습니다.
3.1 정의
A clusure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). in other words, a closure gives you access to an outer function’s scope from an inner function. - MDN Closures
클로져는 함수와 그 외부를 둘러싸고 있는 렉시컬 환경의 조합이다. 다른 말로, 클로져란 내부 함수에서 외부 함수에 있는 상태에 접근할 수 있는 권한을 주는 것을 말한다.
cf. 클로져(clusure) = 폐쇄, 닫혀있는
cf. MDN Closures
클로져 = 중첩된 함수에서 내부에 있는 함수가 외부에 있는 렉시컬 환경에 접근할 수 있는 것
1// outer 스코프-----------------------2function outer() {3const x = 04// inner 스코프---------------------5function inner() {6x7}8inner()9// ---------------------------------10}11outer()
- 코드를 실행하자마자 함수 선언문이라 인식해, 전역 스코프 렉시컬 환경이 만들어짐
- 바로
outer();로 넘어가outer()를 호출하는 순간 outer 스코프 렉시컬 환경이 만들어짐
- 바로
- 그 안에
inner()라는 함수를 만나 inner 스코프 렉시컬 환경을 만듭니다. - inner가 호출이 되고 끝나면 스택에서 나가고, 다시 outer 렉시컬 환경으로 넘어감
- 또 outer 함수가 끝나면 스택에서 나가고, 전역 렉시컬 환경으로 넘어감
- 마지막으로 어플리케이션이 종료가 되면 실행 컨텍스트 스택이 텅텅 빔
1// outer 스코프-----------------------2function outer() {3const x = 04// inner 스코프---------------------5function inner() {6x7}8return inner9// ---------------------------------10}11const inner = outer()12inner()
- 코드를 실행하자마자 함수 선언문이라 인식해, 전역 스코프 렉시컬 환경이 만들어짐
- 바로
outer()가 호출되어 inner라는 변수에 담겨지면서 outer 렉시컬 환경이 만들어짐 - 호출된
outer()내부에서 inner()를 선언해서 inner 렉시컬 환경이 만들어짐- inner는 외부에 있는 outer를 참조하게 됩니다.
- 여기서는 inner를 호출하지 않고, return으로 반환해줍니다.
- outer() 함수가 끝나면, outer 스코프 렉시컬 환경이 스택에서 빠지지만 스코프 체인에는 그대로 남아있습니다
inner()를 호출하게 되면 inner 렉시컬 환경에는 x라는 값이 없지만,- 외부에 outer 렉시컬 환경에는 x값이 존재하기에 이 값에 접근 가능
3.2 예제
1// 📝 내부에서 외부에서 접근은 가능2// 📝 내부에서 선언된 변수는 외부에서 접근 불가능3const text = 'hello'4function func() {5console.log(text)6}7func() // hello89// 📝 클로져 : 중첩된 함수에서 내부에 있는 함수가 외부에 있는 렉시컬 환경에 접근할 수 있는 것10// 내부 함수와 외부 함수가 함께 닫히는 느낌11function outer() {12const x = 013function inner() {14console.log(`inside inner: ${x}`) // 외부에 있는 함수에 접근 가능15}16return inner17}18// 클로져에 의해서 inner()가 리턴이 될 때,19// inner()와 함수 외부에 변수가 들어있는 렉시컬 환경도 함께 묶여서 클로져로 반환20const func1 = outer()21func1() // inside inner: 0
3.3 예제2
1// 📝 클로져를 사용하는 이유?2// 내부 정보를 은닉하고, 공개 함수(public, 외부)를 통한 데이터 조작을 위해3// 즉, 캡슐화와 정보은닉을 위해서4// 클래스 private 필드 또는 메소드를 사용하는 효과와 동일!5// 현재는 클래스 private 필드를 사용하면 되지만, 예전 JS버전에서는 클로져를 활용했었음6function makeCounter() {7let count = 08function increase() {9count++10console.log(count)11}12return increase13}14const increase = makeCounter()15increase() // 116increase() // 217increase() // 31819// 📝 Class 문법이 생겼으므로, 이제는 클로져를 사용할 필요가 없습니다.20class Counter {21##count = 022increase() {23this.##count++24console.log(this.##count)25}26}27const counter = new Counter()28counter.increase() // 1
Class 문법이 생겼으므로, 이제는 클로져를 사용할 필요가 없습니다.