옵저버 패턴

지인혁·2023년 11월 16일
0
post-thumbnail

😎 들어가며

데크코스 교육 중 컴포넌트를 분리하여 개발을 하는 방법을 배웠고 응집도, 독립성, 결합도 등에 대해도 알게 되었다.

이러한 개념들을 알게 된 후 나는 항상 어떻게 컴포넌트 독립성을 높이며 결합도가 강할 때는 어떻게 줄일 수 있을까 고민을 하게 되었고 이러한 고민을 멘토님께 질문 했었다.

멘토님께서는 뭐 커스텀 이벤트 방법도 있지만 멘토님께서는 옵저버 패턴을 사용해보는걸 추천하셨고 이번 노션 프로젝트에서 현재 컴포넌트에서 만약 다른 여러 컴포넌트의 상태 변화가 필요할 때 콜백 함수로 전달하는 방식이 아닌 옵저버 패턴을 활용해보기로 결심했다.


옵저버 패턴(Observer Pattern)

옵저버 패턴(Observer Pattern)은 상태가 변경되었을 때 이를 사전에 등록한 관찰자(Observer)에게 알려주는 디자인 패턴이다. 이 패턴은 주로 이벤트 핸들링 시스템을 구현하는 데 사용되며, 하나의 객체의 상태 변화가 다른 객체에게 영향을 주는 상황에서 사용된다.

옵저버 패턴은 대상(Subject)와 관찰자들(Observers)의 구성 요소로 이루어져 있다.

  • Subject는 상태 관리를하며, 상태가 변경되면 등록된 관찰자들(Observers)들에게 알림을 준다.
  • Observers들은 Subject의 상태 변화를 관찰하고, 상태 변화가 일어날 때마다 업데이트를 수행한다.

즉 관찰자들은 관심있는 상태를 구독하고 상태가 변경되면 대상은 관찰자들에게 모두 알림은 주게 되는데 알림을 받은 관찰자들은 해당 상태 변경에 따른 원하는 로직을 수행하게 된다.


구현

우선 옵저버 패턴을 구현하기 앞서 나는 싱글톤 패턴과 혼합하여 해당 클래스를 전역적으로 인스턴스화 시키며 구현했다.

옵저버 패턴을 구현하기 위한 메소드 중 중요한 3가지를 살펴보자면

  • subscribe : 관찰자들이 상태가 변경되면 수행할 콜백함수를 담는 메소드다.
  • unsubscribe : 옵저버 배열에 담긴 콜백함수들을 제거하는 메소드다.
  • notifyAll : 대상이 상태가 변경되면 해당 관찰자들에게 모두 알림을 주고 해당 상태에 따른 콜백함수들을 모두 호출하는 메소드다.
export default class DocumentObserver {
    static instance = null;

    static getInstance() {
        if (!DocumentObserver.instance) {
            DocumentObserver.instance = new DocumentObserver();
        }
        return DocumentObserver.instance;
    }

    constructor() {
        if (DocumentObserver.instance) {
            return DocumentObserver.instance;
        }
        this.observers = [];
    }

    printObservers() {
        console.log(this.observers);
    }

    subscribe(key, observer) {
        if (!this.observers[key]) {
            this.observers[key] = [];
        }
        this.observers[key].push(observer);
    }

    unsubscribe(key, observer) {
        if (this.observers[key]) {
            this.observers[key] = this.observers[key].filter((obs) => obs !== observer);
        }
    }

    notifyAll(key) {
        if (this.observers[key]) {
            this.observers[key].forEach((observer) => observer());
        }
    }
}

나는 Document라는 상태가 수정, 삭제, 추가 되면 DocumentList와 DocumentChildList의 컴포넌트에서 List 상태들을 모두 갱신해줘야 했다.

그래서 Document라는 상태의 대상(Subject)을 가지고 관찰자들은 DOCUMENT_CHILD_LIST, DOCUMENT_LIST의 키 값을 가지고 해당 키에 따른 콜백함수를 등록하여 구독을 한다.

// document 데이터 가져오기
async getDocumentList() {
    const res = await fetchGetDocumentList();
  
    if (res) {
        this.setState({ documentList: res });
    }
}

// 해당 document content 가져오기
async getDocumentContent(documentId) {
    const { title, content, updatedAt, documents } = await fetchGetDocumentContent(documentId);

    this.childDocumentsViewer.setState({ documentList: documents });
    this.documentEditor.setState({ title, content });
}

// documetList 데이터를 다시 불러오는 함수를 구독
DocumentObserver.getInstance().subscribe(
  DOCUMENT_LIST, 
  () => this.getDocumentList()
);
// childList도 리렌더링 해야하므로 documentContent를 불러오는 함수를 구독
DocumentObserver.getInstance().subscribe(
  DOCUMENT_CHILD_LIST, 
  () => this.getDocumentContent(this.state.documentId)
);

이렇게 각 컴포넌트에서 상태가 변경되는 알림을 받을 때 수행할 함수들을 key 값에 맞추어서 subscribe 등록을 해준다.

그 상태가 변경되는 시점에 해당 컴포넌트에서 notifyAll로 알림이 필요한 구독자들의 key 값에 맞추어서 notifyAll을 호출하면 된다.

// document 추가 이벤트
if (className === 'insert-delete__insert') {
    const $li = target.closest('.document-item');
    const documentId = $li.dataset.id;
  
    await this.postDocument(documentId);
  
    // editor page의 documentChilde 컴포넌트에게 알림을 준다
    DocumentObserver.getInstance().notifyAll(DOCUMENT_LIST);
    DocumentObserver.getInstance().notifyAll(DOCUMENT_CHILD_LIST);

// document 삭제 이벤트
if (className === 'insert-delete__delete') {
    const documentId = target.closest('.document-item').dataset.id;
    const path = window.location.pathname.split('/')[2];
  
    await this.deleteDocument(documentId);
  
    // 삭제한 documentId와 현재 editor 작성하고 있는 editor 페이지가 같다면
    if (Number(documentId) === Number(path)) {
        // 그냥 rootPage로 다시 이동한다.
        RouterManger.getInstance().changeUrl(`/`);
    } else {
        // 아니면 documentList가 변경됨을 documentChild 컴포넌트에게 알림을 준다.
        DocumentObserver.getInstance().notifyAll(DOCUMENT_LIST);
        DocumentObserver.getInstance().notifyAll(DOCUMENT_CHILD_LIST);
    }
}

document가 추가되거나 삭제되는 핸들러에 추가, 삭제가 확정이 된다면 그때는 notiyAll의 메서드를 원하는 구독자들 key 값에 맞춰 모두 호출하게 되고 documentList, documentChildList를 보여주는 컴포넌트가 다시 새로 데이터를 받아 최신 상태의 데이터로 리렌더링이 발생한다.


겪은 문제점

처음에는 배열에서 객체 형식의 키로 담지 않고 그냥 키 없이 배열에 담았는데 문제가 발생했다.

우선 알림을 주었을 때 불 필요한 구독자들에게도 알림이 전달되어 불 필요한 콜백 함수가 수행되고 불 필요한 리 레렌더링도 덤으로 발생했다...

그리고 unsubscribe 메서드를 사용할 때 같은 함수의 참조 값을 매개 변수로 주지않는 이상 콜백 함수 삭제가 불가능 했다.

그래서 멘토님께서는 객체 형태의 key를 활용한다면 해결할 수 있을 것이라 말씀해주셨고 key를 통해 구독자들을 관리하게 되면서 딱 필요한 구독자들에게 알림을 주고 구독자들도 원하는 관심사로 분리할 수 있어서 문제점을 모두 해결할 수 있었다.


👏 마치며

데브코스 노션 프로젝트에 옵저버 패턴을 활용하면서 정말 빛을 바랬던 것 같다.

옵저버 패턴을 활용하면서 각 컴포넌트들 간의 상태 변화에 따른 콜백 함수 props 전달이 거의 사라지고 결합도가 낮아지게 되었다.

그래서 코드가 정말 간결해졌고 App.js의 최상위 컴포넌트의 역할의 비중도 매우 줄게 되었다.

멘토님에게 React에서도 자주 사용되는 패턴이냐고 물어봤었는데 React에서는 사실 잘 사용되지 않는다고 했다. 그래서 어 이 좋은 패턴을 왜 사용안하지 곰곰히 생각해봤는데 리액트에서는 전역 상태관리인 Redux, zustans 등이 있고 이러한 라이브러리가 옵저버 패턴의 기능을 대처할 수 있구나 생각했다.

멘토님께서도 정답이라고 옵저버 패턴보다는상태관리 라이브러리를 사용하여 상태를 관리한다고 말씀하셨다.

profile
대구 사나이

0개의 댓글