본문 바로가기
Development/JavaScript

자바스크립트로 옵저빙(변수 변화 탐지)를 구현하는 방법들

by 개발자 데이빗 2022. 9. 22.

자바스크립트의 옵저빙

자바스크립트에서 변수의 변화를 탐지하는 방법들에 대해 정리해보았다.

옵저버(관찰자) 디자인 패턴

리덕스에서 사용되는 옵저버 패턴 예시

getState()를 통해 상태를 바라보는 순간 subscribe가 된다는 점이라던가 변경된 state와 이전 state를 비교해 리렌더링을 한다던가 하는 부분처럼 실제 구현과는 차이가 있다.

하지만 디자인 패턴을 파악하기 위한 예시이므로 생략한다.

export function createStore() {
    // 외부에서 접근할 수 없도록, state값을 저장(캡퍼링)하기 위해 클로저 구현
    let state;
    const handlers = [];

    function dispatch(newState) {
        state = newState
        // 호출될때마다 구독된 리스너들 실행
        handlers.forEach((listener) => {
            listener();
        })
    }

    function getState() {
        return state;
    }

    // 리스너 콜백 함수를 등록
    function subscribe(listener) {
        handlers.push(listener);
    }

    return {
        dispatch,
        getState,
        subscribe,
    }
}

const store = createStore()

function renderDom() {
    // 무언가 실제 돔과 가상돔을 비교하여 리렌더링하는중...
    console.log('rerender!');
}

function listener() {
    console.log('변경된 state', store.getState());
    renderDom()
}

store.subscribe(listener)

store.dispatch({
    awesomeData: 'dabin',
});

위 코드는 글로벌 상태를 가지고 있는 store를 만드는 createStore()함수이다.

state는 외부에서 함부로 변경하면 안되기 때문에 클로저 함수를 통해 비공개변수로 만든다.

state를 변경하기 위한 dispatch함수, state에 접근하기 위한 getState함수, state의 변경을 탐지하기 위한 subscribe함수를 외부로 노출시킨 모습이다.

 

이후 코드와 같이 subscribe 내부변수(메소드)를 실행하며 매개변수로 listener콜백을 넘겨 dispatch가 될때마다 변화된 state와 함께 리렌더링을 진행한다.

 

옵저버 패턴의 간단한 코드로 프론트엔드 개발자라면 한번쯤은 접해봤을 리덕스의 구조를 구현할 수 있다.

getter와 setter를 사용

es5에서는 Object.defineProperty를 사용하여 getter, setter를 정의하여 구현하며 es6에서는 get, set 키워드를 사용하여 구현한다.

카운터 예제

// es5
var count = {
}

Object.defineProperty(count, 'number', {
    get() {
        return this.num || 0;
    },
    set(num) {
        this._num = num;
        console.log(this._num);
        document.querySelector('#count').textContent = this._num;
    }
})


document.querySelector('#up').addEventListener('click', function() {
    count.number++;
});
document.querySelector('#down').addEventListener('click', function() {
    count.number--;
});
// es6 (es2015)
let count =  {
    get number() {
        return this._num || 0;
    },
    set number(num) {
        console.log(num);
        document.querySelector('#count').textContent = this._num;
    }
};

document.querySelector('#up').addEventListener('click', function() {
    count.number++;
});
document.querySelector('#down').addEventListener('click', function() {
    count.number--;
});

 

Proxy

es6에서는 Proxy 객체를 통해서도 구현할 수 있다.

카운터 예제

// es6 (es2015) Proxy
const count = {};
const handler = {
    get: (obj, name) => {
        if (name === 'number') {
            return this._num || 0;
        }
    },
    set: (obj, name, value) => {
        if (name === 'number') {
            this._num = value;
            console.log(count);
            document.querySelector('#count').textContent = this._num;
        }
    }
};

const proxy = new Proxy(count, handler);

document.querySelector('#up').addEventListener('click', () => {
    proxy.number++;
});
document.querySelector('#down').addEventListener('click', () => {
    proxy.number--;
});

 

 

주의할 점

getter에서 값을 대입하는 실수를 하는 경우 무한 루프에 빠져 스택 오버플로우가 일어날 수 있다.

 

Proxy 객체란?

Proxy 객체는 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 새로운 행동을 정의할 때 사용된다.
즉, count객체의 속성 접근, 할당에 대한 행동을 새롭게 정의하여 옵저빙을 구현한다.

기존의 객체는 그대로 두고 핸들러를 통해 새로운 행동들만 정의한다.

이게 무슨 말이냐면 proxy로 인해 생긴 객체에 프로퍼티를 추가하면 기존 객체도 영향을 받지만 새로운 행동들은 proxy객체에만 정의된다.

아래 getter 예제 코드를 보면 이해가 더 쉽다.

아래 예시 코드는 getter를 재정의하여 존재하지 않는 프로퍼티에 접근하면 default value를 리턴하는 코드이다.

let targetObj = {};
let handler = {
    get: function(target, name) {
        return name in target?
        target[name] : 'default value'
    }
}

let proxy = new Proxy(targetObj, handler);
proxy.a = 1;
proxy.b = undefined;

console.log(proxy.a); // 1
console.log(proxy.b); // undefined
console.log(proxy.c); // default value
console.log(targetObj); // {a: 1, b: undefined}
console.log(targetObj.c); // undefined

getter, setter 이외에도 apply, constructure 등 다양한 행동을 재정의할 수 있지만 이 포스팅은 옵저빙을 다루는 글이므로 자세한 사용 예시는 아래 링크를 참조

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Proxy 기본 구문은 다음과 같다.

  • target: proxy와 함께 감싸진 target 객체 (native array, function, 다른 proxy를 포함한 객체);
  • handler: 프로퍼티들이 function인 객체, 동작이 수행될 때 handler는 proxy의 행동을 정의한다.
new Proxy(target, handler);

 

Proxy 기본 setter 예제

setter를 통해 age 프로퍼티의 타입과 범위를 검증하는 예제이다.

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // The default behavior to store the value
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // TypeError
person.age = 300; // RangeError

 

댓글