Discover us

About us

Projects

Blog

Events

Members

Development Blog

GDGoC CAU 개발자와 디자이너의 작업 과정과
결과물을 공유하는 공간입니다.

어떻게 프로젝트를 시작하게 되었고,
진행하면서 느낀 개발자와 디자이너의
생생한 스토리를 직접 확인해보세요!

Development

Observer 패턴

  • #Front-End
  • Junghyun Song
  • 2024. 11. 1.

Observer 패턴

Observer 패턴이란?

객체 간의 일대다(1:N) 관계에서 한 객체의 상태가 변화할 때, 그 객체에 의존하는 다른 객체들이 알림을 받아 변화를 반영할 수 있게 하는 패턴
💡
구독한 객체에게 이벤트가 발생할 때마다 관찰자가 알림을 받을 수 있다.
 
  1. Subject / Observable (주체) : 상태를 가지고 있으며, 관찰자에게 상태 변화를 통지하는 객체
      • 관찰자 등록/제거 메서드 가짐 - subscribe(), unsubscribe()
      • 상태가 변경되면 모든 관찰자에게 변화를 알림 - notify()
  1. Observer (관찰자) : 주체의 상태 변화를 수신하여 처리하는 객체
      • 관찰 대상이 업데이트 되면 그에 반응하는 메서드 정의
 
// Subject 객체 정의 class Subject { 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)); } }
Subject 객체를 정의했다. 관찰자들의 배열을 가지고, 관찰자 등록/제거/알림전송 3가지 메서드를 가진다.
 
export default function App(){ return ( <div className="App"> <Button>Click me!</Button> <FormControlLabel control={<Switch />} /> </div> ) }
Button, Switch 컴포넌트를 가진 App을 만들었다. 사용자가 버튼을 클릭하거나 스위치를 토글할 때마다 타임스탬프를 기록하고 토스트 알림을 화면에 노출할 것이다.
 
import {ToastContainer, toast} from 'react-toastify' function logger(data){ // 타임스탬프 기록 console.log(`${Date.now()} ${data}`); } function toastify(data){ // 토스트 알림 toast(data); } const subject = new Subject(); // subject 인스턴스 생성 subject.subscribe(logger); // logger 함수를 관찰자로 등록 subject.subscribe(toastify); // toastify 함수를 관찰자로 등록 export default function App(){ return ( <div className="App"> <Button>Click me!</Button> <FormControlLabel control={<Switch />} /> <ToastContainer /> </div> ) }
Subject의 notify()로부터 data를 받게 될 logger, toastify 함수를 만들었다. 이 두 함수가 observer로써 작동하기 위해 subject 인스턴스를 생성하고 subscribe()를 사용했다. 이제 이벤트가 발생할 때마다 두 함수는 토스트 알림을 받게된다.
 
export default function App(){ function handleClick(){ subject.notify("User cliked button!"); } function handleToggle(){ subject.notify("User toggled switch!"); } return ( <div className="App"> <Button variant="contained" onClick={handleClick}> Click me! </Button> <FormControlLabel control={<Switch onChange={handleToggle} />} label="Toggle me!" /> <ToastContainer /> </div> ); }
버튼 클릭 handleClick, 스위치 토글 handleToggle 이벤트 핸들러를 만들고 그 안에서 nofify()를 실행한다. 이렇게 이벤트가 발생하면 Subject의 notify는 observer인 logger, toastify 함수에게 data를 인자로 넘긴다.
전체 플로우는 아래의 영상과 같다.
 
위 예제의 실행 결과는 다음과 같다.
 

활용예제

뉴스 구독 시스템

  • NewsSubject : Subject 역할. 뉴스 주제
  • NewsSubscriber : Observer 역할. 구독자
Observer가 함수가 아닌 사람인 경우! 더 직관적
// Subject 클래스 정의 class NewsSubject { constructor() { this.observers = []; // 구독자 목록 } // 구독자 추가 subscribe(observer) { this.observers.push(observer); } // 구독자 제거 unsubscribe(observer) { this.observers = this.observers.filter(sub => sub !== observer); } // 모든 구독자에게 알림 전송 notify(news) { this.observers.forEach(observer => observer.update(news)); } } // Observer 클래스 정의 class NewsSubscriber { constructor(name) { this.name = name; } // 뉴스 업데이트 시 호출될 메서드 update(news) { console.log(`${this.name} received news: ${news}`); } } // 주제 인스턴스 생성 const newsSubject = new NewsSubject(); // 구독자 인스턴스 생성 const subscriber1 = new NewsSubscriber("Subscriber 1"); const subscriber2 = new NewsSubscriber("Subscriber 2"); // 구독자 등록 newsSubject.subscribe(subscriber1); newsSubject.subscribe(subscriber2); // 뉴스 업데이트 (구독자들에게 알림 전송) newsSubject.notify("Breaking News: Observer Pattern in Action!"); // 구독자 제거 후 알림 테스트 newsSubject.unsubscribe(subscriber1); newsSubject.notify("Another Update: Only Subscriber 2 receives this.");
예제 실행결과
Subscriber 1 received news: Breaking News: Observer Pattern in Action! Subscriber 2 received news: Breaking News: Observer Pattern in Action! Subscriber 2 received news: Another Update: Only Subscriber 2 receives this.
 
 

사례분석

RxJS : Observer 패턴을 구현한 유명 오픈소스 라이브러리
Reactive X는 Observer 패턴, 이터레이터 패턴, 함수형 프로그래밍을 조합하여 이벤트의 순서를 이상적으로 관리할 수 있다.
RxJS 를 사용하면 Subject를 따로 정의하지 않아도 Subject, Observer를 만들어낼 수 있다. 공식 문서의 예제로 사용자가 문서를 드래그 중인지 아닌지 콘솔에 출력해주는 것을 확인할 수 있다.
예제코드
import React from "react"; import ReactDOM from "react-dom"; import { fromEvent, merge } from "rxjs"; import { sample, mapTo } from "rxjs/operators"; import "./styles.css"; merge( // mousedown : 마우스 클릭 이벤트 fromEvent(document, "mousedown").pipe(mapTo(false)), // mousemove : 마우스 드래그 이벤트 fromEvent(document, "mousemove").pipe(mapTo(true)) ) // mouseup : 마우스 버튼을 누르고 있다가 떼는 순간 이벤트 .pipe(sample(fromEvent(document, "mouseup"))) // 드래그 여부를 구독 .subscribe(isDragging => { console.log("Were you dragging?", isDragging); }); ReactDOM.render( <div className="App">Click or drag anywhere and check the console!</div>, document.getElementById("root") );
 
 

장점

  1. 느슨한 결합 (Loose Coupling) : 주체와 관찰자가 직접적으로 서로를 참조하지 않고 순수하게 상태 변경에만 집중하면 된다. ⇒ 결합도가 낮아지고 유지보수가 쉬워진다
  1. 확장성 : 새로운 관찰자 추가 또는 제거가 간단하다.
  1. 실시간 업데이트 : 주체의 상태가 변경되어 실시간 데이터 업데이트가 필요한 경우에 유용하게 활용된다.
  1. 재사용성 : 주체와 관찰자가 각각 독립적으로 재사용 될 수 있다.

단점

  1. 성능 저하 : 관찰자가 많아지면 성능이 저하될 수 있다.