서론
최근 진행했던 고객사의 프로젝트에서 16개의 컴포넌트에서 데이터를 취합하여 API 요청을 해야 하는 상황이 있었다.
기존 프로젝트에 비슷하게 구현된 코드베이스가 있어 그걸 참고해서 구현을 하던 와중에 Pub-Sub Pattern을 사용한걸 발견했다.
컴포넌트 페이지마다 데이터를 취합하여 다음 페이지로 넘어갈때, 최상단의 커스텀훅스에 선언한 핸들링 함수에
Pub-Sub Pattern을 사용하여 페이지 이동/데이터 핸들링 이벤트처리를 하여 결합도를 낮추고, 응집도를 높인 코드였다.
몇년동안 외주 프론트엔드 개발을 해왔지만, 처음 봤기때문에 내겐 꽤 신선한 충격(?) 이었다.
나라면 처음부터 이렇게 설계할 생각을 할수 있을까? 라는 생각이 들었다.
그리고 Pub-Sub Pattern을 모른다면, 이해 할수가 없는 코드기도 했다.
그리하여 어렴풋이 알고 있던 나의 지식을 조금 더 짙게 만들자 공부해보고 기록하기로 결심했다.
본론
우리가 개발을 하면서 자주 사용하는, addEventListener / Promise / useState, props / Fetch / webSocket / RxJS 등의 API들이 Pub-Sub패턴 / 옵저버 패턴과 유사하게 구현이 되어져있다.
Pub-Sub 패턴을 사용하면 컴포넌트 간에 상태를 전파하거나, 비동기 작업 완료 시 다른 부분에 알림을 보내는 등의 다양한 상황에서 활할 수 있다.
Pub-Sub 패턴
- Publisher(발행자)와 Subscriber(구독자) 사이의 중간 매개체인 Broker(브로커)를 사용한다.
- Publisher는 특정 주제나 토픽에 이벤트를 발행하고, Broker는 해당 이벤트를 구독하는 모든 Subscriber에게 발행된
내용을 전달한다.
- Publisher와 Subscriber는 서로에 대해 알 필요가 없고, 중간에 Broker를 통해 통신한다.
- 주제나, 토픽을 중심으로 이벤트를 발행하고, 해당 주제를 구독하는 모든 구독자에게 전달한다.
지도에 마커를 표시하는 웹을 구현해보는 포스팅을 직접해보면서 이해했다.
<!-- map.html -->
<!DOCTYPE html>
<html>
<head>
<title>My Favorite Places</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="sidebar">
<h1>My fav places on earth v1.0</h1>
<!-- footer를 여기에 둔다 -->
<div class="cities">
<b>Added cities:</b>
<div id="citiesList"></div>
</div>
</div>
<div class="main-content-area">
<div id="map"></div>
</div>
<script src="https://maps.googleapis.com/maps/api/js?key=API키넣어주세요."></script>
<script src="map.js" type="module"></script>
<script src="sidebar.js" type="module"></script>
</body>
</html>
/* map.js */
let googleMap;
import { addPlace, getPlaces, subscribe } from "./dataService.js";
function renderMarkers(placesArray) {
googleMap.markerList.forEach((m) => m.setMap(null)); // 모든 마커 제거
googleMap.markerList = [];
// myPlaces 배열의 요소를 기반으로 마커를 추가한다
placesArray?.forEach((place) => {
const marker = new google.maps.Marker({
position: place.position,
map: googleMap,
});
googleMap.markerList.push(marker);
});
}
function init() {
googleMap = new google.maps.Map(document.getElementById("map"), {
center: { lat: 0, lng: 0 },
zoom: 3,
});
googleMap.markerList = [];
googleMap.addListener("click", addMarker);
}
function addMarker(event) {
addPlace(event.latLng);
// addMarket를 하면, 리렌더링이 돌아야한다.
// 여기서 renderMarkers(getPlaces()) 를 해줄필요가 없어진다.
// subscribe(renderMarkers)를 dataService함수의 addPlace 에서 publish를 호출하기에 가능하다.
// 즉 발행자함수에서 구독자들의 callbackFunction(여기서는 renderMarkers)을 호출하기때문에 가능한것이다.
}
init();
renderMarkers(getPlaces());
subscribe(renderMarkers); // 구독자 callbackFunction 등록
/* sidebar.js */
import { getPlaces, subscribe } from "./dataService.js";
function renderCities(placesArray) {
// 도시 목록을 표현하기 위한 DOM 엘리먼트를 가져온다
const cityListElement = document.getElementById("citiesList");
// 먼저 클리어 하고
cityListElement.innerHTML = "";
// forEach 함수를 써서 하나씩 다시 리스트를 그려낸다.
// getPlaces 함수 호출로 얻은 placesArray로 교체
placesArray.forEach((place) => {
const cityElement = document.createElement("div");
cityElement.innerText = place.name;
cityListElement.appendChild(cityElement);
});
}
renderCities(getPlaces());
subscribe(renderCities); // 구독자 callbackFunction 등록
/* dataService.js */
let myPlaces = [];
const geocoder = new google.maps.Geocoder();
// 함수를 받도록 배열로 처리
let changeListeners = [];
/**
*
* @param {*} callbackFunction
* @description 구독자 함수
*/
export function subscribe(callbackFunction) {
changeListeners.push(callbackFunction);
}
/**
*
* @param {*} data
* @title 발행자 함수
* @description
* sidebar / map 함수내에서 subscribe를 함수를 실행한다.
* subsribe(구독자) 함수의 인자는 publish 될때, 실행할 함수이다.
* sidebar에서는 renderCities 이고, map에서는 renderMarkers 이다.
* publish(발행자) 가 될때, renderCities함수와 renderMarkers함수에서 사용될 첫번째, 인자값인 myPlaces를 넘긴다.
* publisher(발행자) 함수에서는 changeListeners를 순회하면서,
* publish에 인자로 넘긴 myPlaces를 changeListeners배열에 담겨있는 renderCities, renderMarkers 함수 인자로 넘겨 실행(호출)한다.
* 이로써 renderCities / renderMarkers 함수가 실행되면서,
* 도시리스트 최신화 / 마커리스트 최신화가 실행이 된다.
* 이렇게 함으로써 dataService.js에서는 데이터만 핸들링한다.
* map.js에서는 지도를 그리기만 한다.
* sidebar.js에서는 마커를 그리기만 한다.
* 결과적으로 관심사가 분리되고 코드의 결합도는 낮아진다.
*/
function publish(data) {
changeListeners.forEach((changeListener) => changeListener(data));
}
export function addPlace(latLng) {
// Google API 를 실행하여 도시 이름을 검색한다.
// 두 번째 인자는 요청한 결과에 따른 응답이 왔을 때 처리를 담당하는 콜백 함수
geocoder.geocode({ location: latLng }, function (results) {
try {
// 콜백 안에서 결과에 따른 도시 이름을 추출한다
const cityName = results.find((result) =>
result.types.includes("locality")
).address_components[0].long_name;
// 그리고 우리가 준비해놓은 변수에 집어넣는다
myPlaces.push({ position: latLng, name: cityName });
publish(myPlaces);
// 그 다음 localStorage와 동기화한다
localStorage.setItem("myPlaces", JSON.stringify(myPlaces));
} catch (e) {
// 도시를 찾을 수 없을 때 콘솔에 메세지를 출력한다
console.error("No city found in this location! :(");
}
});
}
// localStorage에 있는 정보를 꺼내 콜렉션에 넣는 함수
function initLocalStorage() {
const placesFromLocalStorage = JSON.parse(localStorage.getItem("myPlaces"));
if (Array.isArray(placesFromLocalStorage)) {
myPlaces = placesFromLocalStorage;
publish(); // 지금은 만들어지지 않은 함수. 나중에 적용될 예정
}
}
// 현재 가지고 있는 장소의 목록을 출력
export function getPlaces() {
return myPlaces;
}
initLocalStorage();
body {
display: flex;
}
.main-content-area {
width: 100%;
}
#map {
height: 100vh;
}
p.s
js파일 module 임포트를 하면 cors에러가 발생해, http-server를 npm으로 받아 로컬에서 실행했다.
Reference
Why every beginner front-end developer should know publish-subscribe pattern?
AKA: How to implement asynchronous code in a less painful way.
itnext.io
[번역] 초보 프론트엔드 개발자들을 위한 Pub-Sub(Publish-Subscribe) 패턴을 알아보기
비동기 자바스크립트 코드를 덜 괴롭게 이해하는 방법
www.rinae.dev
'JavaScript > JavaScript' 카테고리의 다른 글
| SolidJS 살펴보기-1 (0) | 2024.07.04 |
|---|---|
| 창, 탭, 프레임, iframe 간의 통신 / Broadcast Channel API (2) | 2024.03.16 |
| JS 스택(Stack) (0) | 2023.12.07 |
| RxJS Observable / of / from / lastValueFrom / concatMap, toArray (1) | 2023.11.24 |
| JavaScript call apply bind 살펴보기 (0) | 2023.05.10 |