jblog
Observer 패턴
ArchitectureGuru 디자인 패턴 #18

Observer 패턴

상태 변화를 구독자들에게 자동으로 알려주는 Observer 패턴을 알아봅니다.

2026-01-2310 min readdesign-pattern, behavioral, architecture

#유튜브를 구독하면 벌어지는 일

좋아하는 유튜브 채널이 있다고 해보자.

새 영상이 올라왔는지 확인하려고 매시간 채널에 들어가서 확인한다면?

새로고침 지옥

미친 짓이다. 그래서 우리는 구독 버튼을 누른다.

구독을 하면 새 영상이 올라올 때 알림이 자동으로 온다. 내가 확인하러 갈 필요가 없다. 채널(발행자)이 나(구독자)에게 알려주는 것이다.

이게 바로 Observer 패턴이다. 그리고 프론트엔드 개발에서 가장 중요한 패턴 중 하나라고 감히 말할 수 있다.

visible: false

Observer란?

객체의 상태가 변할 때, 그 객체를 구독하고 있는 모든 객체에게 자동으로 알려주는 패턴

핵심 구조는 단순하다.

  1. Subject (발행자) — 상태를 가지고 있고, 구독자 목록을 관리한다
  2. Observer (구독자) — Subject의 상태 변화를 통보받는다

Subject의 상태가 바뀌면, 등록된 모든 Observer에게 "야, 바뀌었어!" 하고 알려준다.

visible: false

코드로 보기

기본 Event Emitter

가장 직관적인 형태부터 만들어보자.

// Observer 인터페이스
interface Observer<T> {
  update(data: T): void;
}
 
// Subject 인터페이스
interface Subject<T> {
  subscribe(observer: Observer<T>): void;
  unsubscribe(observer: Observer<T>): void;
  notify(data: T): void;
}
 
// 이벤트 발행자
class EventEmitter<T> implements Subject<T> {
  private observers: Set<Observer<T>> = new Set();
 
  subscribe(observer: Observer<T>): void {
    this.observers.add(observer);
    console.log(`구독자 추가! 현재 ${this.observers.size}명`);
  }
 
  unsubscribe(observer: Observer<T>): void {
    this.observers.delete(observer);
    console.log(`구독 해제. 현재 ${this.observers.size}명`);
  }
 
  notify(data: T): void {
    this.observers.forEach((observer) => observer.update(data));
  }
}

유튜브 채널 시뮬레이션

interface Video {
  title: string;
  duration: number;
}
 
// 유튜브 채널 (Subject)
class YouTubeChannel extends EventEmitter<Video> {
  private channelName: string;
 
  constructor(name: string) {
    super();
    this.channelName = name;
  }
 
  uploadVideo(title: string, duration: number): void {
    const video: Video = { title, duration };
    console.log(`\n📹 [${this.channelName}] 새 영상 업로드: "${title}"`);
    this.notify(video); // 모든 구독자에게 알림!
  }
}
 
// 구독자 (Observer)
class Subscriber implements Observer<Video> {
  constructor(private name: string) {}
 
  update(video: Video): void {
    console.log(
      `🔔 ${this.name}: "${video.title}" 알림 도착! (${video.duration}분)`,
    );
  }
}
 
// 사용
const channel = new YouTubeChannel("코딩하는 준범");
 
const sub1 = new Subscriber("김철수");
const sub2 = new Subscriber("이영희");
const sub3 = new Subscriber("박민수");
 
channel.subscribe(sub1);
channel.subscribe(sub2);
channel.subscribe(sub3);
 
channel.uploadVideo("Observer 패턴 10분 정리", 10);
// 📹 [코딩하는 준범] 새 영상 업로드: "Observer 패턴 10분 정리"
// 🔔 김철수: "Observer 패턴 10분 정리" 알림 도착! (10분)
// 🔔 이영희: "Observer 패턴 10분 정리" 알림 도착! (10분)
// 🔔 박민수: "Observer 패턴 10분 정리" 알림 도착! (10분)
 
channel.unsubscribe(sub2); // 이영희 구독 해제
 
channel.uploadVideo("Decorator 패턴 실전편", 15);
// 📹 [코딩하는 준범] 새 영상 업로드: "Decorator 패턴 실전편"
// 🔔 김철수: "Decorator 패턴 실전편" 알림 도착! (15분)
// 🔔 박민수: "Decorator 패턴 실전편" 알림 도착! (15분)

구독자가 늘어도 줄어도 채널(Subject)의 코드는 변하지 않는다.

알림 폭주

visible: false

더 실용적인 예시: 상태 관리 Store

프론트엔드에서 가장 많이 쓰는 형태는 Store다. Zustand, Redux의 핵심이 이것이다.

type Listener<T> = (state: T) => void;
 
class Store<T> {
  private listeners: Set<Listener<T>> = new Set();
  private state: T;
 
  constructor(initialState: T) {
    this.state = initialState;
  }
 
  getState(): T {
    return this.state;
  }
 
  setState(updater: (prev: T) => T): void {
    this.state = updater(this.state);
    this.emit(); // 상태가 바뀌면 모든 리스너에게 알림
  }
 
  subscribe(listener: Listener<T>): () => void {
    this.listeners.add(listener);
    // 구독 해제 함수를 반환 — 이 패턴이 정말 중요하다
    return () => this.listeners.delete(listener);
  }
 
  private emit(): void {
    this.listeners.forEach((listener) => listener(this.state));
  }
}
 
// 사용
interface AppState {
  count: number;
  user: string | null;
}
 
const store = new Store<AppState>({ count: 0, user: null });
 
// 구독
const unsubscribe = store.subscribe((state) => {
  console.log(`카운트가 변경됨: ${state.count}`);
});
 
store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
// 카운트가 변경됨: 1
 
store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
// 카운트가 변경됨: 2
 
unsubscribe(); // 더 이상 알림 받지 않음
 
store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
// (아무 출력 없음)

subscribe구독 해제 함수를 반환하는 패턴을 주목하자. React의 useEffect 클린업, Zustand의 subscribe, RxJS의 unsubscribe가 모두 이 패턴을 따른다.

visible: false

사실 이미 매일 쓰고 있다

Observer 패턴이 낯설게 느껴질 수도 있지만, 프론트엔드 개발자라면 이미 매일 사용하고 있다.

1. addEventListener

// DOM 이벤트 = Observer 패턴 그 자체
const button = document.querySelector("#myButton");
 
// subscribe
button.addEventListener("click", (e) => {
  console.log("클릭됨!");
});
 
// 여러 Observer 등록 가능
button.addEventListener("click", logAnalytics);
button.addEventListener("click", playClickSound);
button.addEventListener("click", updateUI);

addEventListener는 Observer 패턴의 가장 원초적인 형태다. 버튼(Subject)을 클릭하면 등록된 모든 핸들러(Observer)가 호출된다.

2. React의 useState

function Counter() {
  const [count, setCount] = useState(0);
  //           ^^^^^^^^ 상태가 바뀌면 → 컴포넌트가 리렌더링(=알림)
 
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

setCount로 상태를 변경하면 React가 해당 컴포넌트를 자동으로 리렌더링한다. 이것이 바로 Observer 패턴이다. 상태(Subject)가 바뀌면 컴포넌트(Observer)가 반응하는 것이다.

3. RxJS Observable

import { fromEvent, debounceTime, map } from "rxjs";
 
// 검색창 입력을 구독
const search$ = fromEvent(searchInput, "input").pipe(
  debounceTime(300),
  map((e: Event) => (e.target as HTMLInputElement).value),
);
 
// Observer 등록
search$.subscribe((query) => {
  console.log(`검색어: ${query}`);
  fetchSearchResults(query);
});

RxJS는 Observer 패턴을 스테로이드 맞은 버전으로 확장한 것이다. 이벤트 스트림을 구독하고, 변환하고, 조합할 수 있다.

4. WebSocket

const ws = new WebSocket("wss://chat.example.com");
 
// 서버가 메시지를 보내면 → 자동으로 알림
ws.addEventListener("message", (event) => {
  const data = JSON.parse(event.data);
  appendMessage(data);
});
 
ws.addEventListener("open", () => console.log("연결됨"));
ws.addEventListener("close", () => console.log("연결 끊김"));

WebSocket도 마찬가지다. 서버(Subject)가 메시지를 보내면 클라이언트의 핸들러(Observer)가 호출된다.

모든 곳에 Observer

visible: false

언제 사용하면 좋을까?

  1. 한 객체의 변화가 다른 여러 객체에 영향을 줄 때

    • 상태 관리, 실시간 데이터, 이벤트 시스템
  2. 구독자를 런타임에 동적으로 추가/제거해야 할 때

    • 컴포넌트 마운트/언마운트 시 구독 관리
  3. 발행자가 구독자의 구체적인 타입을 알 필요 없을 때

    • 느슨한 결합이 필요한 모든 상황

visible: false

장단점

장점단점
Subject와 Observer 간 느슨한 결합구독 해제를 빠뜨리면 메모리 누수 발생
런타임에 구독 관계를 동적으로 변경 가능Observer가 많으면 알림 순서 예측이 어려움
OCP — 기존 코드 수정 없이 새 Observer 추가순환 의존성이 생기면 무한 루프 위험
이벤트 기반 아키텍처의 핵심디버깅 시 이벤트 흐름 추적이 까다로움

visible: false

정리하며

Observer는 상태 변화를 구독자에게 자동으로 알려주는 패턴이다.

핵심을 한 줄로 요약하면,

"변하면 알려줘. 내가 매번 확인하러 가지 않을게."

addEventListener, useState, subscribe, on — 프론트엔드에서 매일 만나는 이 API들이 전부 Observer 패턴 위에 세워져 있다. 이 패턴을 이해하면 프론트엔드의 반응형 프로그래밍이 왜 이렇게 동작하는지 근본적으로 이해할 수 있다.

과장 좀 보태서, 프론트엔드 개발자에게 Observer는 공기 같은 패턴이다. 평소에는 의식하지 못하지만, 없으면 아무것도 할 수 없다.

visible: false

#Reference

Refactoring Guru - Observer

refactoring.guru

댓글

댓글을 불러오는 중...