심화개념 Proxy, Reflect, 디바운스/쓰로틀
Proxy (get/set 트랩, 객체 동작 가로채기)
Proxy는 객체에 대한 기본 동작(속성 읽기, 쓰기, 삭제 등)을 가로채서 커스텀 로직을 끼워 넣을 수 있게 해주는 객체다. “이 객체에 접근하거나 값을 바꾸려고 할 때, 그 전에 내가 먼저 확인하고 처리하겠다”는 개념이다.
const user = { name: "gori", age: 20 };
const proxyUser = new Proxy(user, {
get(target, key) {
console.log(`${key} 읽음`);
return target[key];
},
set(target, key, value) {
if (key === "age" && value < 0) {
console.log("나이는 음수가 될 수 없습니다");
return false;
}
target[key] = value;
return true;
}
});
console.log(proxyUser.name); // "name 읽음" 출력 후 "gori" 반환
proxyUser.age = -5; // "나이는 음수가 될 수 없습니다" (변경 안 됨)get, set처럼 동작을 가로채는 함수를 **트랩(trap)**이라고 부른다. 객체 값을 읽거나 쓸 때마다 유효성 검사, 로깅 같은 로직을 자동으로 끼워 넣을 수 있다.
Reflect
Reflect는 객체를 다루는 기본 동작(읽기, 쓰기, 삭제 등)을 메서드로 정리해둔 내장 객체다. Proxy의 트랩 이름과 1:1로 대응해서, 둘은 항상 함께 쓰인다.
const handler = {
set(target, key, value) {
return Reflect.set(target, key, value); // target[key] = value와 같은 동작
}
};const user = { name: "gori" };
Reflect.get(user, "name"); // "gori"
Reflect.set(user, "age", 20); // user.age = 20
Reflect.has(user, "name"); // true ("name" in user와 동일)Proxy가 동작을 가로채는 역할이라면, Reflect는 그 동작을 표준화된 방식으로 실행해주는 역할이다.
얕은 복사 vs 깊은 복사
얕은 복사는 객체의 최상위 속성만 새로 복사하고, 중첩된 객체는 원본과 같은 참조를 공유한다.
const original = { name: "gori", address: { city: "서울" } };
const copy = { ...original };
copy.name = "고리";
console.log(original.name); // "gori" (영향 없음)
copy.address.city = "부산";
console.log(original.address.city); // "부산" (영향 받음)깊은 복사는 중첩된 객체까지 모두 새로 복사해서, 원본과 완전히 분리된다.
const copy2 = structuredClone(original);
copy2.address.city = "부산";
console.log(original.address.city); // "서울" (영향 없음)| 구분 | 복사 범위 | 방법 |
|---|---|---|
| 얕은 복사 | 최상위 속성만 | spread(...), Object.assign |
| 깊은 복사 | 중첩 객체까지 전부 | structuredClone |
메모리 관리 & 가비지 컬렉션
JS는 메모리를 자동으로 관리한다. 더 이상 어디서도 참조되지 않는 값을 엔진이 찾아서 자동으로 회수하는데, 이 과정을 **가비지 컬렉션(GC)**이라고 한다.
let user = { name: "gori" };
user = null; // 이 객체를 참조하는 곳이 없어짐 → GC 대상function createUser() {
const user = { name: "gori" };
return user;
}
let gori = createUser(); // gori가 계속 참조하고 있어서 GC 대상 아님이벤트 리스너나 타이머를 정리하지 않고 계속 쌓아두면, 그 안에서 참조하는 데이터가 회수되지 않고 메모리에 남는다. 이를 메모리 누수라고 한다. 다 쓴 리스너는 removeEventListener로, 다 쓴 타이머는 clearInterval로 정리하는 것이 좋다.
디바운스 & 쓰로틀
짧은 시간에 반복적으로 발생하는 이벤트(스크롤, 타이핑 등)의 실행 빈도를 제어하는 기법이다.
디바운스 — 이벤트가 멈추고 일정 시간이 지난 뒤 마지막 한 번만 실행한다.
function debounce(callback, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => callback(...args), delay);
};
}
const search = debounce((keyword) => {
console.log(`"${keyword}" 검색`);
}, 500);쓰로틀 — 이벤트가 아무리 자주 발생해도 일정 간격마다 최대 한 번만 실행한다.
function throttle(callback, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
callback(...args);
}
};
}
const handleScroll = throttle(() => {
console.log("스크롤 위치:", window.scrollY);
}, 300);| 구분 | 동작 방식 | 예시 |
|---|---|---|
| 디바운스 | 멈추고 일정 시간 후 1번만 실행 | 검색어 자동완성 |
| 쓰로틀 | 일정 간격마다 1번씩 실행 | 스크롤 이벤트 처리 |