클로저 전에 알아야 할 내용
🔥 콜 스택(Call Stack)이 뭐냐?
- 자바스크립트 엔진이 함수 실행을 관리하는 공간
- LIFO(Last In, First Out) 구조 → 마지막에 들어온 함수가 먼저 실행 완료됨
- 함수가 호출될 때 스택에 쌓이고, 실행이 끝나면 스택에서 제거됨
⚙️ 콜 스택 동작 방식
- 함수 호출 → 콜 스택에 push(쌓임)
- 함수 실행 완료 → 콜 스택에서 pop(제거)
- 비동기 함수 (setTimeout, fetch 등) → 콜 스택에서 빠지고 Web APIs → Task Queue → Event Loop 통해 실행
Heap 영역이 뭐냐?
- 런타임 중에 동적으로 메모리 할당하는 공간
- 크기가 가변적이라 필요할 때 메모리를 할당하고, 필요 없으면 해제됨
- JS에서 객체(Object)나 함수(Function) 같은 데이터가 저장됨
- 스택(Stack)보다 크지만 속도는 상대적으로 느림
⚙️ Heap & Garbage Collection (GC)
- JS는 자동 메모리 관리 (Garbage Collector) 가 해줌
- 가비지 컬렉션(GC) → 필요 없는 메모리 정리하는 기능
- Mark & Sweep 알고리즘 사용
- Reachable(도달 가능한) 객체 찾음 (ex. 글로벌 변수, 함수 실행 중인 객체)
- 도달 불가능한 객체는 제거해서 메모리 해제
📝 콜 스택(Call Stack) vs 힙(Heap) vs 가비지 컬렉션(GC) 비교 표
콜 스택(Call Stack) 힙(Heap) 가비지 컬렉션(GC)
역할 | 함수 실행 순서 관리 | 동적 메모리 저장소 (객체, 배열, 함수 등) | 필요 없는 메모리 자동 정리 |
구조 | LIFO (Last In, First Out) | 비정형 구조 (메모리 주소 기반) | Mark & Sweep 알고리즘 사용 |
저장 데이터 | 원시값 (숫자, 문자열), 함수 호출 정보 | 객체, 배열, 함수 등 동적 데이터 | 도달 불가능한(사용되지 않는) 데이터 |
속도 | 빠름 (고정 크기, 단순한 구조) | 상대적으로 느림 (가변 크기, 탐색 필요) | 자동 실행되지만 성능 부담 가능 |
메모리 해제 | 함수 실행 종료 시 자동 해제 | GC가 직접 관리 | 주기적으로 실행되며, 메모리 누수 방지 |
오류 가능성 | Stack Overflow (무한 재귀 등) | 메모리 누수 가능 (참조 남아 있으면 GC 해제 X) | 너무 자주 실행되면 성능 저하 가능 |
예제 | js function a() { b(); } function b() { c(); } function c() {} a(); | js let obj = { name: "나" }; | js let obj = { name: "나" }; obj = null; // GC가 정리! |
🎯 한 줄 요약
- 콜 스택 → 함수 실행을 관리 (LIFO, 빠름, 스택 오버플로우 주의)
- 힙 → 동적 메모리 저장 공간 (객체, 배열 저장, 느림, GC 필요)
- 가비지 컬렉션(GC) → 필요 없는 메모리 자동 정리 (Mark & Sweep 방식)
3.2 클로저(Closure)
함수가 생성될 때 , 외부 변수(환경)을 기억하고 계속 접근할 수 있는 기능
거의 모든 JS 개발자는 클로저를 이용하고 있음
사실 클로저는 JS에만 있는 기능이 아니라 주요 프로그래밍 언어에서 사용함
📌 즉, 함수가 생성된 시점의 변수 값을 나중에도 기억하고 사용할 수 있음!
📌 JavaScript에서 함수는 "어디에서 선언되었는가"를 기억함
이 spirit 을 참조하는 함수가 있어서 spirit은 삭제되지 않고 Heap으로 옮겨짐.
Heap에서는 가비지 컬렉터가 참조되지 않는 데이터를 수거하는데 ,
coco 변수에 저장된 이 함수가 프로그램이 돌아가는 동안은 사라지지 않는다.
**function outerFunction() {
let outerVariable = "나는 외부 변수!";
function innerFunction() {
console.log(outerVariable); // 바깥 함수의 변수를 기억하고 있음!
}
return innerFunction; // 내부 함수 반환
}
const closureFunc = outerFunction(); // outerFunction 실행 후 innerFunction 반환
closureFunc(); // "나는 외부 변수!"**
- outerFunction() 실행이 끝났지만, interFunction() 이 outerVariable 을 기억하고 있음
- 함수가 생성될 당시의 환경을 저장하는 것
✅ 3️⃣ 클로저의 핵심 특징
특징 설명
1. 내부 함수가 외부 함수의 변수를 기억함 | innerFunction()이 outerVariable을 계속 사용할 수 있음 |
2. 외부 함수 실행이 끝나도 변수를 유지 | outerFunction()이 종료된 후에도 outerVariable이 살아있음 |
3. 데이터를 은닉하고 보호 가능 | 클로저를 사용하면 특정 데이터에 대한 접근을 제한 가능 |
✅ 4️⃣ 클로저의 활용 예제
🔹 1. 데이터 은닉 (Private 변수)
function createCounter() {
let count = 0; // 외부에서는 직접 접근 불가
return {
increment: function () {
count++;
console.log(count);
},
decrement: function () {
count--;
console.log(count);
},
getCount: function () {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined (직접 접근 불가!)
✔ count 변수는 직접 접근 불가능하지만, 내부 함수로 조작 가능
✔ 데이터 은닉(캡슐화) 효과를 가짐
🔹 2. 특정 값 유지 (클로저로 상태 저장)
function createAdder(value) {
return function (num) {
return num + value; // 클로저를 이용해 `value`를 기억
};
}
const addFive = createAdder(5);
console.log(addFive(10)); // 15
console.log(addFive(20)); // 25
✔ value 값이 클로저 내부에 저장되어 유지됨!
✔ 새로운 createAdder(10)을 만들면 다른 값도 유지 가능
🔹 3. 이벤트 핸들러에서 클로저 사용
function attachEventListeners() {
let count = 0;
document.querySelector("#button").addEventListener("click", function () {
count++;
console.log(`버튼 클릭 횟수: ${count}`);
});
}
attachEventListeners();
✔ 클로저를 이용해 count 값이 이벤트 발생할 때마다 증가하도록 유지됨
🔹 4. setTimeout에서 클로저 활용
function delayedMessage(msg, delay) {
setTimeout(function () {
console.log(msg);
}, delay);
}
delayedMessage("Hello, 클로저!", 2000); // 2초 후에 "Hello, 클로저!" 출력
✔ 클로저를 사용하면 setTimeout() 내부에서 msg 값을 계속 유지 가능
✅ 5️⃣ 클로저의 메모리 관리 이슈
클로저는 외부 함수의 변수를 계속 참조하기 때문에 메모리가 해제되지 않을 수 있음!
→ 너무 많은 클로저를 남발하면 메모리 누수(Memory Leak)가 발생할 수도 있음
🔹 클로저의 메모리 문제 해결법
✔ 클로저가 더 이상 필요 없을 때 변수를 null로 설정
✔ 너무 많은 클로저를 사용하지 않도록 설계 주의
function createCounter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter = null; // 클로저 참조 제거 (메모리 해제 가능)
✅ 6️⃣ 클로저 vs 일반 함수 비교
구분 일반 함수 클로저
변수 접근 | 함수 내부 변수만 접근 가능 | 외부 함수의 변수도 기억함 |
데이터 유지 | 함수 실행이 끝나면 변수 사라짐 | 변수를 계속 유지할 수 있음 |
활용 예시 | 일반적인 연산, DOM 조작 | 데이터 은닉, 상태 저장, 이벤트 핸들러, 비동기 처리 |
✅ 7️⃣ 클로저 정리
✔ 클로저는 "함수가 선언될 때의 환경을 기억하고, 그 환경에 접근할 수 있는 함수"
✔ 외부 함수의 변수를 내부 함수가 계속 유지할 수 있음
✔ 데이터 은닉, 상태 저장, 이벤트 핸들러, 비동기 처리 등에 활용 가능
✔ 메모리 누수 문제를 주의해야 함
클로저는 이런 언어에서 사용 가능하다.
- 함수가 일급객체이고 중첩 함수 허용
- lexical scoping 지원
this 알기전에 실행컨텍스트, 콜스택 차이 먼저 볼게요.
🏗️ 1. 실행 컨텍스트 (Execution Context)
➡️ "함수가 실행될 때 생성되는 환경(메모리 공간)"
➡️ 함수가 실행되면 변수, 함수 선언, this 값, 스코프 정보를 저장하는 공간이 생성됨.
실행 컨텍스트가 포함하는 것
- 변수 객체(Variable Object, VO) → 함수 내 변수, 함수 선언 저장
- 스코프 체인(Scope Chain) → 함수 내부에서 외부 변수에 접근할 수 있도록 관리
- this 바인딩 → this 값을 결정
제일 위에 함수가 실행 → 종료되면 제일 위에 실행 컨텍스트가 사라짐
(LIFO 구조)
📌 실행 컨텍스트 예제
function outer() {
let a = 1;
function inner() {
let b = 2;
console.log(a + b);
}
inner();
}
outer();
실행 과정
- 전역 실행 컨텍스트 생성 (outer 함수 저장)
- outer() 실행 → 실행 컨텍스트 생성
- inner() 실행 → 실행 컨텍스트 생성 (외부 outer 스코프 포함)
- inner() 실행 종료 → 실행 컨텍스트 삭제
- outer() 실행 종료 → 실행 컨텍스트 삭제
- 전역 실행 컨텍스트 남음
📚 2. 콜 스택(Call Stack) (LIFO Last In , First Out)
➡️ "실행 컨텍스트를 저장하는 Stack 자료구조"
➡️ **"어떤 함수가 실행 중인지, 어떤 함수가 끝났는지"**를 관리
📌 콜 스택 동작 예제
function first() {
second();
console.log("첫 번째 함수 실행");
}
function second() {
third();
console.log("두 번째 함수 실행");
}
function third() {
console.log("세 번째 함수 실행");
}
first();
콜 스택 변화 과정
스택 순서 실행 중인 함수
1️⃣ | first() 호출 → 스택에 추가 |
2️⃣ | second() 호출 → 스택에 추가 |
3️⃣ | third() 호출 → 스택에 추가 |
4️⃣ | third() 실행 완료 → 스택에서 제거 |
5️⃣ | second() 실행 완료 → 스택에서 제거 |
6️⃣ | first() 실행 완료 → 스택에서 제거 |
✅ | 모든 함수 실행 종료 → 스택이 비어 있음 |
🎯 🚀 실행 컨텍스트 vs 콜 스택 차이 정리
실행 컨텍스트(Execution Context) 콜 스택(Call Stack)
역할 | 함수가 실행될 때 변수, this, 스코프 정보를 저장 | 실행 중인 함수를 관리 |
형태 | 메모리 공간 (환경) | Stack 자료구조 |
관계 | 하나의 실행 컨텍스트가 하나의 함수 실행 환경을 담당 | 실행 컨텍스트를 스택에 push/pop |
종료 시점 | 함수 실행이 끝나면 삭제됨 | 스택에서 함수가 pop될 때 함께 사라짐 |
예제 | 변수, 함수 선언, this 값 저장 | 함수 실행 순서 관리 |
✅ 결론
- 실행 컨텍스트 → 함수 실행을 위한 환경(메모리 공간)
- 콜 스택 → 실행 컨텍스트를 관리하는 Stack 자료구조
- 함수 실행 시 → 콜 스택에 실행 컨텍스트가 push
- 함수 종료 시 → 콜 스택에서 pop되며 실행 컨텍스트 제거
3.3 this 키워드
this는 어떤 객체를 가르키는 키워드다.
근데 그 “어떤”이 뭔지 정의를 내릴 수 없다.
“어떤 객체”는 상황에 따라 다르기 때문이다.
왜 this가 호출 방식에 따라 달라질까?
자바스크립트는 "함수가 어디서 선언되었냐"가 아니라, "어디서 호출되었냐" 를 기준으로 this 값을 결정함.
→ 즉, "실행 컨텍스트" 가 this를 결정하는 핵심 요소임!
📌 실행 컨텍스트(Execution Context)란? ⇒ 함수가 실행되는 환경이다.
- 함수 호출 방식에 따라 this 값이 달라짐
- 객체 메서드 → 해당 객체가 this
- 화살표 함수 → 상위 스코프의 this를 가져옴
- call, apply, bind로 this 강제 설정 가능
function이라고 정의하면 이 함수는 window 객체에 등록이 된다.
1. this는 어디에 저장되길래 호출 방식마다 다를까?
➡️ 실행 컨텍스트가 생성될 때 this 값이 저장(바인딩)됨.
➡️ "함수가 호출될 때" 실행 컨텍스트가 만들어지고, 그 순간 this가 결정됨.
➡️ this는 실행 컨텍스트의 내부 환경(VO, Lexical Environment) 안에 저장됨.
📌 실행 컨텍스트 구조를 보면 이렇게 생겼음:
실행 컨텍스트 = {
VariableEnvironment: { ... },
LexicalEnvironment: { this: ??? }, <-- 여기에 this가 저장됨
ThisBinding: ??? <-- this를 참조하는 공간
}
- 실행 컨텍스트가 생성될 때 Lexical Environment 안의 this 값이 정해짐.
- 함수가 실행될 때, 실행 컨텍스트의 this 값을 참조함.
📌 즉, this는 함수가 실행될 때 실행 컨텍스트 내부에서 저장되기 때문에 호출 방식마다 달라지는 것!
2. "실행 컨텍스트가 생성될 때 자동으로 this가 바인딩됨"
✔️ "자동 바인딩"이란 실행 컨텍스트가 만들어질 때 this 값이 결정(등록)된다는 의미.
✔️ 즉, 어떤 객체가 this가 될지 실행 컨텍스트가 자동으로 결정해줌.
✔️ 이 결정 과정에서 "어떻게 함수가 호출되었는지"가 중요한 역할을 함.
3. "함수가 어떻게 호출되었는지"에 따라 this가 달라지는 이유?
➡️ 함수 실행 시 실행 컨텍스트가 만들어지고, "함수를 호출한 방식"에 따라 this를 어디로 바인딩할지 결정함.
➡️ 즉, "누가 이 함수를 호출했느냐"가 this를 결정함!
📌 🚀 this가 결정되는 원리 (호출 방식마다 다른 이유)
호출 방식 this가 가리키는 값 이유
전역 실행 (window) | window (브라우저) | 전역 컨텍스트에서 실행되면 this = window |
일반 함수 호출 | window (엄격 모드 undefined) | 실행 컨텍스트 생성 시 기본적으로 전역 객체 바인딩 |
객체 메서드 호출 | 해당 객체 | . 앞에 있는 객체가 this로 설정됨 |
생성자 함수 호출 (new 사용) | 새로 생성된 객체 | new 키워드를 사용하면 새로운 객체가 this가 됨 |
화살표 함수 | 상위 스코프의 this | 실행 컨텍스트 생성 시 this를 새로 바인딩하지 않음 (Lexical this) |
call / apply / bind 사용 | 지정한 객체 | this를 직접 바꿀 수 있음 |
이벤트 핸들러 | 이벤트가 발생한 요소 | 이벤트가 실행된 대상이 this가 됨 |
💡 🚀 this가 결정되는 진짜 핵심 원리
- 자바스크립트는 함수를 실행할 때 실행 컨텍스트를 생성하면서 this를 바인딩함.
- 실행 컨텍스트가 만들어질 때 어떤 방식으로 호출되었는지를 확인하고, 그에 맞는 this를 저장함.
- 즉, "함수 호출 방식"이 곧 실행 컨텍스트의 this 바인딩 방식을 결정하는 원인!
- 화살표 함수는 실행 컨텍스트 생성 시 this를 새로 바인딩하지 않음 → 상위 스코프 this를 그대로 사용!
// 대충이름.tsx
// this => window로
// this => 화살표 함수
// function 선언
// 함수 표현식 => const 함수선언 = function(){}
// 함수표현식 (설탕친게 화살표) const 함수설탕 = () => { }
// this를 사용할 때는 함수 표현식은 화살표함수랑 달라요.
// 화살표 =>this 대충이름 , 함수표현식 => window
export function 대충이름(){
const arrow = () => {
this.dddd
return 어쩌구
}
}
**그래서 화살표 함수는 this를 의도적으로 컨트롤하기에 최적이다.
(함수표현식은 안됨)**
➡️ 화살표 함수는 실행 컨텍스트에서 this를 새로 바인딩하지 않음
➡️ 즉, 화살표 함수 내부에서 this를 사용하면 상위 스코프의 this를 그대로 참조함.
➡️ this가 바뀌는 걸 방지할 때 유용함!
🔥 왜 화살표 함수가 this를 다루기 좋을까?
- 일반 함수는 호출 방식에 따라 this가 달라져서 실수할 가능성이 높음.
- 화살표 함수는 this를 상위 컨텍스트에서 가져오기 때문에 예측 가능하고 안전함!
- 특히 콜백 함수나 이벤트 핸들러에서 this가 변하는 걸 막을 때 좋음.
📌 일반 함수 vs 화살표 함수의 this 차이
const obj = {
name: "JavaScript",
normalFunc: function () {
console.log("일반 함수 this:", this); // obj
},
arrowFunc: () => {
console.log("화살표 함수 this:", this); // window (obj가 아님!)
}
};
obj.normalFunc(); // this = obj
obj.arrowFunc(); // this = window (객체가 아닌 상위 스코프 참조!)
📌 일반 함수는 obj를 가리키지만, 화살표 함수는 window를 가리킴!
📌 화살표 함수는 this를 새로 바인딩하지 않기 때문에 obj를 가리키지 않음.
🎯 화살표 함수가 유용한 경우
1️⃣ setTimeout에서 this 유지
const obj = {
name: "JavaScript",
sayLater: function () {
setTimeout(() => {
console.log("화살표 함수 this:", this.name); // this가 obj를 유지!
}, 1000);
}
};
obj.sayLater();
✅ 화살표 함수 사용 → this가 obj를 유지!
❌ 일반 함수였다면 this가 window가 됨.
2️⃣ 이벤트 핸들러에서 this 고정
const obj = {
name: "JavaScript",
handleEvent: function () {
document.getElementById("btn").addEventListener("click", () => {
console.log("화살표 함수 this:", this.name); // this가 obj를 유지!
});
}
};
obj.handleEvent();
✅ 화살표 함수 사용 → this가 obj를 유지!
❌ 일반 함수였다면 this가 클릭한 버튼 요소가 됨.
🚀 결론
✔️ 화살표 함수는 this를 새로 바인딩하지 않음 → 상위 스코프의 this를 유지
✔️ setTimeout, 이벤트 핸들러, 콜백 함수에서 this가 바뀌는 문제를 방지할 때 유용
✔️ 일반 함수는 this가 호출 방식에 따라 달라지지만, 화살표 함수는 예측 가능함
this를 의도적으로 유지하고 싶다면 화살표 함수가 최고다.
3.4 프로토 타입
🔥 자바스크립트 프로토타입(Prototype) 개념 정리
➡️ 자바스크립트는 프로토타입 기반(prototype-based) 언어
➡️ 클래스가 아니라 프로토타입을 통해 객체가 상속을 받음
➡️ 프로토타입 체인(prototype chain)을 이용해 속성과 메서드를 찾음
1️⃣ 프로토타입(Prototype)이란?
- 부모자식지간 객체다. 이말임
- 모든 객체는 prototype이라는 숨겨진 링크(참조)를 가짐
- 이 prototype이 다른 객체(부모 객체)로부터 속성과 메서드를 상속받음
- 자바스크립트의 상속 구조는 프로토타입을 통해 구현됨
📌 프로토타입 예제
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hello, my name is ${this.name}`);
};
const user1 = new Person("Minho");
user1.sayHello(); // "Hello, my name is Minho"
✅ Person 생성자의 prototype에 sayHello를 추가하면 모든 Person 객체가 이를 상속받음.
2️⃣ 프로토타입 체인(Prototype Chain)이란?
➡️ 객체에서 프로퍼티나 메서드를 찾을 때 해당 객체에 없으면 프로토타입을 따라 올라가면서 찾음
➡️ 최종적으로 Object.prototype까지 올라가고, 그래도 없으면 undefined 반환
📌 프로토타입 체인 예제
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(`${this.name}가 소리를 냅니다.`);
};
const dog = new Animal("멍멍이");
dog.speak(); // "멍멍이가 소리를 냅니다."
console.log(dog.toString()); // `toString`은 Object.prototype에 정의됨
✅ dog.speak()를 호출할 때 dog 객체 → Animal.prototype → Object.prototype 순서로 찾아감.
✅ dog.toString()을 호출할 때도 자신에게 없으면 프로토타입 체인을 따라 Object.prototype에서 찾음
3️⃣ Object.create()란? (프로토타입을 직접 설정하는 방법)
➡️ Object.create(proto)를 사용하면 직접 프로토타입을 설정하여 객체를 생성할 수 있음
📌 Object.create() 예제
const parent = {
greet() {
console.log("안녕하세요!");
}
};
const child = Object.create(parent); // parent를 프로토타입으로 하는 객체 생성
child.greet(); // "안녕하세요!" (부모 객체에서 상속받음)
✅ Object.create(parent)를 사용하면 child 객체가 parent를 프로토타입으로 가짐
✅ 즉, child에는 greet()이 없지만 프로토타입 체인(parent)을 통해 찾음
🎯 🚀 최종 정리
✔️ JS는 클래스 기반 X → 프로토타입 기반 O
✔️ 프로토타입 → 객체가 상속받는 부모 객체
✔️ 프로토타입 체인 → 객체에서 프로퍼티를 찾을 때 상위 프로토타입을 따라가며 검색
✔️ Object.create() → 프로토타입을 직접 설정하여 객체를 생성하는 방법
자바스크립트는 클래스를 사용하지 않고도 프로토타입을 통해 상속을 구현할 수 있음!
'Study > JavaScript' 카테고리의 다른 글
You don't know JS yet(2장 2.1~2.2) (1) | 2025.02.27 |
---|---|
You don't know JS yet(1장 4) (1) | 2025.02.20 |
You don't know JS yet(3-1) 이터레이션 (0) | 2025.02.13 |
You don't know JS yet(2) (0) | 2025.02.06 |
You don't know JS yet(1.5~1.8) (0) | 2025.01.23 |