본문으로 건너뛰기

안동민 개발노트

본문 시작

Proxy와 Reflect

Proxy 트랩으로 객체 조회·할당·호출을 가로채고 Reflect의 기본 연산에 위임해 메타 프로그래밍을 구현합니다.

우리는 8장에서 let, const, 화살표 함수, 구조 분해 할당, 스프레드 연산자, 그리고 Symbol, Map, Set까지 최신 핵심 기능을 학습했습니다.

이번에는 더 추상적이지만 실무적으로 중요한 주제인 ProxyReflect를 다룹니다.

Proxy는 객체의 기본 동작(프로퍼티 접근, 할당, 함수 호출 등)을 가로채서(intercept) 원하는 방식으로 제어하게 해줍니다.

Reflect는 이런 동작을 표준 메서드 형태로 제공해 Proxy와 함께 쓸 때 안정적인 구현을 돕습니다.

이 장에서는 Proxy의 트랩(trap) 동작과 Reflect의 역할을 연결해 자바스크립트 메타 프로그래밍(Meta-programming)의 기반을 정리하겠습니다.

Proxy는 가로채고 Reflect는 기본 동작을 보존한다

Proxy는 객체 앞의 관문이고 Reflect는 관문 안에서 원래 객체 연산을 표준 방식으로 이어 주는 도구다.

구간역할좋은 사용깨지기 쉬운 지점
target원본 객체와 prototype 규칙 보관실제 데이터는 target에 둠proxy와 target을 섞어 전달
handler필요한 trap만 정의get/set/has/delete 감시모든 연산을 무리하게 가로챔
Reflect원래 내부 연산에 위임receiver, boolean 반환 규칙 보존trap 안에서 proxy를 다시 호출
invariant언어 불변식 우선non-configurable 규칙 확인숨길 수 없는 속성을 숨기려 함

Proxy: 객체의 동작 가로채기

Proxy 객체는 다른 객체(타겟 객체)의 프록시(대리인) 역할을 합니다.

Proxy를 사용하면 타겟 객체에 대한 모든 연산(프로퍼티 읽기/쓰기, 함수 호출 등)을 가로채어 원하는 로직을 추가할 수 있습니다.

Proxy 생성하기

new Proxy(target, handler) 생성자를 사용하여 Proxy 객체를 생성합니다.

  • target: 프록시할 대상 객체 (일반 객체, 배열, 함수 등).
  • handler: 객체의 동작을 가로챌 트랩(trap) 메서드들을 정의하는 객체.
const targetObject = {
    message1: '안녕하세요',
    message2: '반갑습니다'
};

const handler = {
    // get 트랩: 프로퍼티를 읽으려 할 때 호출됨
    get: function(target, property, receiver) {
        console.log(`[Proxy] '${String(property)}' 프로퍼티를 읽으려 합니다.`);
        // 원래의 프로퍼티 값을 반환
        return target[property];
    },
    // set 트랩: 프로퍼티에 값을 할당하려 할 때 호출됨
    set: function(target, property, value, receiver) {
        console.log(`[Proxy] '${String(property)}' 프로퍼티에 '${value}'를 쓰려 합니다.`);
        // 원래의 할당 동작 수행
        target[property] = value;
        return true; // 성공적으로 할당되었음을 나타냄
    }
};

const proxyObject = new Proxy(targetObject, handler);

// proxyObject를 통해 프로퍼티에 접근하거나 값을 할당하면 handler의 트랩이 동작합니다.
console.log(proxyObject.message1); // get 트랩 호출 후 "안녕하세요" 출력
proxyObject.message1 = '안녕';     // set 트랩 호출
console.log(proxyObject.message1); // get 트랩 호출 후 "안녕" 출력

console.log(proxyObject.message2); // get 트랩 호출 후 "반갑습니다" 출력

주요 트랩(Traps) 종류

handler 객체에는 다양한 종류의 트랩 메서드를 정의할 수 있습니다.

각 트랩은 특정 객체 연산을 가로챕니다.

트랩 메서드설명가로채는 연산
get(target, property, receiver)프로퍼티 읽기proxy.foo, proxy['bar']
set(target, property, value, receiver)프로퍼티 쓰기proxy.foo = 1, proxy['bar'] = 2
has(target, property)in 연산자'foo' in proxy
deleteProperty(target, property)delete 연산자delete proxy.foo
apply(target, thisArg, argumentsList)함수 호출 (Proxy 대상이 함수일 때)proxy(...args)
construct(target, argumentsList, newTarget)new 연산자 (생성자로 호출할 때, Proxy 대상이 생성자 함수일 때)new proxy(...args)
ownKeys(target)Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols()Object.keys(proxy)
getPrototypeOf(target)Object.getPrototypeOf(), __proto__Object.getPrototypeOf(proxy)
setPrototypeOf(target, prototype)Object.setPrototypeOf()Object.setPrototypeOf(proxy, proto)
isExtensible(target)Object.isExtensible()Object.isExtensible(proxy)
preventExtensions(target)Object.preventExtensions()Object.preventExtensions(proxy)
defineProperty(target, property, descriptor)Object.defineProperty(), Object.defineProperties()Object.defineProperty(proxy, 'foo', descriptor)
getOwnPropertyDescriptor(target, property)Object.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor(proxy, 'foo')
예시: 유효성 검사를 하는 프록시
const userProfile = {
    name: "Unknown",
    age: 0
};

const userValidator = {
    set: function(obj, prop, value) {
        if (prop === 'age') {
            if (typeof value !== 'number' || value < 0 || value > 150) {
                console.error("나이는 0에서 150 사이의 숫자여야 합니다.");
                return false; // 할당 실패
            }
        } else if (prop === 'name') {
            if (typeof value !== 'string' || value.length < 2) {
                console.error("이름은 최소 2글자 이상이어야 합니다.");
                return false; // 할당 실패
            }
        }
        obj[prop] = value; // 유효성 검사 통과 시 실제 할당
        return true;
    }
};

const proxyUser = new Proxy(userProfile, userValidator);

proxyUser.name = "John Doe"; // 유효성 통과
proxyUser.age = 30;         // 유효성 통과
console.log(proxyUser);     // 결과: { name: 'John Doe', age: 30 }

proxyUser.age = -5;         // 결과: 나이는 0에서 150 사이의 숫자여야 합니다.
proxyUser.name = "A";       // 결과: 이름은 최소 2글자 이상이어야 합니다.

console.log(proxyUser);     // 여전히 { name: 'John Doe', age: 30 } (변경되지 않음)

이처럼 Proxy를 사용하면 객체의 내부 구현을 건드리지 않고도 외부에서 동작을 제어할 수 있습니다.

이는 Vue 3의 반응성 시스템(Reactivity System)이나 MobX 같은 상태 관리 라이브러리에서 데이터 변경 감지에 활용됩니다.


Reflect: 객체 연산의 내장 헬퍼 객체

Proxy 트랩은 객체 연산별 관문이다

읽기, 쓰기, 존재 확인, 삭제는 각각 다른 trap으로 들어온다. Reflect를 함께 쓰면 가로챈 뒤 원래 의미를 보존하기 쉽다.

연산trapReflect 위임대표 용도
읽기getReflect.get(target, key, receiver)기본값, 접근 로그, 반응성 추적
쓰기setReflect.set(target, key, value, receiver)값 검증, 권한 검사
존재 확인hasReflect.has(target, key)private key 숨김, 정책 적용
삭제deletePropertyReflect.deleteProperty(target, key)삭제 허용 범위 제한

Reflect는 ES2015(ES6)에서 도입된 내장 객체로, 객체에 대한 다양한 내부 연산들을 메서드 형태로 제공합니다.

ReflectProxy와 밀접한 관련이 있으며, 다음과 같은 특징을 가집니다.

  • 메서드 제공: Object 객체의 일부 메서드(Object.keys(), Object.defineProperty())와 유사하지만, Reflect는 대부분의 객체 내부 연산을 Function.prototype.apply()new 연산자처럼 '메서드 호출' 형태로 제공합니다.
  • Proxy 핸들러와의 시너지: Proxy 트랩 내에서 Reflect 메서드를 사용하면, 원래의 기본 객체 연산을 깔끔하게 호출할 수 있습니다. 이는 target[property]와 같은 직접적인 접근보다 권장됩니다.
  • 불리언 반환: 많은 Reflect 메서드는 성공 여부를 불리언 값으로 반환하여, 연산의 성공/실패 여부를 쉽게 판단할 수 있도록 합니다. (예: Reflect.defineProperty, Reflect.deleteProperty)

Reflect의 주요 메서드

Reflect의 메서드들은 대부분 Proxy 핸들러 트랩과 1:1로 매칭됩니다.

  • Reflect.get(target, property, receiver)
  • Reflect.set(target, property, value, receiver)
  • Reflect.has(target, property)
  • Reflect.deleteProperty(target, property)
  • Reflect.apply(target, thisArgument, argumentsList)
  • Reflect.construct(target, argumentsList, newTarget)
  • ... 그 외 Object 객체와 유사한 메서드들
Proxy 핸들러 내에서 Reflect 사용 예시

Proxy 핸들러 내에서 Reflect를 사용하면, 기본 동작을 호출하면서도 this 바인딩(receiver 인자)과 같은 복잡한 내부 로직을 정확하게 처리할 수 있습니다.

const user = {
    firstName: "Jane",
    lastName: "Doe",
    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
};

const loggingHandler = {
    get(target, prop, receiver) {
        console.log(`[Reflect] '${String(prop)}' 프로퍼티에 접근했습니다.`);
        // Reflect.get을 사용하여 원래의 getter를 정확하게 호출
        return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
        console.log(`[Reflect] '${String(prop)}' 프로퍼티를 '${value}'로 설정했습니다.`);
        // Reflect.set을 사용하여 원래의 setter를 정확하게 호출
        return Reflect.set(target, prop, value, receiver);
    }
};

const proxiedUser = new Proxy(user, loggingHandler);

console.log(proxiedUser.firstName); // get 트랩, Reflect.get 호출
proxiedUser.lastName = "Smith";     // set 트랩, Reflect.set 호출
console.log(proxiedUser.fullName);  // get 트랩 (fullName getter가 this.firstName/lastName을 참조하므로 다시 get 트랩 호출)

위 예시에서 Reflect.get(target, prop, receiver)는 단순히 target[prop]을 하는 것보다 receiver 인자를 통해 fullName과 같은 getter의 this 컨텍스트를 프록시 객체 자신으로 정확하게 유지시켜줍니다.

이는 Proxy의 동작을 훨씬 견고하게 만듭니다.

Reflect의 다른 유용한 메서드

ReflectObject 객체에서 에러를 던지던 일부 연산들을 불리언 값을 반환하도록 하여 더욱 유연한 에러 핸들링을 가능하게 합니다.

const obj = {};

// Object.defineProperty는 실패 시 에러를 던짐
try {
    Object.defineProperty(obj, 'prop', { value: 1, writable: false });
    Object.defineProperty(obj, 'prop', { value: 2 }); // TypeError: Cannot redefine property
} catch (e) {
    console.error(e.message);
}

// Reflect.defineProperty는 실패 시 false를 반환
const success1 = Reflect.defineProperty(obj, 'anotherProp', { value: 10 });
console.log(success1); // 결과: true

const success2 = Reflect.defineProperty(obj, 'prop', { value: 2 }); // 실패
console.log(success2); // 결과: false (에러를 던지지 않음)

이러한 특성은 특히 복잡한 객체 조작 로직이나 라이브러리 구현 시 유용합니다.

Proxy 트랩을 직접 작성할 때는 "무엇을 가로챌지"만큼 "기본 객체 규칙을 어떻게 보존할지"가 중요합니다.

아래 다이어그램은 무한 재귀, 잘못된 반환값, receiver 누락처럼 실수하기 쉬운 지점을 구현 전 체크리스트로 정리합니다.

Proxy 트랩 안전 점검 기준

Proxy 트랩은 자유도가 높지만, 객체의 불변 규칙과 반환 규칙을 어기면 런타임 오류나 예측하기 어려운 동작으로 이어집니다.

  1. 재귀 방지
    프록시를 다시 읽지 않기

    트랩 내부에서 proxy[prop]를 호출하면 같은 트랩에 다시 들어갑니다. target과 Reflect에 위임합니다.

  2. 반환 규칙
    성공 여부를 맞추기

    set, deleteProperty, defineProperty는 성공 여부를 boolean으로 돌려줘야 호출부가 올바르게 판단합니다.

  3. receiver 유지
    getter의 this 보존

    Reflect.get(target, prop, receiver)를 쓰면 상속과 getter가 바라보는 this를 안전하게 유지합니다.

  4. 불변 조건
    객체 규칙을 속이지 않기

    동결된 객체나 non-configurable 속성은 실제 상태와 다른 결과를 반환할 수 없습니다.

브라우저 화면 상태를 다룰 때는 Proxy로 변경을 감지하고 Reflect로 기본 동작을 보존한 뒤 렌더링을 예약하는 흐름으로 이해할 수 있습니다.

Proxy는 쓰기 지점을 모아 렌더를 예약한다

상태 객체 앞에 Proxy를 두면 set 트랩에서 기본 할당, 변경 비교, 렌더 예약을 한 경계에서 처리할 수 있다.

지점역할주의검증
Action 상태 쓰기클릭, 입력, 비동기 응답이 상태 프로퍼티 변경을 만든다.DOM을 직접 바꾸면 상태와 화면의 기준이 갈라진다.모든 변경이 상태 쓰기를 통해 시작되는지 추적한다.
set trap 감지 경계어떤 key가 어떤 값으로 바뀌는지 한곳에서 본다.중첩 객체는 별도 Proxy가 없으면 내부 변경을 놓친다.변경 로그가 key, old value, new value를 남긴다.
Reflect.set 기본 동작원래 객체의 할당 규칙과 반환값을 보존한다.반환값을 무시하면 실패한 할당도 성공처럼 보일 수 있다.성공한 할당에서만 이후 렌더 예약이 실행된다.
Render queue 화면 갱신여러 변경을 묶어 다음 프레임 DOM 갱신으로 보낸다.매 변경마다 즉시 렌더하면 입력 중 화면이 흔들린다.같은 프레임의 중복 변경이 한 번의 갱신으로 합쳐진다.
DevTools 원인 추적상태 변경 로그와 브레이크포인트로 화면 변경 원인을 찾는다.렌더 결과만 보면 어떤 상태가 원인인지 늦게 찾는다.문제 화면에서 마지막 상태 변경자를 확인할 수 있다.

Proxy와 Reflect 통합 정리

이번 장에서는 ES2015(ES6)에서 도입된 객체 동작의 메타 프로그래밍 기능인 ProxyReflect를 학습했습니다.

Proxy·Reflect는 부가 기능을 원래 연산 옆에 붙인다

검증, 로깅, 반응성은 원본 객체 의미를 대체하는 기능이 아니라 접근 순간에 붙는 부가 규칙이다.

단계무슨 일이 일어나는가확인할 것실패 신호
1 접근proxy.name 또는 proxy.name = value외부에 proxy가 전달됐는가target을 직접 만짐
2 traphandler가 연산을 받음필요한 연산만 잡는가숨은 동작이 너무 많음
3 Reflect기본 객체 동작으로 위임반환값 규칙이 맞는가strict mode에서 TypeError
4 effect로그, 검증, 화면 갱신 연결부작용 위치가 보이는가디버깅 경로가 불명확

Proxytarget 객체의 프로퍼티 읽기/쓰기, 함수 호출 같은 연산을 handler로 가로챌 수 있습니다.

이를 이용하면 유효성 검사, 로깅, 접근 제어를 객체의 핵심 구현과 분리해 둘 수 있습니다.

또한, Reflect 객체가 객체 내부 연산을 위한 메서드들을 제공하며, 특히 Proxy 핸들러 내에서 target[prop] 대신 Reflect 메서드를 사용함으로써 보다 정확하고 견고한 동작을 보장한다는 것을 배웠습니다.

Reflect는 연산 성공 여부를 불리언으로 반환하여 에러 핸들링을 유연하게 할 수 있도록 돕습니다.

ProxyReflect는 객체 연산을 가로채거나 표준 내부 연산에 맞춰 위임할 때 사용합니다.

Vue.js 3 같은 프레임워크의 반응성 구조를 읽을 때도 이 개념이 기준이 됩니다.

Proxy·Reflect 적용 전에는 비용을 먼저 본다

Proxy는 강력하지만 디버깅 난도와 성능 비용을 만든다. 목적이 검증·로깅·반응성처럼 명확할 때만 선택한다.

질문A에 가까운 답위험한 답대안
가로챌 연산이 분명한가get/set 등 특정 trap만 필요모든 접근을 막연히 감시명시적 setter
기본 동작을 유지하나Reflect로 위임직접 흉내 내며 규칙 누락일반 함수 래퍼
오류가 예측 가능한가invariant와 strict 실패 처리실패가 조용히 무시됨스키마 검증
디버깅 이득이 큰가정책이 한곳에 모임호출자가 동작을 추적 못함명시적 API

마지막 다이어그램은 Proxy 생성, 주요 트랩, Reflect 위임, receiver와 반환값 주의점을 기준으로 객체 연산을 안전하게 가로채는 방법을 정리합니다.

Proxy/Reflect는 접근 계약을 명시한다

target은 데이터, handler는 접근 정책, Reflect는 원래 의미를 이어 주는 표준 호출이다. 세 역할이 섞이면 코드가 불투명해진다.

역할책임좋은 신호나쁜 신호
target원본 상태 보관직접 노출하지 않음일부 코드는 target, 일부는 proxy 사용
handler읽기·쓰기 정책 정의trap이 짧고 목적이 명확trap이 비즈니스 로직을 모두 품음
Reflect기본 동작 호출prototype/receiver 의미 유지수동 구현으로 edge case 누락
callerproxy를 객체처럼 사용실패가 명확히 보임가로채기를 모른 채 의존
Proxy와 Reflect는 객체 동작을 감싸는 쌍이다

Proxy가 연산을 멈춰 세우고, Reflect가 같은 연산의 기본 경로를 호출한다. 둘을 함께 보면 메타프로그래밍의 경계가 선명하다.

객체 동작Proxy가 하는 일Reflect가 하는 일주의점
property readget trap으로 관찰Reflect.get으로 기본 읽기receiver this 바인딩
property writeset trap으로 검증Reflect.set으로 기본 쓰기boolean 반환
membershiphas trap으로 제한Reflect.has로 in 동작 유지숨겨도 불변식은 유지
deletedeleteProperty로 통제Reflect.deleteProperty 위임삭제 실패 처리