마이크로프론트엔드 정보 교환 방식 개선

사공광열·2025년 3월 28일

1. 첫 번째 접근법: Props 전달 방식

가장 먼저 시도한 방법은 React의 기본적인 props를 사용하는 것이었습니다.


function App() {
  const [selectedAccount, setSelectedAccount] = useState(null);

  const handleAccountSelect = (account) => {
    setSelectedAccount(account);
  };

  return (
    <div>
      <BankApp onAccountSelect={handleAccountSelect} />
      <GoalsApp selectedAccount={selectedAccount} />
    </div>
  );
}

이 방식은 간단했지만, 몇 가지 문제점이 있었습니다:

문제점

  • 불필요한 리렌더링: 계좌 데이터가 변경될 때마다 관련 없는 컴포넌트까지 다시 그려져 성능이 저하됬습니다.
  • Props Drilling: 데이터가 여러 컴포넌트 계층을 통과하면서 코드가 복잡해지고 유지보수가 어려웠습니다.
  • 복잡한 상태 관리: 여러 앱에 걸친 상태를 관리하기가 점점 더 어려워졌습니다.

2. 두 번째 접근법: 로컬 스토리지 활용

다음으로는 브라우저의 로컬 스토리지를 사용해 앱 간 데이터를 공유했습니다.

// Bank 애플리케이션에서 데이터 저장
const selectAccount = (account) => {
  localStorage.setItem('selectedAccount', JSON.stringify(account));
  // 변경 이벤트 발생
  window.dispatchEvent(new Event('storage'));
};

// Goals 애플리케이션에서 데이터 사용
useEffect(() => {
  const handleStorageChange = () => {
    const accountData = JSON.parse(localStorage.getItem('selectedAccount'));
    setAccountData(accountData);
  };

  // 초기 로드
  handleStorageChange();

  // 변경 이벤트 리스너
  window.addEventListener('storage', handleStorageChange);
  return () => {
    window.removeEventListener('storage', handleStorageChange);
  };
}, []);

이 방식으로 props drilling 문제는 해결했지만, 새로운 문제들이 생겼습니다:

문제점

  • 메모리 누수: 이벤트 리스너를 제대로 정리하지 않으면 메모리가 낭비됐습니다.
  • 보안 취약점: 계좌 정보와 같은 민감한 데이터가 로컬 스토리지에 노출되어 보안 위험이 있었습니다.
  • 동기화 이슈: 여러 탭을 사용하거나 복잡한 데이터 구조에서 일관성 유지가 어려웠습니다.

3. 최종 접근법: MessageEventBus 패턴

마지막으로 도입한 방식은 중앙집중식 이벤트 관리 시스템인 MessageEventBus입니다.


// Bank 서비스에서 이벤트 발행
const { publish } = useMessageEventBus();

const handleAccountSelect = (account) => {
  publish("ACCOUNT_SELECTED", account);
};

// Goals 서비스에서 이벤트 구독
const { useSubscription } = useMessageEventBus();
const [accountData, setAccountData] = useState(null);

useSubscription("ACCOUNT_SELECTED", (data) => {
  setAccountData(data);
});

이 방식은 이전 접근법들의 문제점을 해결하고 여러 장점을 제공했습니다:

장점

  • 효율적인 메모리 관리: 필요한 컴포넌트만 업데이트되어 성능이 개선되었습니다.
  • 느슨한 결합: 앱들이 서로 직접 의존하지 않아 독립적으로 개발할 수 있엇습니다.
  • 확장성: 새로운 이벤트 유형을 쉽게 추가하고 여러 앱 간 통신을 원활하게 할 수 있었습니다.
  • 보안 강화: 민감한 데이터가 메모리 내에서만 관리되어 더 안전해졌습니다.

마지막 이벤트 기억 기능 도입

MessageEventBus를 사용하면서 중요한 개선점 중 하나는 "마지막 이벤트 기억" 기능이었습니다. 이 기능을 도입한 이유는 다음과 같았습니다:

  • 페이지 이동 문제: 사용자가 페이지를 이동하거나 앱을 전환할 때 메시지 상태가 사라지는 문제가 발생했습니다.
  • 컴포넌트 지연 로딩: 일부 컴포넌트가 나중에 로드될 경우, 이미 발행된 이벤트를 놓치는 상황이 발생했습니다.
  • 사용자 경험 일관성: 사용자가 작업 흐름 중에 다른 페이지로 이동했다가 돌아와도 선택한 계좌 정보가 유지되어야 했습니다.
public subscribe<T = any>(
    eventType: string,
    callback: EventCallbak<EventMessage<T>>
  ): () => void {
    if (!this.subscribers.has(eventType)) {
      this.subscribers.set(eventType, new Set());
    }
    this.subscribers.get(eventType)!.add(callback);

    // 구독 시 마지막 이벤트가 있으면 즉시 발행
    const lastEvent = this.lastEvents.get(eventType);
    if (lastEvent) {
      setTimeout(() => callback(lastEvent), 0);
    }

    return () => this.unsubscribe(eventType, callback);
  }

  public unsubscribe(eventType: string, callback: EventCallbak): void {
    if (this.subscribers.has(eventType)) {
      this.subscribers.get(eventType)!.delete(callback);
    }
  }

  public publish<T = any>(eventType: string, payload: T): void {
    const message: EventMessage<T> = {
      type: eventType,
      payload,
      timestamp: Date.now(),
    };

    // 마지막 이벤트 저장
    this.lastEvents.set(eventType, message);

    if (this.subscribers.has(eventType)) {
      this.subscribers.get(eventType)!.forEach((callback) => {
        callback(message);
      });
    }
  }

  // 마지막 이벤트 조회 메서드
  public getLastEvent<T = any>(eventType: string): EventMessage<T> | undefined {
    return this.lastEvents.get(eventType) as EventMessage<T> | undefined;
  }

이 기능 덕분에:

  • 사용자가 페이지를 이동하거나 앱을 전환해도 중요한 상태 정보가 유지됩니다
  • 새로 마운트된 컴포넌트가 구독을 시작하면 자동으로 최신 데이터를 받게 됩니다
  • 전체 애플리케이션에서 일관된 상태를 유지할 수 있습니다

결론

마이크로프론트엔드에서 효율적인 앱 간 통신은 전체 시스템의 성능과 유지보수성에 큰 영향을 미칩니다. 저희는 기본적인 props 전달에서 시작해 로컬 스토리지를 거쳐 최종적으로 MessageEventBus 패턴을 도입함으로써, 성능, 보안, 그리고 개발 경험을 크게 향상시켰습니다.

특히 마지막 이벤트 기억 기능의 도입으로 페이지 이동 시 데이터가 사라지는 문제를 해결하여 사용자 경험을 크게 개선했습니다. 이 접근법은 현재 저희의 마이크로프론트엔드 아키텍처에서 핵심적인 역할을 하고 있으며, 새로운 기능과 서비스를 추가할 때도 일관된 통신 패턴을 제공하고 있습니다.

profile
Interactive Developer

0개의 댓글