icon
8장 : 최신 ECMAScript 기능

Proxy와 Reflect

우리는 8장에서 let, const, 화살표 함수, 구조 분해 할당, 스프레드 연산자, 그리고 Symbol, Map, Set과 같은 ES2015(ES6) 이후의 핵심 기능들을 학습했습니다. 이 기능들은 자바스크립트 코드를 더 간결하고 효율적이며, 다양한 상황에서 유연하게 데이터를 다룰 수 있도록 도와주었습니다. 이제 ES2015의 가장 강력하고도 추상적인 기능 중 하나인 Proxy 와, Proxy와 함께 사용되거나 객체 조작을 위한 새로운 API를 제공하는 Reflect 에 대해 알아보겠습니다.

Proxy는 객체의 기본적인 동작(예: 프로퍼티 접근, 할당, 함수 호출 등)을 '가로채서(intercept)' 개발자가 원하는 대로 커스터마이징할 수 있게 해주는 기능입니다. 이는 프레임워크나 라이브러리에서 데이터 바인딩, 유효성 검사, 로깅 등 복잡한 로직을 구현할 때 매우 유용합니다. Reflect는 이러한 객체 동작에 대한 기본적인 연산을 메서드로 제공하여 Proxy와 함께 사용될 때 강력한 시너지를 발휘합니다.

이번 장에서는 Proxy의 개념과 다양한 '트랩(trap)'을 통한 객체 동작 가로채기, 그리고 Reflect의 역할과 사용법을 깊이 있게 탐구해 보겠습니다. 이 기능들은 자바스크립트의 메타 프로그래밍(Meta-programming) 능력을 한 단계 끌어올려 줍니다.


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: 객체 연산의 내장 헬퍼 객체

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 (에러를 던지지 않음)

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


마무리하며

이번 장에서는 ES2015(ES6)에서 도입된 객체 동작의 메타 프로그래밍을 위한 강력한 기능인 ProxyReflect 에 대해 심도 있게 학습했습니다.

여러분은 Proxytarget 객체의 handler를 통해 다양한 객체 연산(프로퍼티 읽기/쓰기, 함수 호출 등)을 '가로채서' 커스터마이징할 수 있다는 것을 이해했습니다. 이를 통해 유효성 검사, 로깅, 접근 제어 등 복잡한 로직을 객체의 핵심 구현을 변경하지 않고도 구현할 수 있음을 확인했습니다.

또한, Reflect 객체가 객체 내부 연산을 위한 메서드들을 제공하며, 특히 Proxy 핸들러 내에서 target[prop] 대신 Reflect 메서드를 사용함으로써 보다 정확하고 견고한 동작을 보장한다는 것을 배웠습니다. Reflect는 연산 성공 여부를 불리언으로 반환하여 에러 핸들링을 유연하게 할 수 있도록 돕습니다.

ProxyReflect는 일상적인 애플리케이션 개발에서 직접적으로 자주 사용되지는 않지만, Vue.js 3와 같은 모던 프레임워크의 반응성 시스템의 핵심 엔진으로 활용될 정도로 강력한 기능입니다. 이들을 이해하는 것은 자바스크립트의 깊은 동작 원리를 파악하고, 복잡한 라이브러리나 프레임워크의 내부 로직을 분석하는 데 큰 도움이 될 것입니다.