Proxy
- Proxy 는 특정 객체를 감싸 프로퍼티 읽기, 쓰기와 같은 객체에 가해지는 작업을 중간에서 가로채는 객체이다.
- 가로채진 작업은 Prosxy 자체에서 처리되기도 하고, 원래 객체가 처리하도록 그대로 전달되기도 한다.
const target = { x: 10, y: 20 };
const handler = {
get: function(obj, prop) {
console.log("속성에 접근했습니다.")
return obj[prop];
}
}
let proxy = new Proxy(target, handler);
console.log(proxy.x); // "속성에 접근했습니다." 출력 후 10 반환
-
target : 감싸게 될 객체
-
handler: 동작을 가로채는 메서드인 '트랩'이 담긴 객체로, 프락시를 설정한다.
- 만약 트랩이 없으면, proxy 에 가해지는 모든 작업은 target 에 전달된다.
-
Proxy 는 일반 객체와 다른 행동 양상을 보이는
특수객체(exotic object)
이다. 즉, 프로퍼티가 없음- handler 가 비어있으면 Proxy 에 가해지는 작업은 target 에 곧바로 전달된다.
Proxy 메모리할당은?
new Proxy()
로 생성된 Proxy 객체는 실제로 새로운 메모리 공간을 할당받는다.- 하지만, 이 새로운 메모리 공간은 주로 Proxy 객체 자체의 메타 데이터를 저장하는데 사용된다.
- 즉, handler 객체와 target 에 대한 참조를 저장하는데 사용된다.
- Proxy 객체는 원본(타겟)객체에 대한 참조를 유지한다.
- 타겟 객체 자체는 별도로 복사되지 않고, 동일한 메모리 위치를 가리킨다.
- Proxy는 타겟 객체의 속성에 직접 접근하지 않고, 가로채기(interception) 메커니즘을 통해 작동한다.
- 이는 대량의 데이터를 복사하지 않고도 객체의 동작을 수정할 수 있게 해준다.
Proxy 사용 사례 - immer
- immer 를 사용하면 마치 객체를 직접 수정하는 것 처럼 코드를 작성하지만, 실제로는 불변성을 유지하면서 새로운 상태를 생성한다.
- immer 의 핵심 개념은
draft
이다.- draft는 proxy 를 사용하여 구현되어있고,
- 개발자가 draft를 수정하면 Immer는 이 변경사항을 추적하고, 최종적으로 새로운 불변상태를 생성한다.
import produce from 'immer';
const baseState = [
{ title: "Learn TypeScript", done: true },
{ title: "Try Immer", done: false }
];
const nextState = produce(baseState, draft => {
draft[1].done = true;
draft.push({ title: "Tweet about it" });
});
- 위 코드에서
nextState
는baseState
를 변경하지 않고 새로운 상태를 생성한다. - Immer 가 Proxy 를 사용하는 이유는, 개발자가 마치 객체를 직접 수정하는 것 처럼 코드를 작성하게끔 할 수 있어서!
- 그럼 produce 내부는 어떻게 되어있을까?
function produce(baseState, recipe) {
const draft = createProxy(baseState); // 여기서 프록시를 만든다.
// 프록시로 만드는 이유는 아래에 createProxy 에서
// set 메서드 실행 시, 변경사항을추적하기 위함임.
const changes = []; // set 에 가해진 변경사항들은 요 changes 배열에 모두 저장된다.
const recordChange = (path, value) => {
changes.push({ path, value });
};
// 그리고 handler 함수가 실행되고, draft (프록시 객체)를 인자로 넘긴다.
recipe(draft);
// 고러면 요 handler 함수에서는 알아서 쿵짝쿵짝 draft 객체 변경하고 이러쿵저러쿵 한다.
// draft 객체 내 속성을 변경했다면 위의 chnages 배열에 담겼을거고
// 마지막으로 원본 객체에 변경사항들을 적용한다.
return applyChanges(baseState, changes);
}
function createProxy(target) {
return new Proxy(target, {
get(target, prop) {
// 속성 접근 로직
},
set(target, prop, value) {
// 속성 설정 로직 및 변경 기록
recordChange([prop], value); // 🎃 변경 기록
return true; // 🎃 실제로 해당 원본객체를 변경하지는 않는다 (target[prop] 변경X)
}
// 기타 필요한 트랩들...
});
}
function applyChanges(baseState, changes) {
// 🎃 원본 객체와 변경사항을 기반으로 아예 새로운 객체를 만든다.
const newState = Array.isArray(baseState) ? [...baseState] : {...baseState};
for (const change of changes) {
let current = newState;
const { path, value } = change;
// 마지막 속성 전까지 순회
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
if (!(key in current)) {
current[key] = typeof path[i + 1] === 'number' ? [] : {};
}
current = current[key];
}
// 마지막 속성에 값 할당
const lastKey = path[path.length - 1];
if (value === undefined) {
if (Array.isArray(current)) {
current.splice(lastKey, 1);
} else {
delete current[lastKey];
}
} else {
current[lastKey] = value;
}
}
return newState;
}
- 즉 immer 에서 proxy 를 사용하는 목적은
- 실제 객체는 변경하지 않기 위함
객체에 가해지는 변경사항(set)
들을 추적하기 위함이다.