어서와, 개발은 처음이지?

관찰자(observer) 패턴과 발행/구독(publish/subscribe)과 프론트엔드 본문

디자인패턴

관찰자(observer) 패턴과 발행/구독(publish/subscribe)과 프론트엔드

오지고지리고알파고포켓몬고 2022. 12. 3. 17:44
반응형

영상으로 보기 - https://youtu.be/aH4U6bfi_Ds

 

1. 옵저버

(이론이 지루하시다면 3번 단락으로 넘어가세요!)

 

옵저버 패턴은 상태(state)상태 관찰하는 관찰자(observer)라는 개념을 통해서 상태 변화가 있을때 각 관찰자가 인지하도록하는 디자인 패턴입니다.

 

프론트엔드 개발자라면 옵저버 패턴이라는 이름을 들어본적 없어도 상태가 변할때 어떤 동작이 수행된다는 개념은 이미 익숙하실텐데

dom에 event listener를 달아두어 dom의 상태 변화를 관찰하고 listener를 통해 기능을 수행한다거나, 모던 프론트엔드 세계에서 상태 변경에 따라 화면이 변화되는 것이 그 예가 될 것 같습니다.

 

이 철학을 간단하게 구현해보면

function createObserverState() {
  const observerState = {
    state: { name: 'xxx' },
    listeners: [],
    subscribe: (listener) => observerState.listeners.push(listener),
    setState: (state) => {
      observerState.state = state
      observerState.listeners.forEach(listener => listener(state))
    }
  }
  return observerState
}

이런식으로 구현할 수 있을텐데요

 

상태를 담는 state, 상태 변화를 통지받을 listener 함수들을 보관하는 listeners, listeners에 함수를 등록할 수 있는 subscribe 메소드, 상태를 변화시키는 setState 메소드로 구성하여

어디선가 setState가 호출되어서 상태 변화가 일어나면 listeners에 등록된 각 listener를 통해 관측자들에게 상태가 변화했음을 알리는 원리인 것이죠.

 

1-1. profileState를 관찰하는 두 관측자(observer)
1-2. listener를 통해 상태 변화를 인지하는 각 관찰자

 

2. 발행/구독

옵저버 패턴을 이야기하면 빠지지 않고 등장하는것으로 publish subscribe 혹은 pub sub이라고도 불리우는 발행/구독 모델이 있는데, 하나의 상태를 감지하는 디자인을 넘어서 broker라고도 불리는 중개자라는 개념을 통해, 불특정 다수의 관찰자들에게 그들이 구독하는 관심사에 대한 메세지를 전달하는 디자인입니다.

 

이 역시 간단하게 구현해보면

export const broker = {
  listeners: {},
  publish: (key, message) =>
    broker.listeners[key]?.forEach(listener => listener(message)),
  subscribe: (key, listener) => {
    if(!broker.listeners[key]) {
      broker.listeners[key] = [listener]
      return
    }
    broker.listeners[key].push(listener)
  }
}

위와 같이 특정 key로 구분되는 리스너 집합에게 메세지를 발행하는 publish 메소드와, 특정 key로 발행되는 메세지를 구독할 리스너를 등록하는 subscribe로 구성할 수 있습니다.

 

(이러한 역할을 수행하는 공식 api로는 nodejs의 event emitter와 브라우저의 message channel, event target등 이 있어요.)

 

발행/구독 디자인을 사용하면

발행자와 구독자 간 관계를 브로커가 대행하고, 이들은 서로의 존재를 모른채 인터페이스 혹은 메세지에만 의존하기 때문에 시스템 간 상호 의존성을 줄여주는 특징이 있어서 msa 패러다임에서 kafka같은 메세지 큐를 사용해 복잡하게 얽힌 시스템 간 의존성을 줄이는 설계를 적용하기도 합니다.

 

system 간 직접 의존 vs 중개자(broker)를 통한 의존

 

3. 어디에 쓸 수 있을까?

지루한 이론 얘기는 이 정도로 마무리하고, 그래서 이걸 어디에 쓸 수 있을까요?

 

모던 프론트엔드 시대가 오면서 프론트엔드 라이브러리 혹은 프레임워크에 의존적인 개발을 하게될 수 밖에없는데, 우리는 때때로 그들이 정해둔 컴포넌트(component) 수준의 관계 형성 / 상호작용 문제에 직면하게 됩니다.

 

하지만 옵저버의 철학은 기본적으로 상태 변화를 유발(혹은 메세지를 발행)시키는 쪽과 그것을 수신하는 관찰자를 reactive하게 이어주면서도 상호 의존성은 줄여주기 때문에 컴포넌트 간 발생할 수 있는 복잡한 관계를 풀어주는데에 시도해보기 용이합니다.

 

예를들어서 react를 사용하는 프로젝트에서 axios 요청에 대한 에러 응답이 도착했을때 toast를 띄우고싶으면 어떻게 해야할까요?

 

각 요청마다 catch를 하는건 비효율적일것같고 막연하게는 axios interceptor에 무슨짓을 해야될 것 같은데,

axios layer는 react lifecycle의 외부에 위치하고있기 때문에 에러를 어떻게 전달할지 감이 잘 오지 않는것같아요.

 

여기에 발행/구독 디자인을 사용해보면 어떨까요?

// broker 모듈
export const errorBroker = new EventTarget()
export const SHOW_TOAST_KEY = 'SHOW_TOAST'
export const publishError = (error) => 
  errorBrocker.dispatchEvent(new CustomEvent(SHOW_TOAST_KEY, { 
    detail: error
  }))

위 구현처럼 간단하게 에러 관련 메세지들을 중개할 broker를 만들고

// react 컴포넌트
export default function Toast() {
  const [errorMessage, setErrorMessage] = useState('')

  const toast = useCallback((message) => {
    setErrorMessage(message)
    setTimeout(() => setErrorMessage(''), 3000)
  }, [])

  useEffect(() => {
    errorBroker.addEventListener(SHOW_TOAST_KEY, (e) => toast(e.detail.message))

    return () => errorBroker.removeEventListener(toast)
  }, [toast])

  return {errorMessage && <>...</>}
}

toast 노출을 담당하는 react 컴포넌트에서 구독을 걸어두면

// axios 모듈
axios.interceptors.response.use(
  (res) => {
    // do something
  },
  (err) => {
    publishError(err)
    return Promise.reject(err)
  }
)

axios interceptor에서 broker에게 에러 메세지를 publish하는 것으로 react 컴포넌트에게 일을 시킬 수 있게되겠죠!

 

이는 react toastify라는 라이브러리가 별도의 컴포넌트 설정을 하지않아도 react lifecycle을 갖는 toast 컴포넌트를 노출할 수 있는것과 비슷한 원리입니다.

toast emitter

그리고 또 어떤 것 들을 할 수 있을까요?

라이브러리에 의존하게된다 라는 것 역시 의존성 문제로 볼 수 있는데 이 관점에서도 생각해볼만한 문제입니다

 

zustand라는 전역 상태 관리 도구를 들어보신적 있으신가요?

핵심 로직이 100줄도 안되는것으로도 유명한 이 녀석을 유심히 살펴보면 vanilla.ts라는 파일에 옵저버 메커니즘의 상태관리 로직이 들어있고 react.ts라는 파일에서 react lifecycle에 따라 동작할 수 있도록 제공하는 형태를 띄고있습니다. (4.1.4v 기준)

 

친숙한 옵저버 패턴 (https://github.com/pmndrs/zustand/blob/v4.1.4/src/vanilla.ts#L63)

이게 무슨 의미일까요?

 

여러분도 마음만 먹으면 리액트의 lifecycle에 온전히 의존하지 않는 수준으로도 본인만의 특별한 상태관리 체계를 만들 수 있다는 뜻이지요!

4. 마무리

이 전에 말씀드린대로

디자인 패턴의 핵심은 각 패턴이 어떤 문제를 해결하기위해서 어떤 아이디어를 사용했는지에 대해 이해하는것이라고 생각하는데

 

비단 제가 말씀드린 사례 뿐만아니라 여러분이 경험하고 계신 문제에도 응용해볼만한 포인트가 있을지 고민하고, 창의력을 발휘해서 다양한 시도를 해보신다면 더욱 풍부한 구조 설계를 진행할 수 있지않을까 싶습니다.

 

그럼 또 다른 디자인 패턴과 아이디어를 들고 다시 찾아오도록 하겠습니다!

'디자인패턴' 카테고리의 다른 글

팩토리 패턴(factory pattern)에 대한 생각  (2) 2022.10.07
3 Comments
댓글쓰기 폼