Micro Frontends 번역글 2/5

Juyeon Ma
15 min readAug 2, 2019

--

이 글은, https://martinfowler.com/articles/micro-frontends.html 페이지를 번역한 글 입니다.

저는 번역 경험이 거의 없습니다. 서투른 글이라 번역체의 분명하지 않은 문장 표현들이 종종 있겠습니다만.. 너그러이 보아 주셨으면 좋겠고, 얼마든지 개선점이나 조언 주시면 감사하겠습니다. 저는 이 문서의 내용이 분명 우리에게 도움이 될 것이라 생각하고, 많은 분들과 나누고 싶습니다.

(그리고 문서가 좀 깁니다. 따로 편집하거나 요약하지 않았습니다.)

미흡한 자료 보아 주셔서 감사합니다 :)

Contents

(이 페이지에서 볼 내용 끝에 * 로 표시)

• 이점 (Benefits)

⁃ 점차적인 업그레이드 (Incremental upgrades)
⁃ 단순하고 분리된 코드베이스 (Simple, decoupled codebases)
⁃ 독립적인 배포 (Independent deployment)
⁃ 자율적인 팀 (Autonomous teams)
⁃ 간단히 말해서 (In a nutshell)

• 예제 (The example)

• 통합에 대한 접근 방식 (Integration approaches) *

⁃ 서버사이드 템플릿 구성 (Server-side template composition) *
⁃ 빌드 타임 통합 (Build-time integration) *
⁃ iframe을 통한 런타임 통합 (Run-time integration via iframes) *
⁃ JavaScript를 통한 런타임 통합 (Run-time integration via JavaScript) *
⁃ 컴포넌트를 통한 런타임 통합 (Run-time integration via Web Components) *

• 스타일링 (Styling)

• 공유컴포넌트 라이브러리 (Shared component libraries)

• 어플리케이션 간 통신 (Cross-application communication)

• 백엔드 통신 (Backend communication)

• 테스팅 (Testing)

• 상세 예제 (The example in detail)

⁃ 컨테이너 (the container)
⁃ 마이크로 프론트엔드 (The micro frontends)
⁃ 라우팅을 통한 어플리케이션 간 통신 (Cross-application communication via routing)
⁃ 공통 컨텐츠 (Common content)
⁃ 인프라 (Infrastructure)

• 단점 (Downsides)

⁃ 페이로드 크기 (Payload size)
⁃ 환경의 차이 (Environment differences)
⁃ 운영 및 거버넌스 복잡성 (Operational governance complexity)

• 결론 (Conclusion)

통합에 대한 접근 방식

앞서의 꽤 느슨한 정의를 감안할 때 합리적으로 micro frontends라 할 수 있는 많은 접근법이 있습니다. 이 섹션에서는 몇 가지 예를 제시하고 그 절충점에 대해 논의합니다.

일단, 모든 방식에 걸쳐 자연스럽게 나타나는 아키텍처가 있습니다. 애플리케이션의 각 페이지에 대한 micro frontend application (이하 micro app) 이 있으며, 하나의 컨테이너 애플리케이션이 있습니다.

  • Header와 Footer같은 공통 페이지 엘리먼트를 렌더링합니다.
  • 인증 및 탐색등의 교차적 문제를 해결합니다.
  • 페이지에 다양한 micro frontend를 가져오고 각 micro frontend에게 언제 어디서 자신을 렌더링할지 알려줍니다
그림 5 : 일반적으로 페이지의 시각적 구조에서 아키텍처를 파생시킬 수 있습니다

방법 1. 서버사이드 템플릿 구성

먼저, 새롭지 않은 프론트엔드 개발방식으로 시작합니다: 여러 templates나 fragments로 서버에서 HTML을 렌더링합니다. 페이지의 공통요소를 포함하고 있는 index.html이 있으며 , server-side include를 통해 각 부분의 HTML 파일을 페이지의 특정 부분에 삽입합니다.

<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Feed me</title>
</head>
<body>
<h1>🍽 Feed me</h1>
<!--# include file="$PAGE.html" -->
</body>
</html>

Nginx를 통해 이 파일을 제공하며, 요청될 URL을 $PAGE 변수에 매칭하여 설정을 구성합니다.

server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
ssi on;
# Redirect / to /browse
rewrite ^/$ http://localhost:8080/browse redirect;
# Decide which HTML fragment to insert based on the URL
location /browse {
set $PAGE 'browse';
}
location /order {
set $PAGE 'order';
}
location /profile {
set $PAGE 'profile'
}
# All locations should render through index.html
error_page 404 /index.html;
}

이는 상당히 보편적인 서버사이드 구성입니다. 이것을 micro frontends 라 부를 수 있는 이유는, 각 부분을 독립적인 팀에서 자체적으로 도메인 컨셉을 포괄하고 이를 딜리버리할 수 있는 방식으로 코드를 분할했기 때문입니다. 위에 나타나지 않은 것은 다양한 HTML 파일들이 웹 서버 상에서 어떻게 쓰여지는지에 대한 것입니다. 그러나 각자 고유한 배포 파이프라인이 있다는 가정은, 다른 페이지에 영향을 미치거나 다른 페이지를 고려하지 않고 변경 사항을 배포할 수 있게 해 줍니다.

독립성을 높이기 위해, 각 micro app을 렌더링하고 서빙하는 역할을 하는 분리된 서버를 구성하고, 맨 앞단에 한 서버를 두어 다른 서버들에게 요청을 하게 할 수 있습니다. 응답에 대한 캐싱을 잘 적용한다면 지연시간 발생 없이 수행 할 수 있습니다.

그림 6 : 각 서버는 독립적으로 구축 및 배포 할 수 있습니다.

이 예는 micro frontends 가 반드시 새로운 기술일 필요는 없으며 복잡하지 않아도 된다는 것을 보여줍니다. 아키텍쳐가 코드베이스와 팀 자율성에 어떻게 영향을 미치는지에 대해 신중해야만 기술 스택에 관계없이 동일한 이점을 얻을 수 있습니다.

방법 2. 빌드 타임 통합

종종 볼 수 있는 한 가지 접근법은 각 micro app을 패키지로 배포하고 컨테이너 어플리케이션에 모두를 library dependency로 포함시키는 것입니다. 다음은 컨테이너의 package.json가 예제 애플리케이션을 찾는 방법입니다 .

{
"name": "@feed-me/container",
"version": "1.0.0",
"description": "A food delivery web app",
"dependencies": {
"@feed-me/browse-restaurants": "^1.2.3",
"@feed-me/order-food": "^4.5.6",
"@feed-me/user-profile": "^7.8.9"
}
}

이 방법은 처음엔 의미있는 것처럼 보입니다. 단독으로 배포 가능한 자바 스크립트 번들을 생성하여 다양한 어플리케이션의 공통 dependencies의 중복을 제거할 수 있습니다. 그러나, 이 접근 방식은 제품의 각 부분을 변경하기 위해 모든 micro frontends application을 다시 컴파일하고 릴리즈해야 함을 의미합니다. 마이크로 서비스와 마찬가지로, 우리는 그간 이러한 강하게 결합된 릴리즈 프로세스같은 것들로 충분히 고통을 겪어왔습니다. 우리는 이런 종류의 접근에 반대하는 방법을 추천할 것입니다.

어플리케이션을 독자적으로 개발, 테스트할 수 있는 별도의 코드베이스로 나누는 데에 어려움을 겪었으므로, 출시 단계에서 있을 다른 모든 커플링은 소개하지 않겠습니다. 우리는 빌드타임이 아닌 런타임에 어플리케이션을 통합할 수 있는 방법을 찾아야 합니다.

방법 3. iframe을 통한 런타임 통합

브라우저에서 어플리케이션을 구성하는 가장 간단한 방법 중 하나는 흔한 iframe입니다. 본질적으로, iframe을 사용하면 독립적인 하위 페이지로 하나의 페이지를 쉽게 만들 수 있습니다. 그들은 또한 스타일링과 전역 변수면에서 서로 간섭하지 않는 훌륭한 격리 수준을 제공합니다.

<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<iframe id="micro-frontend-container"></iframe>
<script type="text/javascript">
const microFrontendsByRoute = {
'/': 'https://browse.example.com/index.html',
'/order-food': 'https://order.example.com/index.html',
'/user-profile': 'https://profile.example.com/index.html',
};
const iframe = document.getElementById('micro-frontend-container');
iframe.src = microFrontendsByRoute[window.location.pathname];
</script>
</body>
</html>

서버 측 include 와 마찬가지로, iframe로 페이지를 작성하는 것은 새로운 기술이 아니며 그렇게 흥미롭지는 않습니다. 그러나 앞에서 설명한 micro frontends 의 주요 이점을 다시 살펴보면 어플리케이션을 어떻게 분할하고 팀을 구성할 지 조심스러울 때, iframe은 대부분 요건에 부합합니다.

우리는 종종 iframe을 선택하기 꺼려합니다. 그 꺼림칙한 이유 중 하나는 iframe이 조금 “멍청”하다고 느껴지는 것 말고도, 그럴 만한 이유가 있습니다. 이러한 쉬운 분리는 다른 옵션보다 유연성이 떨어지는 경향이 있습니다. 애플리케이션의 서로다른 부분을 통합하기가 어렵기 때문에 routing, history, deep-link가 더욱 복잡해지고, 페이지가 완벽하게 응답하도록 해야 하는 추가적인 과제가 주어집니다.

방법 4. JavaScript를 통한 런타임 통합

이 접근법은 아마도 가장 유연한 접근방식일 것이며 팀이 가장 자주 채택할 방식일 것입니다.

각 micro app은 <script>태그를 사용하여 페이지에 삽입되고, 로드 시 전역 함수를 시작점으로 사용합니다.
컨테이너 앱은 어떤 마이크로앱이 마운트되어야 하는지를 결정하고 마이크로앱에게 언제 그리고 어디에서 렌더링해야할지를 알려주는 관련 함수를 호출합니다.

<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<!-- These scripts don't render anything immediately -->
<!-- Instead they attach entry-point functions to `window` -->
<script src="https://browse.example.com/bundle.js"></script>
<script src="https://order.example.com/bundle.js"></script>
<script src="https://profile.example.com/bundle.js"></script>
<div id="micro-frontend-root"></div><script type="text/javascript">
// These global functions are attached to window by the above scripts
const microFrontendsByRoute = {
'/': window.renderBrowseRestaurants,
'/order-food': window.renderOrderFood,
'/user-profile': window.renderUserProfile,
};
const renderFunction = microFrontendsByRoute[window.location.pathname];
// Having determined the entry-point function, we now call it,
// giving it the ID of the element where it should render itself
renderFunction('micro-frontend-root');
</script>
</body>
</html>

이 예제는 분명히 원시적이지만 기본적인 기술을 보여줍니다. 빌드 타임 통합과 달리, 각 bundle.js 파일을 독립적으로 배포할 수 있습니다. 또한, iframe과 달리 micro app간의 통합을 완벽하고 유연하게 처리할 수 ​​있습니다. 그리고 우리는 위의 코드를 여러가지 방법으로 확장할 수 있습니다. 예를 들어, 필요에 따라 각 JavaScript 번들을 다운로드하거나, micro app을 렌더링할때 데이터를 주고받을 수 있습니다.

이 접근법의 유연성과 독립적으로 배치 가능함이 결합되어 이것이 우리의 기본 선택이 되며, 또한 이것이 현실에서 가장 많이 발견된 선택입니다. 전체 예제에서 더 자세히 살펴보겠습니다.

방법 5. 웹 컴포넌트를 통한 런타임 통합

이전 방법의 한 가지 변형은, 각 micro app이 컨테이너가 호출할 전역 함수를 정의하는 대신, 컨테이너가 인스턴스화 할 HTML 커스텀 엘리먼트를 정의하는 것입니다.

<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<!-- These scripts don't render anything immediately -->
<!-- Instead they each define a custom element type -->
<script src="https://browse.example.com/bundle.js"></script>
<script src="https://order.example.com/bundle.js"></script>
<script src="https://profile.example.com/bundle.js"></script>
<div id="micro-frontend-root"></div><script type="text/javascript">
// These element types are defined by the above scripts
const webComponentsByRoute = {
'/': 'micro-frontend-browse-restaurants',
'/order-food': 'micro-frontend-order-food',
'/user-profile': 'micro-frontend-user-profile',
};
const webComponentType = webComponentsByRoute[window.location.pathname];
// Having determined the right web component custom element type,
// we now create an instance of it and attach it to the document
const root = document.getElementById('micro-frontend-root');
const webComponent = document.createElement(webComponentType);
root.appendChild(webComponent);
</script>
</body>
</html>

최종 결과는 이전 예제와 매우 유사합니다. 가장 큰 차이점은 ‘the web component way’를 행하고 있다는 것입니다. 웹 컴포넌트의 스펙이 마음에 든다면, 그리고 브라우저가 제공하는 기능을 사용하는 것을 좋아한다면 이 방법이 좋습니다. container app과 micro app간에 고유한 인터페이스를 정의하려는 경우에는, 이전 예제를 대신 사용하는 것이 좋습니다.

다음 글에 이어서…

--

--