자바스크립트 파일을 작성하게 되면, 하나의 전역 상태 값을 여러 개의 자바스크립트 파일이 공유하거나 참조해야 하는 경우가 존재한다. 예를 들어서 여러 개의 페이지로 구성된 웹 서비스가 있다고 가정해보자. 해당 웹 서비스의 한 개의 페이지에서 다크 모드를 적용하게 되면, 다른 웹 페이지 역시 다크 모드가 적용되는 기능을 제공한다고 한다고 해보자.
이 경우에는 각 페이지와 연결된 JS 파일이 하나의 상태 값을 공유해서, 각 페이지가 랜더링 될 때 동적으로 다크 모드가 적용되도록 해야 한다. 이를 위해서 전역 상태를 적절히 관리하는 것이 필요하며, 전역 상태가 변경될 경우 해당 상태를 참조하고 있는 옵저버(Observer)들에게 상태가 변경되었음을 알려주는 디자인 패턴을 옵저버 패턴이라고 한다.
이번 글을 통해 옵저버 패턴에 대해 자세히 알아보고, 자바스크립트에서 제공하는 클로저를 통해 옵저버 패턴을 구현해보고자 한다.
위키 피디아에 따르면 옵저버 패턴의 핵심은 옵저버 또는 리스너(listener)라 불리는 하나 이상의 객체를 관찰 대상이 되는 객체에 등록시켜 각각의 옵저버들이 관찰 대상인 객체가 발생시키는 이벤트를 받아 처리한다.
즉, 어떤 객체의 상태를 다른 객체에게 알려 해당 객체가 변경된 상태값을 인지하고 동작을 처리하도록 알려주는 것을 의미한다. 이러한 디자인 패턴은 이 상태값이 변경될 때, 상태값을 참조하고 있는 옵저버 객체들이 변경된 상태값을 누락없이 인지하도록 만들 수 있다는 장점이 있다.
일반적으로 브라우저에서 상태값을 저장하기 위해서 쿠키 혹은 로컬 스토리지를 사용한다. 로컬 스토리지는 지난 포스팅에서 언급했듯 쿠키와 달리 만료 시점을 사용자가 명시하지 않을 경우 브라우저가 종료되어도 유지되기 때문에 랜더링 될 때 클라이언트 브라우저의 상태값을 저장하기 용이하다고 했다.
그러나 포스팅 서두에서 언급한 것과 같이 페이지 간 전환 시, 해당 페이지가 다크 모드 여부를 판단하기 위해서 매번 로컬 스토리지를 참조하는 것이 적절할까?
물론 서버에 매번 데이터를 요청하는 것은 아니지만 개인적으로 로컬 스토리지에 매번 직접 값을 참조하거나 업데이트 하는 것은 데이터의 상태를 안전하게 유지할 수 없다고 생각한다.
로컬 스토리지는 전역 객체에 등록된 객체로 어디에서든 값을 참조하거나 저장할 수 있기 때문에 의도치 않게 코드 중간에 해당 값을 변경하게 될 경우 로컬 스토리지에 있는 값을 참조하는 다른 객체들에게 영향을 주게 된다. 여기서 중요한 것은 의도치 않은 상태값의 변경으로 에러가 발생하게 될 경우 이를 찾거나 유지 보수 하는 것은 굉장히 어려울 것이다.
따라서 상태값은 초기 랜더링 시 이를 전역 객체에 저장하고, 해당 상태값을 참조하거나 변경할 수 있는 권한을 특정 함수에게 위임하는 방법이 더 안전하다고 생각한다.
클로저는 상위 스코프의 식별자를 참조하면서 상위 함수보다 생명주기가 더 긴 함수를 의미한다. 클로저는 이런 특성 때문에 일반적으로 상태를 안전하게 저장하거나 참조하기 위해 많이 사용된다.
예를 들어 위에서 언급한 다크 모드를 예시로 살펴보자.
// state.mjs
const themeState = (()=>{
let theme = localStorage.getItem('theme');
return {
getTheme () {
return theme;
}
setTheme(newState) {
theme = newState;
localStorage.setItem('theme', newState);
}
}
})();
로컬스토리지에 저장된 theme 정보를 theme 식별자에 저장하고 이를 참조하거나 값을 변경할 수 있는 클로저 함수 getTheme과 setTheme 메서드를 갖는 객체를 themeState에 반환하였다.
위 코드는 즉시 실행함수로 처음 코드가 평가되어 단 1번만 실행되게 된다. 이 때 즉시 실행 함수의 렉시컬 환경에 식별자 theme이 저장되게 되며, 해당 식별자는 getTheme과 setTheme 메서드만 참조할 수 있으므로 theme 상태값은 외부에서 함부러 변경할 수 없게 된다.
위의 코드를 통해 옵저버 패턴으로 응용해보도록 하자.
const themeState = (()=>{
let theme = localStorage.getItem('theme');
const observerCollection = [];
return {
getTheme () {
return theme;
},
setTheme(newState) {
theme = newState;
localStorage.setItem('theme', newState);
this.notifyAll();
},
registerObserver(observer){
if(!observerCollection.includes(observer))
observerCollection.push(observer);
},
unregisterObserver(observer){
if(observerCollection.indexOf(observer) !== -1)
observerCollection.splice(observerCollection.indexOf(observer), 1);
},
notifyAll(){
observerCollection.forEach(observer => observer.theme = theme);
}
}
})();
const observer = {
theme : 'white',
notify() {
themeState.setTheme(this.theme);
}
}
옵저버 패턴을 구현하기 위해 다음과 같은 기능을 구현했다.
ObserverCollection : 상태값을 참조하고 있는 옵저버들의 목록을 관리하기 위한 객체
registerObserver : observerCollection에 옵저버 목록을 등록하는 메서드
unregisterObserver : observerCollection에 옵저버 목록을 삭제하는 메서드
notifyAll : state 값의 상태가 변경될 경우, 등록된 observer 객체의 상태값을 변경해주는 역할.
각 옵저버는 자신의 상태값(테마 정보)을 가지고 있으며, 자신의 상태값이 변경되면(즉, 자신의 테마 정보가 변경되면 이를 다른 옵저버에게 이를 알려 다른 객체 역시 동일한 테마 정보를 갖도록) 상태값을 notify() 메서드를 통해 상태값을 업데이트 합니다.
상태값의 변경은 클로저 함수 setTheme () 메서드를 호출하여 상태값을 변경한다. setTheme() 메서드는 상태값을 업데이트하고, 옵저버 컬렉션에 저장된 모든 옵저버 객체의 상태값을 notifyAll() 메서드를 호출하여 변경한다.
물론 위의 코드가 좋은 코드라고 확신할 수 없으며, 아직 배우는 입장이다 보니 해당 코드의 수정이 필요할 수 있다.
하지만 옵저버 패턴을 공부하면서 결국 옵저버 패턴의 핵심은 하나의 전역 상태를 여러 객체가 참조할 경우, 상태가 변경되었을 때 누락없이 해당 상태의 변경을 모든 객체가 반영할 수 있도록 만들어 주는 패턴이라고 생각했다.
이외에도 모듈을 통해 상태값을 관리는 방법 등 전역 상태를 관리하는 다양한 방법이 존재하며, 앞으로 이에 대해 관심을 가지고 공부가 필요하다는 점을 알게 되었다.