개요
observer pattern을 사용하면, 관찰자(observer라고 불림. 특정 객체, 함수 등이 될 수 있음)가 다른 관찰 가능한 객체(observable이라고 불림)를 구독하도록 할 수 있다.
이벤트가 발생하면, observable 객체는 자신을 구독하고 있는 모든 observer 객체들에게 알린다.
(대충 observer ->observable이렇게 바라보고 있다고 생각하면 된다.)
observable 객체는 보통 3개의 중요한 부분을 포함한다:
observers: 특정 이벤트가 발생할 때마다 알림을 받을 observer들로 이루어진 배열subscribe(): observer들을 observer list에 추가하기 위한 메서드unsubscribe(): observers들을 observers list에서 제거하기 위한 메서드notify(): 특정 이벤트가 발생할 때마다 모든 observer들에게 알리기 위한 메서드
만들어보자
ES6 class를 사용해서 쉬운 예제를 만들어보자
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));
}
}
notify 메서드에 전달한 data가 observer들의 인자로 전달되도록 한 부분을 눈여겨 보자.
React에서 쉬운 예제
import React from "react";
import { Button, Switch, FormControlLabel } from "@material-ui/core";
import { ToastContainer, toast } from "react-toastify";
import observable from "./Observable";
function handleClick() {
observable.notify("User clicked button!");
}
function handleToggle() {
observable.notify("User toggled switch!");
}
function logger(data) {
console.log(`${Date.now()} ${data}`);
}
function toastify(data) {
toast(data, {
position: toast.POSITION.BOTTOM_RIGHT,
closeButton: false,
autoClose: 2000
});
}
observable.subscribe(logger);
observable.subscribe(toastify);
export default function App() {
return (
<div className="App">
<Button variant="contained" onClick={handleClick}>
Click me!
</Button>
<FormControlLabel
control={<Switch name="" onChange={handleToggle} />}
label="Toggle me!"
/>
<ToastContainer />
</div>
);
}
:FarFileCode: 스토어 구현 예제
const createStoreImpl = (createState) => {
let state;
const listeners = new Set();
const setState = (partial, replace) => {
const nextState = typeof partial === 'function' ? partial(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state =
replace ?? (typeof nextState !== 'object' || nextState === null) ? nextState : Object.assign(state, nextState);
// notify listeners
listeners.forEach((listener) => listener(state, previousState));
}
};
const getState = () => state;
const getInitialState = () => initialState;
const subscribe = (listener) => {
listeners.add(listener);
// unsub
return () => listeners.delete(listener);
};
const destroy = () => listeners.clear();
const api = { getState, setState, subscribe, destroy, getInitialState };
const initialState = (state = createState(setState, getState, api));
return api;
};
const createStore = (createState) => (createState ? createStoreImpl(createState) : createStoreImpl);
export const countStore = createStore((set, _get, _store) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
desc: () => set((state) => ({ count: state.count - 1 })),
}));
observer pattern은 다양한 방법으로 사용될 수 있다. 비동기적이거나 이벤트 기반의 데이터를 다룰 때 매우 유용하다.
예를 들어 특정 데이터 다운로드가 완료되거나, 유저가 게시판에 새로운 메시지를 보내면 다른 모든 멤버들이 알림을 받아야 할 때마다 특정 컴포넌트가 알림을 받기를 원하는 경우에 사용할 수 있다.
장점
observer pattern은 관심사 분리와 단일 책임 원칙을 적용하는 훌륭한 하나의 방법이다. observer 객체는 observable객체에 밀접하게 결합되어 있지 않고, 언제든지 (unsub이나 destroy 같은 메서드로) 탈결합(decoupled)할 수 있다.
observable객체는 오로지 발생하는 이벤트를 모니터링해야 하는 데에만 책임이 있고, 반면 observer들은 간단하게 (전달)받은 데이터들을 다루는 데에만 관심사가 있다.
단점
observer가 너무 복잡해지면, observable객체가 자신을 구독하고 있는 모든 observer에게 notify할 때 성능 문제를 불러일으킬 수도 있다.
레퍼런스
이 글은 옵시디언(Obsidian)에서 작성되었습니다. 티스토리에서 상대경로 링크는 작동하지 않습니다. 큰 이미지 파일은 업로드되지 않을 수 있습니다.