Observer 패턴에서 특정 객체를 구독할 수 있는데 구독하는 주체를 Observer라고 하고, 구독 가능한 객체를 Observable이라고 한다.
이벤트가 발생할 때마다 Observable은 모든 Observer에게 이벤트를 전파한다.
observers
: 이벤트가 발생할 때마다 전파할 Observer들의 배열subscribe()
: Observer를 Observers 배열에 추가한다.unsubscribe()
: Observers 배열에서 Observer를 제거한다.notify()
: 등록된 모든 Observer들에게 이벤트를 전파한다.아래는 ES6 클래스를 사용하여 Observable을 구현한 예제이다.
class Observable {
constructor() {
this.observers = []
}
subscribe(func) {
this.observers.push(func)
}
unsubscribe(func) {
this.observers = this.observers.filter(observer => observer !== func)
}
notify(data) {
this.observers.forEach(observer => observer(data))
}
}
subscribe 메서드를 통해 Observer를 등록하고, 반대로 unsubscribe를 통해 등록 해지할 수 있다.
그리고 notify 메서드를 통해 모든 Observer에게 이벤트를 전파할 수 있다.
구현한 Observable 객체를 이용해 Button
컴포넌트와 Switch
컴포넌트를 가진 기본적인 앱을 만들어보자.
export default function App() {
return (
<div className="App">
<Button>Click me!</Button>
<FormControlLabel control={<Switch />} />
</div>
)
}
앱 내에서 일어나는 사용자 인터렉션을 추적하고 싶다. 사용자가 버튼을 클릭하던 스위치를 토글하던 간에 타임스탬프를 로깅하려고 한다. 또 이벤트 발생 시마다 토스트 알림을 화면에 노출하려고 한다.
사용자가 handleClick
또는 handleToggle
함수를 호출할 때마다 핸들러는 Observable의 notify
를 호출한다.
notify
메서드는 등록된 모든 Observer에게 handleClick
또는 handleToggle
에서 전달된 데이터를 포함한 이벤트를 전파한다.
먼저 logger
함수와 toastify
함수를 만들자. 이 함수들은 notify
메서드로부터 data
를 받게 된다.
import { ToastContainer, toast } from 'react-toastify'
function logger(data) {
console.log(`${Date.now()} ${data}`)
}
function toastify(data) {
toast(data)
}
export default function App() {
return (
<div className="App">
<Button>Click me!</Button>
<FormControlLabel control={<Switch />} />
<ToastContainer />
</div>
)
}
아직까지 Observable과 logger
, toastify
함수가 연결되지 않았기 때문에 알림을 받을 수 없다.
이 함수들이 Observer로써 작동하기 위해서는 Observable의 subscribe
메서드를 사용해야 한다.
import { ToastContainer, toast } from 'react-toastify'
function logger(data) {
console.log(`${Date.now()} ${data}`)
}
function toastify(data) {
toast(data)
}
observable.subscribe(logger)
observable.subscribe(toastify)
export default function App() {
return (
<div className="App">
<Button>Click me!</Button>
<FormControlLabel control={<Switch />} />
<ToastContainer />
</div>
)
}
이제 이벤트가 발생할 때마다 logger
와 toastify
함수는 알림을 받게 될 것이다. 이제 Observable에 이벤트를 전파하는 코드만 작성하면 된다.
버튼 클릭에 해당하는 handleClick
, 스위치 토글에 해당하는 handleToggle
이벤트 핸들러를 만들고 안에서 Observable의 notify
를 실행해 이벤트를 전파한다. 이 때 Observer에서 필요한 데이터를 인자로 넘긴다.
import { ToastContainer, toast } from 'react-toastify'
function logger(data) {
console.log(`${Date.now()} ${data}`)
}
function toastify(data) {
toast(data)
}
observable.subscribe(logger)
observable.subscribe(toastify)
export default function App() {
function handleClick() {
observable.notify('User clicked button!')
}
function handleToggle() {
observable.notify('User toggled switch!')
}
return (
<div className="App">
<Button>Click me!</Button>
<FormControlLabel control={<Switch />} />
<ToastContainer />
</div>
)
}
전체 플로우를 살펴보면, handleClick
과 handleToggle
이 Observable의 notify를 호출하고, 뒤이어 이를 구독하고 있던 Observer logger
와 toastify
함수는 이 이벤트를 받아 특정 동작을 수행한다.
앱 내에서 인터렉션이 발생하는 동안 logger
와 toastify
는 notify
의 호출로부터 이벤트를 계속 받을 수 있다.
Observer 패턴은 다양하게 활용할 수 있지만 비동기 호출 혹은 이벤트 기반 데이터를 처리할 때 매우 유용하다.
만약 어떤 컴포넌트가 특정 데이터의 다운로드 완료 알림을 받기 원하거나 사용자가 메시지 보드에 새로운 메시지를 게시했을 때 모든 멤버가 알림을 받거나 하는 등의 상황에 유용하다.
RxJS는 Observer 패턴을 구현한 유명 오픈소스 라이브러리이다.
RxJS를 사용하면 Observable과 Observer(Subscriber)를 만들어낼 수 있다.
아래 예제는 공식 문서에 소개된 것으로 사용자가 문서를 드래그 중인지 아닌지 콘솔에 출력해준다.
RxJS는 이것 말고도 Observer 패턴에 대한 많은 빌트인 기능들을 제공한다.
import React from "react";
import ReactDOM from "react-dom";
import { fromEvent, merge } from "rxjs";
import { sample, mapTo } from "rxjs/operators";
import "./styles.css";
merge(
fromEvent(document, "mousedown").pipe(mapTo(false)),
fromEvent(document, "mousemove").pipe(mapTo(true))
)
.pipe(sample(fromEvent(document, "mouseup")))
.subscribe(isDragging => {
console.log("Were you dragging?", isDragging);
});
ReactDOM.render(
<div className="App">Click or drag anywhere and check the console!</div>,
document.getElementById("root")
);
Observer패턴을 사용하는 것 역시 관심사의 분리와 단일 책임의 원칙을 강제하기 위한 좋은 방법이다. Observer 객체는 Observable 객체와 강하게 결합되어 있지 않고 언제든지 분리될 수 있다. Observable 객체는 이벤트 모니터링의 역할을 갖고, Observer는 받은 데이터를 처리하는 역할을 갖게 된다.
Observer가 복잡해지면 모든 Observer들에 알림을 전파하는 데 성능 이슈가 발생할 수 있다.
< 출처 : https://patterns-dev-kr.github.io/design-patterns/observer-pattern/ >