옵저버 패턴
발행-구독 패턴에 대해 공부하기 전 챌린지반에서 옵저버 패턴을 공부했다.
어떤 변화가 일어났을 때 그 변화를 감지해야할 곳에서 변화 상태를 구독하는 방식으로 구현하는 것을 의미한다.
이 패턴을 구현하기 위해 특정 요소의 업데이트가 발생할 때 호출할 함수들 목록(구독자/관찰자 목록이라고 할 수 있다!)을 만들어두고, 구독할 요소들에서 변경된 값을 반영하기위한 함수(변화시 호출될 콜백함수)들을 그 구독자 목록에 push해주면 된다.
옵저버 패턴의 특징
- 상태 변화가 발생하는 요소(주체)가 자신의 변화가 미칠 영향을 몰라도 된다.
- 주체와 그 관찰자의 결합도가 낮다.
- 옵저버로 등록(구독)해야하므로 두 요소는 완전히 모르는 상태가 아니다.
구현 예제
// Subject.js -> 주체
const createSubject = () => {
const observers = new Map();
const addObserver = (key, observer) => {
if (!observers.has(key)) {
observers.set(key, []);
}
observers.get(key).push(observer);
};
const removeObserver = (key, observer) => {
if (observers.has(key)) {
const filtered = observers.get(key).filter((obs) => obs !== observer);
observers.set(key, filtered);
}
};
const notify = (key, data) => {
if (observers.has(key)) {
observers.get(key).forEach((observer) => observer(data));
}
};
const subscribe = (key, callback) => {
addObserver(key, callback);
return () => removeObserver(key, callback);
};
return { notify, subscribe };
};
export const subject = createSubject();
// userCounterObserver.js
import { useState, useEffect } from "react";
import { subject } from "./Subject";
export const useCounterObserver = () => {
const [state, setState] = useState(0);
useEffect(() => {
const unsubscribe = subject.subscribe("counter", setState);
return () => unsubscribe();
}, []);
// 상태를 업데이트하고 구독자들에게 알림을 주는 함수
const increment = () => {
const newState = state + 1;
subject.notify("counter", newState); // 'counter' 키에 대해 알림
};
return [state, increment];
};
// App.jsx -> 관찰자/구독자
import React from "react";
import { useCounterObserver } from "./useCounterObserver";
const CounterComponent = () => {
const [count, increment] = useCounterObserver();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
const App = () => {
return (
<div>
<h1>React Observer Pattern with Closures</h1>
<CounterComponent />
<CounterComponent />
</div>
);
};
export default App;
발행-구독 패턴

옵저버 패턴에서 더 나아가 두 요소가 서로를 아예 모르게 만들 수 있는 방법이다.
옵저버 패턴에서는 구독이라는 행위때문에 어느정도의 결합도가 있을 수 밖에 없지만, 발행-구독 패턴은 발행자와 구독자 사이의 중재자 (메시지 브로커/이벤트버스)가 있어 서로를 모르게 설계할 수 있다!
즉, 직접적으로 연결하여 변화를 알리고 감지하는 것이 아니라 중재자를 통해 메시지를 전달하는 것이다.
발행자와 구독자는 왜 서로를 몰라야할까?
이는 유지보수성과 확장성과 연결되어있다!
1) 확장성: 발행자/구독자가 서로를 신경쓰지 않고 쉽게 새로운 것을 발행 or 새로운 것을 구독할 수 있다.
- 발행자는 구독자가 어떤 항목을 어떤 방식으로 구독하고있는지 고려하지 않고 새로운 항목을 발행할 수 있다.
- 구독자는 언제든지 구독/구독해제 하거나 새로 발행된 내용을 구독할 수 있다.

- 즉, 이렇게 새로운 topic을 추가하는 등의 과정이 쉬워진다!
2) 낮은 결합도: 서로에 대한 의존도를 없애 발행자 혹은 구독자의 변경이 서로에게 영향을 미치지 않는다.
- 발행자의 구현 방식이 변경됐더라도 그에 맞춰 구독자의 구현 방식을 변경할 필요가 없다는 의미!
- ex. fetch, Promise 등이 모두 양쪽의 구현 방식을 알지 못해도 동일한 방법으로 원하는 동작을 실행하는 것처럼!
3) 재사용성 증가: 발행자와 구독자가 독립적으로 설계되어 다양한 상황에 더 쉽게 재사용할 수 있다.
발행-구독 패턴의 특징
- 발행자와 구독자가 서로의 존재를 전혀 알지 못한다.
- 이벤트를 구독하고 발행하는 과정이 이벤트 버스에 모두 모여있어 응집도가 높다.
- 구독자가 더이상 변화를 감지할 필요가 없을 때, 이벤트 버스에 여전히 구독자의 참조가 남아있다면 메모리 누수로 이어질 수 있다. (명시적으로 구독 해제 과정 필요!)
- 옵저버 패턴과 발행-구독 패턴 모두 동일하게 결합도가 높지 않아 에러 발생시 추적과 디버깅이 어려울 수 있다
구현 예시

// EventBus.js
const EventBus = {
// 구독 목록
listeners: {},
// 구독자 등록 (구독자가 사용)
// 지금 구독하려는 이벤트가 우선 구독자 목록의 키로 존재하는지 확인
// 존재하지 않는 경우 지금 동작이 첫 구독이므로 event: [] 형태의 요소를 하나 추가해준다
// 그 후 event: [구독자에서 변경된 값을 받아 사용하기 위한 callback] 형식으로 구독을 추가해준다
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
},
// 구독 해제 (구독자가 사용)
// 어떤 이벤트에 대한 어떤 callback을 제거할지 매개변수로 전달
// 구독 목록에 구독 해제하려는 이벤트가 있는지 확인한다 없는 경우 구독이 없으므로 별도의 처리 하지 않음
// 이벤트가 갖고있는 요소들에서 callback을 필터하여 구독 해제한다
// ex. event: [callback1, callback2] -> event: [callback2] 라면 callback1이 구독 취소한 것
off(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event].filter(
(listener) => listener !== callback,
);
}
},
// 이벤트 발행 (발행자가 사용)
// 이벤트와 그 이벤트로 변경이 발생한 값을 매개변수로 전달
// 구독 목록에 이벤트가 있는 경우, 그 이벤트를 구독하고있는 구독자들이 있다는 의미이므로 등록돼있는 callback 목록을 순회하면서 데이터를 전달해줌
// 구독 목록(listeners)에 이벤트가 없는 경우 아직 그 이벤트를 구독하고있는 구독자가 없다는 의미이므로 별도의 처리하지 않음
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach((callback) => {
callback(data);
});
}
},
};
export default EventBus;
import EventBus from "./EventBus.js";
import { useState } from "react";
// 새로운 과제를 입력바다 EventBus를 통해 발행한다.
const Publisher = () => {
const [formValue, setFormValue] = useState({ deadline: "", details: "" });
const handleInput = (e) => {
setFormValue({ ...formValue, [e.target.name]: e.target.value });
};
const handlePublish = (e) => {
e.preventDefault();
EventBus.emit("taskPublish", formValue);
setFormValue({ deadline: "", details: "" });
};
return (
<div>
<h1>과제 제출하기</h1>
<form onSubmit={handlePublish}>
<input
placeholder="과제 마감기한"
value={formValue.deadline}
name="deadline"
onChange={handleInput}
/>
<input
placeholder="과제 내용"
value={formValue.details}
name="details"
onChange={handleInput}
/>
<button>과제 제출하기</button>
</form>
</div>
);
};
export default Publisher;
import { useEffect, useState } from "react";
import EventBus from "./EventBus.js";
const Subscriber = () => {
const [tasks, setTasks] = useState([]);
useEffect(() => {
// 구독하는 이벤트 발생시 호출할 콜백함수
const callback = (newTask) => {
setTasks((prevTasks) => [...prevTasks, newTask]);
};
// 구독
EventBus.on("taskPublish", callback);
// 언마운트시 구독 해제
return () => {
EventBus.off("taskPublish", callback);
};
}, []);
return (
<div>
<h1>나의 과제 목록</h1>
{tasks.map((task, index) => {
return (
<div key={`${Date.now() + index}`}>
<h3>과제 {index}</h3>
<p>마감기한: {task.deadline}</p>
<p>내용: {task.details}</p>
</div>
);
})}
</div>
);
};
export default Subscriber;
머리속으로는 알겠는데 직접 구현하려니 어려워서 하나하나 주석으로 동작에 대한 설명을 추가했다..
구현 중 헤맸던 부분


문제
새로운 과제를 등록했을 때 배열의 뒤에 추가되는 것이 아니라 마지막 요소가 삭제되고 추가되는 현상이 발견됨
기존 코드
구독자에서 추가된 데이터를 처리하는 부분
useEffect(() => {
const callback = (newTask) => {
setTasks([...tasks, newTask]);
};
EventBus.on("taskPublish", callback);
return () => {
EventBus.off("taskPublish", callback);
};
}, []);
시도한 것

코드 수정 등의 영향으로 다시 렌더링됐을 때 등록돼있는 task 외에 이후 Publisher의 발행으로 등록되는 새로운 과제는 추가되지 않는 것을 확인함
개발자도구로 확인해보니 클로져로 tasks가 등록돼있는 것에서 힌트를 얻음

지금 코드에서 구조분해할당으로 펼쳐지는 tasks는 초기값인 빈 배열이고, callback이 선언될 때의 tasks인 []가 클로져로 유지되고 있는 건지 추측함
const callback = (newTask) => {
setTasks((prevTasks) => [...prevTasks, newTask]);
};
state 업데이트 방식을 함수형 업데이트로 변경!
완성!

알맞게 동작하는 것을 확인했다! callback 내부의 tasks는 선언 당시의 tasks 초기값인 []이기 때문에 그 값이 계속 유지되었던 것!
참고자료
[Unity/디자인 패턴] 옵저버 패턴 & 발행-구독 패턴
게임 개발을 하다보면 어떤 행위가 일어났을 때 다른 객체에게 알림이 가게 하는 시스템이 필요할 때가 많다. 예를 들어 한 게임에서 몬스터가 플레이어의 공격을 맞아 처치하는 이벤트가 발생
velog.io
[번역] 초보 프론트엔드 개발자들을 위한 Pub-Sub(Publish-Subscribe) 패턴을 알아보기
비동기 자바스크립트 코드를 덜 괴롭게 이해하는 방법
rinae.dev