과제 중에 Proxy 객체를 이용해서 옵저버 패턴을 구현하라는 요구가 있었다. Proxy 객체는 뭐고, 옵저버 패턴은 무엇인가? 이것을 찾고 구현하는데 꽤 오랜 시간이 걸렸다. 내가 이해한 방식이 맞는 지는 모르겠지만, 나중에 다시 찾을 때 쉽도록 내가 이해한 것과 구현한 방식을 기록한다.
먼저 옵저버 패턴이 뭔지부터 이해를 해보자.
옵저버 패턴(Observer Pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등으로 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다.
(출처: 위키백과)
간단히 그림으로 표현하면 다음과 같다.
(이해한 대로 그려서 틀릴 수도 있음.)
그렇다면 Proxy 객체는 무엇일까?
Proxy 객체를 사용하면 한 객체에 대한 기본 작업을 가로채고 재정의하는 프록시를 만들 수 있습니다. Proxy 객체를 사용하면 원래
Object
대신 사용할 수 있는 객체를 만들지만, 이 객체의 속성 가져오기(get), 설정(set) 및 정의와 같은 기본 객체 작업을 재정의할 수 있습니다.new Proxy(target, handler)
target
: 프록시할 원복 객체
handler
: 가로채는 작업과 가로채는 작업을 재정의하는 객체(출처: mdn)
Proxy 객체를 사용하면 객체에 대한 기본 작업을 가로채고 재정의할 수 있다고 한다. 어떻게 하는 것인지 예시를 보자.
const target = { message1: "hello", message2: "everyone" }; const handler3 = { get(target, prop, receiver) { if (prop === "message2") { return "world"; } return Reflect.get(...arguments); }, }; const proxy3 = new Proxy(target, handler3); console.log(proxy3.message1); // hello console.log(proxy3.message2); // world(handler가 없으면 everyone 출력)
get의 매개 변수
target
: 대상 객체
prop
: property 가져올 속성의 이름
receiver
: 프록시 또는 프록시에서 상속되는 객체
예시 코드를 보면, proxy3의 객체의 속성 값을 가져오기 위해서 proxy3.message1
과 proxy3.message2
를 썼다. 하지만 handler3
의 get
메서드로 작업을 가로챘다.
그렇다면 옵저버 패턴을 사용하기 위해서는 속성 값을 설정할 때 가로채면 될 것 같다.
옵저버 패턴은 옵저버가 관찰 대상이 되는 객체의 상태 변화를 보고 기능을 수행하는 객체에 전달하는 방식이다. Proxy를 통하면 객체의 속성 값을 설정할 때, 동작을 가로채서 다른 객체가 동작하도록 만들 수 있다.
const target = { color: 'white', }; const boxColorChange = (color) => { const $box = document.querySelector('.color-box'); $box.style.backgroundColor = color; }; const $btnList = document.querySelectorAll('.btn-color'); $btnList.forEach(($btn) => { $btn.addEventListener('click', () => { proxy.color = $btn.innerHTML; }); }); const proxy = new Proxy(target, { set(target, prop, value, receiver) { if (value === 'black') { console.log('검은색은 쓸 수 없습니다.'); return true; } target[prop] = value; boxColorChange(value); console.log(target[prop]); return true; }, });
set의 매개변수
target
: 대상 객체 (선언한 target 객체)
prop
: property 가져올 속성의 이름 (target 객체의 color)
value
: 설정할 속성 값 (button의 inner HTML)
receiver
: 프록시 또는 프록시에서 상속되는 객체
위의 예시 코드는 target
객체의 color
값을 변경하면 boxColorChange
함수가 동작하게 만들었다. 버튼을 누르면 proxy
에 묶은 target
의 color
값을 변경하도록 이벤트리스너를 등록했고, target
객체의 값이 변경되기 때문에 set
이 동작을 가로챈다. 동작을 가로챈 set
내부에 콜백함수를 넣어서 동작하게 만들었고, target
의 color
에 값을 넣고 그 값을 출력하게 만들었다. 마지막으로 만약 value
에 black이 들어오면 실행되지 않도록 하였다.
사실 이 경우 proxy
를 안 쓰고도 간단하게 만들 수 있지만, 옵저버 패턴 예제를 위해서 proxy
객체를 사용해서 코드를 작성해 보았다.
(실제 이게 옵저버 패턴이 맞는지는 잘 모르겠다.)
위의 예제 코드에서도 나왔지만, set을 사용하면 특정 객체의 값을 변경하는 것을 막을 수 있다. 그래서 함부로 변경하면 안되는 값이나, 특정 조건에 맞춰야 하는 값의 경우 임의로 변경하는 것을 막을 수 있다. mdn의 예제를 보자.
const monster1 = { eyeCount: 4 }; const handler1 = { set(obj, prop, value) { if ((prop === 'eyeCount') && ((value % 2) !== 0)) { console.log('몬스터는 짝수의 눈만 가질 수 있습니다.'); } else { return Reflect.set(...arguments); } } }; const proxy1 = new Proxy(monster1, handler1); proxy1.eyeCount = 1; // Expected output: "몬스터는 짝수의 눈만 가질 수 있습니다." console.log(proxy1.eyeCount); // Expected output: 4 proxy1.eyeCount = 2; console.log(proxy1.eyeCount); // Expected output: 2
monster1
의 eyeCount
를 변경하는데 짝수만 입력할 수 있게 만들어 놓았다. 이렇게 하면 홀수로 잘못 입력하는 것을 방지할 수 있다.
const target = { _donotChange : '바꾸지 마세요', canBeChanged: '바꿀 수 있어요' } const handler = { set(target, prop, value, receive){ if(prop.includes('_')) { console.log('바꿀 수 없습니다.') } else { return Reflect.set(...arguments) } } } const proxy = new Proxy(target, handler) proxy._donotChange = '바꾸고 싶다.' // Expected Output: '바꿀 수 없습니다.' proxy.canBeChanged = '이건 바꿀 수 있다.' console.log(proxy.canBeChanged) // Expected Output: '이건 바꿀 수 있다.'
그리고 위의 예시 코드처럼 언더바(_)를 사용해서 값을 임의로 변경하는 것을 막을 수도 있다.(언더바는 변경하면 안되는 변수에 관습적으로 사용한다고 들었다.)
Proxy 객체를 이용해서 옵저버 패턴을 만들기 위해서 이 문서, 저 문서를 뒤졌으나 이렇게 만들면 된다라는 문서를 발견하지 못했다. 그래서 이리저리 테스트 해보던 중 Proxy 객체에 대해서 이해가 되는 부분이 있어서 글로 남겼다. 이 문서가 100% 맞다고 보장할 수는 없지만, 향후 내가 다시 Proxy 객체에 대해서 공부하기 위해서 다시 이 문서를 볼 것은 100% 사실이라고 생각한다. 그 때 내가 이해할 수 있도록 자세히 풀어썼다. 이렇게 글로 정리하니 이해한 내용이 좀 더 잘 정리가 되었다.
혹시라도 이 문서 틀린 내용이 있고, 좀 더 보충해야 하는 내용이 있다면 꼭 댓글로 알려주길 바란다. 잘 모르는 걸 풀어써서 틀린 것이 많을 것이라 예상한다. 그래도 귀엽게 봐주길...