Observer 패턴으로 상태 변화 감지하기

se-een·2023년 3월 13일
1

Vanilla JS 노하우

목록 보기
1/2
post-thumbnail

Observer 패턴이란?

옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. -위키백과

즉, 옵저버 패턴이란 어떠한 상태가 변하면 그 상태를 바라보고(사용하고) 있던 주체들에게 상태가 변했음을 알려주는 것입니다.

이 패턴을 어떻게 활용해볼 수 있을까요?

컴포넌트간 의존성

A라는 컴포넌트는 클릭 이벤트가 발생 했을때 '가'라는 데이터를 갱신하고 그 데이터를 바탕으로 B라는 컴포넌트를 (리)렌더링 해줘야 한다고 가정해봅시다.

그러면 우리는 보통 A 컴포넌트의 클릭 이벤트 콜백함수 내부에 B 컴포넌트 렌더링 메서드를 넘겨주자. 라고 생각해볼 수 있을 것입니다.

그렇게되면 어떤 문제가 생길까요? C,D,E,F... 여러 개의 컴포넌트가 이벤트에 따라 각각의 컴포넌트의 메서드를 호출한다면, 아래의 그림처럼 컴포넌트 간 관계가 매우 복잡해질 것이라고 직감할 수 있을 것입니다.

그렇다면 데이터가 갱신 즉, 상태 변화가 일어나는 곳은 모델이므로 각각의 모델이 자신들의 데이터를 바라보고(사용하고) 있는 주체들에게 상태가 변했음을 알려주어 알아서 (리)렌더링을 해주는 방식은 어떨까요?

컴포넌트간 의존성 분리

위에서 가정한 방식은 아래와 같은 그림으로 컴포넌트간 의존성은 제거될 수 있을 것이라고 추측할 수 있습니다.

그렇다면 한 번 간단하게 구현해보겠습니다. 먼저 Observer.js입니다.

export default class Observer {
  observers;

  constructor() {
    this.observers = [];
  }

  subscribe(render) {
    this.observers.push(render);
  }

  notify(newData) {
    this.observers.forEach((render) => render(newData));
  }
}

필드로 배열을 선언하였는데 이곳에 상태가 변경되면 실행할 메서드를 담아둡니다.

보통은 컴포넌트의 render 메서드를 담아두게 될 것입니다.

A라는 컴포넌트는 클릭 이벤트가 발생 했을때 '가'라는 데이터를 갱신하고 그 데이터를 바탕으로 B라는 컴포넌트를 (리)렌더링 해줘야 한다고 가정해보겠습니다.

위에서 가정한 상황에선 '가'라는 모델의 observers 필드에는 B 컴포넌트의 렌더링 메서드(render)가 들어가는 것입니다.

다음은 모델입니다.

import Observer from './Observer.js';

class 모델_가 extends Observer {
  state;

  constructor() {
    super();

    this.state = '';
  }

  setState(input) {
    this.state = input;
    
    this.notify(this.state);
  }
}

const= new 모델_가();
export default;

모델은 Observer.js를 상속받습니다.

그리고 state 필드를 가지고 있고 state가 변경되면 즉, setState 메서드가 호출되면 Observernotify 메서드에 변경된 state를 담아 호출하고 있습니다.

그렇다면 A라는 컴포넌트의 이벤트 콜백 함수에 setState를 호출하면 되지 않을까요?

A 컴포넌트는 다음과 같이 구현해볼 수 있습니다.

importfrom "../model/모델_가.js"

export default class A {
  $target;
  
  constructor() {
    this.$target = document.querySelector('.a-componenet');
    
    this.render();
    
	this.$target.addEventListener('submit', (e) => {
      this.eventCallback(e);
    });
  }
  
  render() {
    this.$target.innerHTML = this.template();
    

  template() {
    return `
    <form action="submit">
      <input id="inputData" type="text" />
      <button>submit</button>
    </form>`;
  }

  eventCallback(e) {
    e.preventDefault();

    const { inputData } = e.target;.setState(inputData.value);
  }
}

A 컴포넌트는 위와 같이 input 태그의 값을 '가'라는 모델의 setState 메서드의 인자로 담아보냅니다.

이렇게되면 A 컴포넌트의 submit 이벤트가 발생할 때 마다 '가' 모델의 상태를 바라보고(사용하고) 있던 컴포넌트들은 상태가 변할 때마다 렌더링 할 수 있게 되는 것입니다.

'가' 모델의 상태를 바라고 있는 B 컴포넌트는 다음과 같습니다.

importfrom "../model/모델_가.js"

export default class B {
  constructor() {
    this.$target = document.querySelector('.b-componenet');.subscribe(this.render.bind(this));
  }
  
  render(newData) {
    this.$target.innerHTML = this.template(newData);
  }

  template(newData) {
    return `
    <span>${newData}</span>
    `;
  }
}

'가'의 subscribe 메서드에 render함수를 담아 보냄으로써 '가'의 상태 변화가 생길 때마다 자동으로 리렌더링이 가능한 구조입니다.

이로써 컴포넌트간 의존 관계를 분리할 수 있습니다.

위 예제를 바탕으로 구현한 예시 PR을 아래에 첨부하겠습니다. 위 예제와는 달리 컴포넌트의 생명주기를 추상화하였다는 차이점이 있지만 전체적인 구조는 동일하기에 확인해보면 도움이 될 것 같네요. 😀

예시 PR

profile
woowacourse 5th FE

0개의 댓글