옵저버 패턴과 프록시는 서로 다른 패턴이지만, 종종 같이 사용되기도 한다. 프록시는 객체의 동작을 제어하거나 객체에 접근할 때 추가적인 동작을 수행하기 위해 사용되고, 옵저버 패턴은 객체의 상태 변화를 감지하고 상태 변화에 따른 추가적인 동작을 수행하기 위해 사용된다.
이제 모달창과 overlay를 프록시와 옵저버 패턴을 이용해서 전역상태 관리를 진행해 보자.
먼저, 프록시를 이용해서 state
라는 전역 상태 객체를 만든다. 이 객체에는 모달창이나 overlay와 같은 컴포넌트들의 상태를 저장한다.
// store.js
export const state = new Proxy(
{
modalVisible: false,
overlayVisible: false
},
{
// state의 속성값이 변경될 때마다 호출되는 set 메소드
set(target, key, value) {
// 속성값 변경
target[key] = value;
// 속성값 변경을 감지하는 옵저버들에게 알림
notifyObservers(key, value);
// 속성값 변경이 완료된 후 true를 반환
return true;
}
}
);
위 코드에서 state
라는 Proxy 객체를 만들었으며, 이 객체에는 modalVisible
과 overlayVisible
이라는 두 개의 상태를 저장한다.
다음으로 Proxy 객체를 만들 때, set
핸들러를 추가했는데 이 핸들러는 프록시 객체의 속성이 변경될 때마다 호출된다. 이 핸들러에서는 변경된 속성의 이름과 값을 옵저버에게 알리는 notifyObservers
함수를 호출한다.
다음으로, 옵저버 패턴을 구현한다. 옵저버 패턴을 구현하기 위해서는 일반적으로 옵저버를 등록하고 삭제하는 함수와 옵저버에게 알리는 함수가 필요하다.
const observers = new Map();
function addObserver(key, observer) {
if (!observers.has(key)) {
observers.set(key, []);
}
observers.get(key).push(observer);
}
// export function removeObserver(key, observer) {
// // 옵저버 리스트가 존재하는지 확인
// if (observers.has(key)) {
// const observerList = observers.get(key);
// // 해당 옵저버가 존재하는 인덱스를 찾아 제거
// const index = observerList.indexOf(observer);
// if (index !== -1) {
// observerList.splice(index, 1);
// }
// }
// }
function notifyObservers(key, value) {
if (observers.has(key)) {
const observerList = observers.get(key);
observerList.forEach(observer => observer(value));
}
}
위 코드에서는 observers
라는 맵을 만들어서 옵저버를 저장한다. addObserver
함수는 observers
맵에 옵저버를 등록하는 함수이다. 만약 해당 속성에 등록된 옵저버가 없다면, 새로운 배열을 만들어서 옵저버를 추가한다.
removeObserver
함수는 observers
맵에서 옵저버를 삭제하는 함수이다. notifyObservers
함수는 set
핸들러에서 호출되는 함수로, 해당 속성에 등록된 옵저버에게 변경된 값을 알려준다.
하지만 removeObserver
는 React와 같은 라이브러리나 프레임워크에서 사용되는 개념이기 때문에, 바닐라 자바스크립트에서는 필요하지 않기 때문에 삭제 하도록 한다.
결과적으로 store.js
파일에서 프록시와 옵저버 패턴을 이용해서 전역 상태 관리를 구현하고, Overlay.js
, LoginModal.js
, Header.js
파일에서 해당 상태를 사용하도록 구현할 수 있다.
우선 app.js
에 옵저버 함수가 관찰하는 state
의 객체 중에서 modalVisible
와 overlayVisible
가 true일때는 모달창과 overlay
가 보이도록, false
일 때는 보이지 않는 로직을 구현한다. app.js
에서 구현하는 이유는 모달창과 overlay는 프로젝트 전역에 걸쳐서 사용될 가능성이 크기 때문이다. 이렇게 하면 state
가 변경될때마다 그에 맞는 로직이 프로젝트 전반에 걸쳐 실행된다.
// src/components/app.js
...
import { addObserver } from '../../store';
const App = () => {
const router = Router();
const renderApp = () => {
...
const observer = value => {
if (value) {
loginModal.classList.remove('hidden');
overlay.classList.remove('hidden');
document.body.classList.add('no-scroll');
} else {
loginModal.classList.add('hidden');
overlay.classList.add('hidden');
document.body.classList.remove('no-scroll');
}
};
addObserver('modalVisible', observer);
addObserver('overlayVisible', observer);
};
const onPopState = () => {
renderApp();
};
...
const app = { renderApp };
return app;
};
export default App;
import './Header.scss';
...
import { state } from '../../../store';
const Header = () => {
...
...
profileContainer.addEventListener('click', () => {
moreInfo.classList.toggle('hidden');
});
loginTab.addEventListener('click', () => {
state.modalVisible = true;
state.overlayVisible = true;
});
signupTab.addEventListener('click', () => {
state.modalVisible = true;
state.overlayVisible = true;
});
return header;
};
export default Header;
import './LoginModal.scss';
import { state } from '../../../store';
const LoginModal = () => {
...
window.addEventListener('click', event => {
if (
event.target === overlay ||
event.target.closest('.container-login-modal') === null
) {
state.modalVisible = false;
state.overlayVisible = false;
}
});
return loginModal;
};
export default LoginModal;
import './Overlay.scss';
import { state } from '../../../store';
const Overlay = () => {
const app = document.getElementById('app');
const overlay = document.createElement('div');
overlay.classList.add('overlay', 'hidden');
app.appendChild(overlay);
overlay.addEventListener('click', () => {
state.modalVisible = false;
state.overlayVisible = false;
});
return overlay;
};
export default Overlay;