마이크로프론트엔드 아키텍쳐

Kyle·2022년 8월 4일
36
post-custom-banner

참고

Micro Frontends

Micro Frontends

Micro Frontends 번역글 1/5

[Micro Frontend] 마이크로 프론트앤드 - 개념

Micro Frontends | JACOB

마이크로서비스 UI - 마이크로프론트엔드

마이크로 프론트엔드(멀티레포기반)

마이크로 서비스

  • 일반적으로 복잡한 문제에 부딪혔을때 문제를 작게 나누고 작은 문제를 해결하는방법이 효율적이다. → 웹서비스에 적용
  • 데이터 단위로 작게 나누어 작은문제를 하나씩 해결하는것 이것이 마이크로 서비스의 개념이다.

마이크로프론트엔드

독립적으로 제공한 프론트엔드 APP으로 더 큰 하나의 전체 APP을 구성하는 아키텍쳐 스타일

  • 대규모 서비스를 개발할 때 용이하다.
  • 마이크로 프론트엔드란 프론트엔드의 단일 구조를 개별적으로 개발/테스트 및 배포할 수 있는 작고 간단한 단위로 개발하는 패턴
  • 전체화면을 작동할 수 있는 단위로 나누어 개발한 후 서로 조립하는 방식.
  • 작동단위에 사용된 프레임워크의 종류에 상관하지않고 조합가능한 방법을 제공.
  • 단일 팀에 의해 유지될정도의 작은 서비스의 경우에는 오버엔지니어링이 되지 않도록 주의한다.

장점

  • FE 개발의 점진적 업그레이드 / 재작성이 수월해진다.
    • 메인 프레임워크의 큰 변화에 모든 서비스를 업그레이드하는것이 강제되는 것이 아닌, 각자 업그레이드 할 수 있다.
    • 새로운 기술 / 새로운 상호작용방식을 실험하기 위해 독립적으로 수행가능하다.
  • 심플하고, 유지보수가 용이한 코드베이스를 갖는다.
    • 서로의 구성 요소간 특징에 대해 알지못하여 발생하는 의도하지않은 문제들을 피할 수 있다.
    • 도메인 모델을 공유하는것이 어려워짐에따라 APP의 여러 부분간에 데이터-이벤트가 어떻게 전달되었는지 명시하고 확인해야한다. (이는 마이크로프론트엔드라서 하는게아니라 원래 해야만 하는 것임)
  • 분리(독립)배포(일부씩 나누어 업그레이드, 업데이트, 리팩토링)가 쉽다

    - 모든 배포의 범위가 줄어들어 이슈확률이 줄어들게 됨
  • 자율적 팀조직 운영이 가능해진다

    - 팀을 나누는 기준은 항상 최종페이지 기준 기능별로 분리하는 것이 중요하다.

단점

  • 팀 자율성의 증가로 팀 작업 방식이 단편화 될 가능성 존재한다.
  • dependency의 중복을 초래해 사용자가 다운로드해야하는 바이트 수가 증가한다.
  • 배포 번들 사이즈(Payload)의 사이즈가 커진다.
    • 해결책 : 공통 dependency를 외부화시켜서 번들에서 관리하는 것이 중요하다. 이렇게 관리하게 되면 공통 dependecy의 버전을 확실하게 통일화 하는 것이 중요하다. 만약 버전을 통일화하지않으면, 빌드 시점에 충돌을 야기해서 문제가 발생될 것임, 이러한 중복처리를 하지않더라도 모놀리식보다는 개별페이지가 빨리 로드된다.
  • 개발환경의 차이로 인한 복잡도가 상승한다.
    • 해결책 : 정기적으로 프로덕션환경과 같은 환경에서 통합하고 배포하는 작업을 진행해봐야 한다. 문제의 근본까지는 해결이 불가능하지만 적어도 이러한 부분을 유의해서 작업하면된다. 이러한 작업 역시 소규모팀에게는 오버엔지니어링이 될 수 있는 부분이다.
  • 운영 / 관리가 복잡해진다.
    • 각각 앱이 나눠짐에 따라 더 많은것을 고려하고 관리해야하는 것은 자연스러운 것이다.
  • 위 단점에서 나열한 위험들은 충분히 관리할 수 있으며, 적용하는데 드는 비용보다 이점이 중요한 경우가 존재한다면 마이크로프론트엔드를 사용하는게 적합할 것이다.

마이크로 앱을 통합하는 접근방식에 대해

MF로 개발 후 각 단위 APP을 어떻게 통합할지 고려가 필요하다.

  • 각 화면을 조합하는 컨테이너 APP 과 그 내부에 들어가는 단위 APP이 존재.
    - 컨테이너 어플리케이션 [ 각페이지에 대한 마이크로 어플리케이션들, .. ]
    - Header / Footer 공통 페이지 엘리먼트 렌더링 → 인증 및 탐색 등 교차적 문제 해결 → 페이지에 다양한 마이크로 앱을 가저와서 각 앱에게 언제어디서 렌더링 할지 설정

    다양한 통합방식이 존재하지만, 현재 마이크로프론트엔드에서 가장 추천되어지는 것은 JS를 통한 런타임 통합 방식입니다.

  • JavaScript를 통한 런타임 통합

    • 가장 추천되는 접근방식

    • 각 micro app은 <script> 태그를 사용하여 페이지에 삽입되고, 로드 시 전역 함수를 시작점으로 사용함

    • 컨테이너 앱은 어떤 마이크로앱이 마운트 되어야 하는지를 결정하고 마이크로앱에게 언제 어디에서 렌더링해야할지를 알려주는 함수를 호출한다.

    • 예제 : micro-frontends-demo

    • 항상 컨테이너가 메인이되고 그 아래 마이크로 앱들이 존재하는것을 명심한다.

      컨테이너 (Main)

    • 마이크로 앱을 선택하고 배치하는 방법은 App.js 를 통해 관리된다.
      - react router를 사용하여 미리 정의된 router 목록과 현재 URL을 일치시켜 해당 컴포넌트를 렌더링 하는 방식

      <Switch>
        <Route exact path="/" component={Browse} />
        <Route exact path="/restaurant/:id" component={Restaurant} />
      </Switch>
    • Browse 및 Restaurant 컴포넌트

      • 두 경우 모두 MicroFrontend 컴포넌트를 렌더링함
      • 여기서 앱의 고유한 이름과 번들을 다운로드할수 있는 host를 지정한다.
      const Browse = ({ history }) => (
         <MicroFrontend history={history} name="Browse" host={browseHost} />
       );
      const Restaurant = ({ history }) => (
         <MicroFrontend history={history} name="Restaurant" host={restaurantHost} />
       );
      			```
  • App.js 에서 마이크로앱을 선택하면 그것은 MicroFrontend.js에서 렌더링한다.

    • 렌더링할때 마이크로 앱에 고유한 id를 가진 컨테이너 요소를 페이지에 넣으면 된다.

    • 마이크로앱을 다운로드하고 마운트하기위한 트리거로 리액트의 componetDidMount를 사용한다.

      class MicroFrontend extends React.Component {
        render() {
          return <main id={`${this.props.name}-container`} />;
        }
      }
      componentDidMount() {
          const { name, host } = this.props;
          const scriptId = `micro-frontend-script-${name}`;
      if (document.getElementById(scriptId)) {
            this.renderMicroFrontend();
            return;
          }
      fetch(`${host}/asset-manifest.json`)
            .then(res => res.json())
            .then(manifest => {
              const script = document.createElement('script');
              script.id = scriptId;
              script.src = `${host}${manifest['main.js']}`;
              script.onload = this.renderMicroFrontend;
              document.head.appendChild(script);
            });
        }
  • 고유 id를 가진 관련 스크립트가 다운로드 되었는지 확인 (if문)

  • 다운로드가 안되었다면, 메인스크립트의 전체 URL을 조회하기위해 해당 호스트로부터 asset-manifest.json을 가져온다. 이 파일에서 스크립트의 URL을 가져와야한다.

  • 스크립트의 URL을 설정하고 난후 onload핸들러를 이용해 Document에 마이크로프론트엔드를 렌더링한다.

    renderMicroFrontend = () => {
        const { name, history } = this.props;
    window[`render${name}`](`${name}-container`, history);
        // E.g.: window.renderBrowse('browse-container, history');
      };
  • 위 코드를 통해 방금 다운로드한 스크립트에 의해 배치된 전역함수 window.renderBrowse 를 호출한다. 마이크로프론트엔드가 렌더링해야하는 <main> 요소의 ID와 history객체를 전달한다.

  • 이 전역함수는 컨테이너 앱과 마이크로 앱 사이의 key contract로 통합이 이루어지는곳이다. 그러므로 새로운 마이크로 앱을 가볍게 추가될수 있고, 유지보수가 쉽도록 관리해야한다.

    componentWillUnmount() {
        const { name } = this.props;
    window[`unmount${name}`](`${name}-container`);
      }
  • 마이크로 프론트엔드가 DOM에서 제거 될때 관련 마이크로앱도 언마운트 해야한다. 이를 위해 마이크로 프론트엔드 앱이 정의한 해당 전역함수를 생성하고, 적절한 react lifecycle 메소드에서 호출하게 한다

이후 각 마이크로프론트엔드에서 이루어지는 프로세스는 기존 React App의 방식과 동일하다고 보면 된다.

그 외 방식들

  • 서버사이드 템플릿 구성
    • templates나 fragments로 서버에서 HTML을 렌더링
  • 빌드 타임 통합
    • micro app을 패키지로 배포하고 컨테이너 어플리케이션에 모두를 library dependency로 포함시키는 것
    • 이 접근방식은 제품의 각 부분을 변경하기위해 모든 마이크로프론트앱을 다시 컴파일하고 릴리즈해야하는 단점이 존재한다.
    • 빌드타임이 아닌, 런타임 어플리케이션을 통합할 방법이 중요하다.
  • iframe을 통한 런타임 통합
    • iframe을 통해 독립적 하위페이지로 하나의 페이지를 쉽게 만들 수 있다.
    • 스타일링과 전역 변수면에서 간섭하지않는 수준으로 만들 수 있다.
    • 단점
      • 다른것보다 유연성이 떨어지는 경향이 존재
      • 어플리케이션의 다른부분을 통합하기가 어려워 routing, history, deep-link가 복잡해짐
      • 페이지가 완벽하게 응답하도록 개발해야함
  • 웹 컴포넌트를 통한 런타임 통합
    • micro app이 컨테이너가 호출할 전역 함수를 정의하는 대신, 컨테이너가 인스턴스화 할 HTML 커스텀 elements를 정의하는것이다.

공유 컴포넌트 라이브러리

  • micro app간의 시각적 일관성은 프로젝트를 진행하는데 있어서 굉장히 중요하다.
  • 재사용 가능한 공유 UI component library를 개발해야한다, 어렵지만 시각적 일관성을 위해 추천됨
    • 장점
      • 시각적 일관성
      • 코드 재사용 가능
      • 노동력 감소
      • 스타일 가이드의 역할 가능(개발자-디자이너간의 협업을 가능하게 함)
    • 과정에서 주의해야할 점
      • 미리 만들지 말 것
      • 너무 많이 만들지 말 것
      • 공유컴포넌트에 비즈니스로직/도메인로직없이 UI로직만 포함되도록 할 것
        • ex) ProductTable은 product와만 관련이 있는것이기때문에 공유라이브러리가 아닌 micro app의 코드에 있어야함
      • 공유 라이브러리의 일관성, 유효성을 관리할 담당자가 있는것이 좋음
    • 접근 방법
      • 중복이 어느정도 발생하더라도 코드베이스 내에서 자체 컴포넌트를 생성하며 작업한다.
      • 공통된 패턴이 자연스럽게 나타나도록하고, 컴포넌트 API가 명확해지면 공유 라이브러리에서 중복코드를 추출하여 확실한 공통 패턴을 정하는것이 좋다.
      • icons, labels, buttons의 경우 쉽고 확실하게 결정지을수 있는 시각적 엘리먼트를 기본으로 한다.

어플리케이션간 통신 설계

  • 가능한 한 적은 통신으로 앱간 통신이 이루어지도록 설계하는 것이 중요하다.
  • 어떤 종류의 마이크로 앱을 도입하려고 했을때, 그것이 시간이 지남에따라 어떻게 유지보수를 할 것인지에 대해 신중하게 고려해야한다. 여러 마이크로앱 팀과의 의논을 통해 결정이 이루어져야한다.

백엔드 통신 설계

  • BFF ( Back-end for Front-end Pattern )을 이용하라.
    • BFF패턴이란 각 프론트엔드 채널(웹, 모바일앱 등)에 대한 전용 백엔드를 의미하기도 하며, 각 마이크로 앱에대한 백엔드를 의미하기도 한다.
    • 인증관련 통신은 container app이 소유하도록 해서 각 micro app에 전송되어 각 micro 앱 - 서버인증요청으로 통신하도록 한다.

기술 적용 사례 (대표적인 앱/웹) / 적용규모

토스

마이크로 프론트엔드 아키텍처를 도입하기위해 총 세가지 아키텍처에 대해 비교분석하고 고민하였음

  1. 모놀리식
  2. 마이크로서비스
  3. 모노레포 (서비스의 규모가 커졌을때, 각 서비스간 분리도 되며 공통구조를 가져가고 싶을떄 사용)
    - 토스의 내부 웹 프론트엔드 서비스 수 30개 이상
  • 모놀리식

    • 한 패키지 안에 여러 개의 서비스
    • 장점
      • 공통 코드 공유가능
      • 사용 라이브러리 버전을 손쉽게 통일
      • 비용없이 새로운 서비스 구축
      • 서비스 관리 비용 절감
    • 단점
      • 하나 서비스의 변경사항이 다른 서비스까지 영향을 미칠 수 있는 점
      • 서비스별 배포를 할 수 없는점
      • 서비스별 캐싱 정책을 가져가기 어려운점
  • 마이크로서비스
    - 토스는 위에서 나열한 모놀리식의 단점때문에 두번째 아키텍처인 마이크로서비스를 검토하게 됩니다.
    - 마이크로서비스를 함으로써 모놀리식에 비해 얻을 수 있는 장점은 빌드 시간의 획기적인 감소입니다.
    - 모놀리식 : 20분 이상
    - 마이크로서비스 : 2~3분(작은 서비스의 빌드 시간)
    - 모놀리식 아키텍처의 반대되는 이점이 존재함
    - 패키지별로 자유로운 의존성을 선택할 수 있음
    - 서비스가 서로 독립적으로 존재하여 영향을 미칠 수 없음
    - 서비스별 배포 가능

  • 하지만 이런 이점에도 불구하고 토스는 마이크로서비스의 단점으로 인해 마이크로서비스 도입을 하지않기로 결정함

    • 공통 코드 공유의 어려움(멀티레포로 관리되기 때문)
    • 사용하는 라이브러리 버전 파편화 (이것 역시 멀티레포의 특징)
    • 새로운 서비스 구축에 큰 비용
    • 서비스 관리의 복잡도 상승
  • 모노레포
    - 그 후 결정된 것이 모노레포
    - 모노레포란?
    - 하나의 git 레포지토리에 여러 패키지를 담는 것
    - 하나의 레포지토리 안에서 모든 서비스와 라이브러리가 관리됨
    - 예시


    모노레포?

  • 모노레포란 두 개 이상의 프로젝트 코드를 하나의 git 레포지토리에서 관리하는 기법이다.

  • 페이스북이나 구글 마이크로소프트 등에서 사용하고있으며, 일부 인기있는 오픈 소스 프로젝트에서도 monorepo를 쓰고 있다.

  • 모노레포는 모놀리식과 멀티레포의 단점을 보완하고 일부 장점을 섞은 기법

장점

  • 코드의 재사용
  • 의존성 관리
    • 기존의 멀티레포는 각 레포에 prettier, tslint등 코드포매팅 패키지를 각각 설치해야했지만 모노레포는 일일히 레포를 클로닝해서 업데이트하는 과정을 거치지 않아도 됨
  • 작은 커밋과 PR
    • 멀티레포의 경우 변경사항이 있을경우 각각 레포에서 작업하여 PR(pull request)를 보내야 한다
    • 모노레포는 각각의 패키지의 변경사항을 하나의 PR로 제출할수있다
      • 이 때 PR에 포함된 커밋을 작성할때는 한커밋에 하나의 패키지의 변경사항만 기록하는 (Single Responsibility Principle, 단일 책임 원칙)을 지켜야한다.
  • 대규모 리팩토링 유도
    • 모노레포로 관리하게 되면서 하나의 파일에 대한 컨트리뷰터가 상당히 많아진 것을 체감하게됐다(class101).
    • 해당 부분에서 발생하는 로직에 대해 이해하는 동료가 많아 여러 사람이 해당 파트에 효율적으로 로직을 작성하는 방식에 대해 함께 생각 할 수 있게 됨.
  • 팀 간 협업
    • 모든 구성원이 모든 코드에 접근할 수 있기 때문에 팀 간 협업이 자유롭다.

단점

  • 레포지토리의 거대화
    • 분산되어있던 리소스가 하나의 레포에 통합되어 관리되기때문에 레포의 규모가 커진다. 이러한 문제는 서비스가 지속될수록 뿌리처럼 계속 뻗어나갈 가능성이 있다.
  • 느린 빌드시간
    • 멀티레포와는 반대로 레포하나로 관리되기 때문에, 분산된코드의 빌드보다 속도가 느릴수밖에 없다
  • 과도한 의존성
    • 패키지간 의존성 관리가 쉽다는 장점이 있지만 오히려 이러한 장점이 과도한 의존성으로 직결될 수 있다.

결론

  • 모노레포의 장점은 멀티레포의 단점이 되기도하고 멀티레포의 장점은 모노레포의 단점이 되기도한다. 이러한 장단점을 잘 고려해서 어떤 상황에 더 적합한지 고려해서 개발을 진행하면 된다.
profile
깔끔하게 코딩하고싶어요
post-custom-banner

0개의 댓글