Observer 패턴
Observer 패턴에서 특정 객체를 구독할 수 있는데. 구독하는 주체를 Observer라 하고. 구독 가능한 객체를 Observable이라 한다. 이벤트가 발생할 때 마다 Observable은 모든 Observer에게 이벤트를 전파한다.
Observable 객체는 보통 3가지 주요 특징을 포함한다
observers
: 이벤트가 발생할때마다 전파할 Observer들의 배열subscribe()
: Observer를 Observer 배열에 추가한다unsubscribe()
: Observer 배열에서 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 패턴을 구현한 유명 오픈소스 라이브러리이다
ReactiveX 는 Observer 패턴, 이터레이터 패턴, 함수형 프로그래밍을 조합하여 이벤트의 순서를 이상적으로 관리할 수 있다.
RxJS를 사용하면 Observable과 Observer(Subscriber)를 만들어낼 수 있다. 아래 예제는 공식 문서에 소개된 것으로 사용자가 문서를 드래그 중인지 아닌지 콘솔에 출력해준다.
RxJS는 이것 말고도 Observer 패턴에 대한 매우 많은 빌트인 기능들을 제공한다.
장점
Observer 패턴을 사용하는 것도 관심사의 분리와 단일 책임의 원칙을 강제하기 위한 좋은 방법이다. Observer 객체는 Observable 객체와 강결합되어있지 않고 언제든지 분리될 수 있다. Observable 객체는 이벤트 모니터링의 역할을 갖고. Observer는 받은 데이터를 처리하는 역할을 갖게 된다.
단점
Observer가 복잡해지면 모든 Observer들에 알림을 전파하는 데 성능 이슈가 발생할 수 있다.