엔터프라이즈 프론트엔드 애플리케이션 아키텍쳐

히로
CLASS101
Published in
16 min readJun 17, 2021

소프트웨어의 수명과 복잡도는 대개 비례 관계입니다. 아무리 정교하고 아름답게 코드를 작성해도, 시간이 지날수록 코드베이스는 복잡해지기 마련입니다. 그래서 우리는 이런 문제들을 마법같이 해결해 줄 방법들을 찾아다닙니다. 이 글에는 클래스101에서 수십 명의 개발자가 하나의 애플리케이션을 함께 만들어 갈 때 발생하는 문제들을 해결하기 위한 우리의 고민을 담았습니다.

마이크로 프론트엔드

2018년 3월 처음 서비스를 출시한 이후, 매년 큰 폭으로 성장하는 매출만큼 우리의 코드베이스도 나날이 거대해지고 있습니다. 코드베이스가 커진다는 것은 그만큼 이해하기도, 수정하기도 어려워진다는 것과 비슷한 이야기입니다. 아무리 좋은 코드를 작성하기 위해 노력해도 서로 가지고 있는 지식과 의견이 다르기 때문이겠죠.

사실 코드베이스가 커져서 문제가 된다면 해결 방법은 간단합니다. 작게 유지하면 됩니다. nocode라는 재미있는 프로젝트에서 알 수 있듯 작은 코드는 이해하거나 수정하기 쉽고 오류가 발생할 확률도 적습니다.

요즘 백엔드 개발자들은 이미 마이크로서비스 아키텍쳐를 적극적으로 도입하여 거대한 코드베이스를 작게 분리하여 운영하고 있습니다. 아직 그만큼 널리 알려지지는 않았지만 프론트엔드에서도 애플리케이션을 작게 분리해서 독립적으로 개발, 배포할 수 있는 마이크로 프론트엔드라는 아키텍쳐 스타일이 있습니다.

팀별로 작은 코드베이스

우리의 사업 조직은 기획, 개발, 디자인의 기능 단위로 나누어져 있지 않습니다. 대신 크리에이터 플랫폼, 콘텐츠 플랫폼, 101프라임, 커머스, 스토어프론트, 수강 환경 등 해결하려는 문제에 초점을 맞춰 팀이 분리되어 있습니다. 각 팀마다 담당하는 도메인이 있기 때문에 이미 팀별로 코드를 분리하기에 적합한 환경입니다. 이렇게 모두가 전체 코드를 관리하지 않고 각 팀별로 작은 코드베이스를 갖게 된다면 새로 합류하는 개발자는 해당 팀에서 담당하는 도메인만 잘 알아도 충분히 업무를 진행할 수 있을 겁니다. 이해해야 하는 코드의 절대적인 양이 줄어들었기 때문에 온보딩도 더 빨리 끝날 것이고, 새로운 기능을 추가하거나 오류를 수정하기도 수월해지겠죠.

독립적인 배포와 롤백

팀별로 코드베이스를 분리하는 데서 그치지 않고 애플리케이션까지 분리할 수도 있습니다. 각 팀이 독립적으로 배포 및 롤백을 수행할 수 있다면, 다른 셀의 작업에 영향을 주지 않으면서 빠른 주기로 만들고 측정하고 배우며 제품을 개선해나갈 수 있습니다. 한 팀에서 만든 문제가 전체 제품에 영향을 줄 일도 현저하게 줄어들겠죠. 아직은 먼 이야기지만, 각 팀에서 맞는 기술을 알아서 선택할 수도 있을 겁니다.

모노레포 with Nx

2019년부터 우리는 로직 및 컴포넌트 재사용, 개발 환경 설정, 스키마 동기화, 지속적 통합 등과 같은 문제를 해결하기 위해 Lerna라는 도구로 모노레포를 도입해 사용하고 있었습니다. 이에 대한 자세한 내용은 Monorepo with TypeScript (1)에서 확인할 수 있습니다.

하지만 마이크로 프론트엔드 아키텍쳐와 코드베이스 분리를 준비하다 보니, 단순히 코드를 모아 두고 공유하는 것만으로는 충분하지 않았습니다. 모노레포 안에서도 각 라이브러리의 도메인을 설정하고, 라이브러리의 종류에 따라 의존 관계를 제한하고, 라이브러리 사이의 의존성을 확인하는 등 기존에 사용하던 도구로 해결하기 어려운 문제들이 있었고, 마침 Nx라는 이 문제들을 깔끔하게 해결해 주는 도구가 있어 도입하게 되었습니다.

Nx의 장점들에 대해서 짧게 짚어보면

  • 라이브러리에 태그를 지정하고, 이를 통해 의존 관계를 제한할 수 있습니다.
  • affected 명령어를 통해 코드 변경에 영향을 받는 프로젝트에만 명령어를 실행할 수 있습니다. (nx affected --target=test, nx affected --target=build)
  • 라이브러리 별 빌드 결과를 캐싱할 수 있습니다.

정도가 있습니다. 더 자세한 내용은 Nx 공식 문서Nrwl Connect에서 확인할 수 있습니다.

Nx Basics

Nx를 사용해 프로젝트를 생성하면 apps, libs 폴더가 만들어집니다. 우리가 지금까지 해왔던 것과 다르게 대부분의 기능들을 라이브러리에 구현합니다. 라이브러리에는 범용적으로 사용할 수 있는 디자인 시스템, 국제화, 서드파티 모듈 뿐만 아니라 홈 페이지의 히어로 배너, 상품 상세 페이지, 주문 내역 페이지 등 일반적으로 재사용하지 않는 코드까지 작성합니다. 애플리케이션은 그저 필요한 라이브러리들을 가져와 필요한 의존성을 주입하고, 페이지 컴포넌트들을 적절한 경로에 할당해 주는 역할만 하게 됩니다.

작은 라이브러리로 분리하기

우리는 이 라이브러리들을 Feature, Shell, Provider, UI, Utility의 5가지 분류에 따라 나눕니다. 각 라이브러리의 역할은 다음과 같습니다.

Feature Library

기본적으로 한 페이지에서 사용되는 모든 컴포넌트들은 해당 페이지와 같은 이름의 Feature 라이브러리에 작성합니다. Container, Presentational 컴포넌트를 따로 구분하지 않습니다. Presentational 컴포넌트에서 Container 컴포넌트를 사용할 수 있고, 그 반대도 마찬가지입니다.

클래스101 한국 로그인 페이지
account/feature-sign-in 라이브러리

예를 들어, 위 LoginPage 컴포넌트는 libs/account/feature-sign-in 라이브러리의 컴포넌트들로 만들어지고, 해당 라이브러리에는 다음과 같은 컴포넌트들이 있습니다.

  • AgreeTermsMessage
  • AuthLayout
  • AuthStateLayer
  • EmailLoginForm
  • KakaoLoginButton
  • NaverLoginButton
  • FacebookLoginButton
  • GoogleLoginButton
  • AppleLoginButton
  • SocialLoginButton
  • LoginForm

Feature 라이브러리는 Provider, UI, Utility 라이브러리를 참조할 수 있습니다. 다른 Feature 라이브러리를 참조할 수는 없습니다.

Shell Library

Shell 라이브러리는 애플리케이션에 제공되는 특정 도메인의 진입점(Entrypoint)입니다. Feature, UI 라이브러리의 컴포넌트들을 조합해 페이지들을 만듭니다.

위에서 한 페이지에 사용되는 모든 컴포넌트들이 Feature 라이브러리에 모여 있다고 이야기했습니다. 예를 들어, 홈 페이지에서 사용하는 컴포넌트들은 landing/feature-home 라이브러리에, 수강 페이지에서 사용하는 컴포넌트들은 classroom/feature-player 라이브러리에 모여 있는 식입니다.

하지만 클래스101 미국, 일본, 한국의 홈 페이지는 히어로 배너와 상품 목록을 보여준다는 점을 제외하고는 구성이 많이 다릅니다. 같은 목적의 페이지라도 접속한 유저가 크리에이터인지, 클래스메이트인지, 관리자인지에 따라, 혹은 접속한 국가가 한국인지, 미국인지, 일본인지에 따라 조금씩 다른 화면을 보여주어야 합니다.

즉, 같은 HomePage라도 미국 사이트의 HomePage인지, 일본 사이트의 HomePage인지, 한국 사이트의 HomePage인지에 따라 그 구성이 다릅니다. 그래서 우리는 이 도메인에 접근하는 각각의 애플리케이션을 위한 shell 라이브러리를 만듭니다. (shell-us-web, shell-jp-web, shell-kr-web)

위 클래스101 한국 사이트의 로그인 페이지는 libs/account/shell-kr-web 라이브러리에 속해 있고, 해당 Shell 라이브러리에는 다음과 같은 컴포넌트들이 있습니다.

  • SignInPage
  • SignUpPage
  • PhoneVerificationPage

SignInPage 컴포넌트는 이렇게 작성되어 있습니다.

같은 로그인 페이지지만 클래스101 미국 사이트의 로그인 페이지는 libs/account/shell-us-web 라이브러리에 속해 있습니다. 미국 사용자들은 카카오, 네이버 로그인을 사용하지 않기 때문에 해당 로그인 버튼이 존재하지 않습니다.

클래스101 미국 로그인 페이지

미국 사이트의 SignInPage 컴포넌트는 이렇게 작성되어 있습니다.

Shell 라이브러리는 Feature, UI, Provider 라이브러리를 참조할 수 있습니다. Utility 라이브러리를 참조할 수는 없습니다.

Provider Library

주로 둘 이상의 Feature 라이브러리에서 공유하는 상태를 관리하는 라이브러리입니다. 크게 보면 로그인 상태를 관리하는 shared/provider-auth-state, 언어 및 지역 설정을 관리하는 shared/provider-locale 등의 라이브러리가 있습니다.

다른 예시로는 아래의 크리에이터를 위한 수요조사 작성 페이지를 들 수 있습니다.

각각의 정보 입력 화면은 수요조사 도메인의 Feature 라이브러리입니다. (product-survey/feature-edit-survey-basic-information, product-survey/feature-edit-survey-title-and-cover, ...) 다만 Header, Footer 영역과 좌측의 네비게이션은 모든 수요조사 작성 페이지에서 공통으로 사용하고 있기 때문에 feature-edit-survey-layout 라이브러리로 분리될 수 있습니다. 하지만 이 feature-edit-survey-layout 라이브러리는 다른 수요조사 편집 화면의 입력 내용에 따라 진행도와 하단 버튼 등을 다르게 보여줘야 합니다. 이 때 필요한 것이 바로 Feature 라이브러리들 사이에서 상태를 공유하기 위한 Provider 라이브러리입니다. product-survey/provider-edit-survey 라이브러리에 공유 상태를 정의함으로써 상태 공유 문제를 해결할 수 있습니다.

Provider 라이브러리는 다른 Provider, 혹은 Utility 라이브러리를 참조할 수 있습니다.

UI Library

여기까지 오면 쉬워집니다. 이름에서도 알 수 있듯, UI 라이브러리는 Presentational 컴포넌트의 모음입니다. 그러면 한 가지 의문이 생깁니다. 위에서 Feature 라이브러리는 한 페이지에서 사용되는 컴포넌트들이 Container, Presentational의 구분 없이 모두 들어간다고 이야기했기 때문이죠.

예를 들어, 위 로그인 페이지의 AuthLayout 컴포넌트의 구현은 아래와 같습니다.

어떻게 봐도 Presentational 컴포넌트입니다. 그렇다면 이 컴포넌트는 account/feature-sign-in 라이브러리에 위치하는 게 맞을까요? 아니면 account/ui-authshared/ui-account 같은 UI 라이브러리에 위치해야 할까요?

기본적으로 한 페이지를 만드는 데 사용하는 모든 컴포넌트들은 같은 Feature 라이브러리에 있어야 합니다. 하지만 둘 이상의 Feature 라이브러리에서 같은 Presentational 컴포넌트를 사용한다면 이를 UI 라이브러리로 분리합니다. 이 때, 분리한 UI 라이브러리를 사용하는 Feature 라이브러리들이 모두 같은 도메인에 있다면 UI 라이브러리도 같은 도메인에 생성합니다. 서로 다른 도메인의 Feature 라이브러리에서 사용한다면 공유 도메인에 생성합니다.

UI 라이브러리는 다른 UI, 혹은 Utility 라이브러리를 참조할 수 있습니다.

Utility Library

위 4가지 분류에 해당하지 않는 모든 모듈들이 Utility 라이브러리로 들어가게 됩니다. Utility 라이브러리는 클래스101의 도메인 모델과 무관하게 테스트 및 배포가 가능한 수준으로 독립적인 기능을 제공해야 합니다.

예를 들어, 우리는 Apollo Client와 인증, 언어, 지역, 페이지네이션 등의 미들웨어를 관리하는 shared/utils-apollo-client, Firebase의 인증 관련 로직을 위한 shared/utils-third-party/firebase, 국제화를 위한 shared/utils-i18n 등의 Utility 라이브러리를 만들었습니다.

Utility 라이브러리는 다른 Utility 라이브러리만 참조할 수 있습니다.

의존 관계 정리

지금까지 설명한 5종류의 라이브러리와 애플리케이션의 의존 관계를 ESLint 규칙으로 정리하면 아래와 같습니다.

지금까지 이야기한 라이브러리들 중 일부를 가져와 만든 의존성 그래프의 모습입니다.

도메인의 분리

팀별로 작은 코드베이스를 갖게 하자는 목적을 달성하기 위해 라이브러리를 도메인별로 분리합니다. 먼저 알아 둘 것은, 도메인은 한번 정하고 나면 다시 바꾸지 못하거나 옮기기 어려운 것이 아닙니다. 오히려 언제든지 바뀔 수 있고 바꾸기도 쉽습니다. Nx의 @nrwl/workspace:move라는 간단한 명령어로 라이브러리의 경로와 import path를 한꺼번에 변경할 수 있습니다. Created By 도메인의 랜딩 페이지(created-by/feature-landing)를 Landing 도메인의 Created By 페이지(landing/feature-created-by)로 옮기려면 아래 명령어만 입력하면 됩니다.

한 팀이 하나의 도메인을 관리할 수도, 여러 개의 도메인을 관리할 수도 있습니다. 앞서 말했듯 도메인은 분리하기 나름입니다. 우리는 기존 애플리케이션을 account, landing, product, browse, user, classroom, creator 도메인으로 나눠 페이지들을 옮겨 오거나 다시 개발하는 작업을 진행하고 있습니다. 특정 도메인에 속한 라이브러리는 해당 도메인 혹은 공유 도메인의 라이브러리만 참조할 수 있습니다.

공유 도메인

위에서 라이브러리의 종류에 대해 설명할 때, Feature 라이브러리는 한 페이지에서 사용되는 컴포넌트들을 모아 둔 라이브러리라고 설명했습니다. 하지만 글로벌 네비게이션이나 상품 카드 등 일부 컴포넌트는 여러 페이지에 걸쳐 재사용됩니다. 아폴로 클라이언트나 Firebase, 혹은 브라우저 관련 유틸리티 라이브러리 등 특정 도메인에 속하기 어려운 라이브러리도 있습니다. 이런 라이브러리들은 shared 도메인에 위치하게 됩니다.

landing 도메인의 shell-us-web 라이브러리에 있는 HomePage 컴포넌트 구현은 아래와 같습니다.

같은 landing 도메인의 feature-home 라이브러리에 있는 컴포넌트들과 shared 도메인의 feature-navigation, feature-promotion, ui-web, ui-system 라이브러리에 있는 컴포넌트들을 사용해 구성했습니다.

landing 도메인에서 account 도메인의 라이브러리를 가져와 사용하려고 하면 오류가 발생합니다.

애플리케이션의 역할

이렇게 라이브러리를 구성하면 애플리케이션의 역할을 설정 관리와 페이지 구성의 두 가지로 좁힐 수 있습니다.

설정 관리

애플리케이션에서 사용하는 라이브러리들에 필요한 설정과 의존성을 제공합니다.

예를 들어, shared/utils-third-party/kakao 라이브러리의 KakaoService는 API 키와 로그인 후 돌아올 주소가 필요합니다. 이런 설정은 애플리케이션마다 다를 수 있기 때문에 KakaoService가 직접 가지고 있기보다는 애플리케이션에서 초기화할 때 주입해 줍니다.

다른 예로, shared/ui-system 라이브러리의 Link 컴포넌트는 애플리케이션이 Gatsby를 사용하는지, Next.js를 사용하는지, 혹은 react-router-dom을 사용하는지와 관계없이 잘 동작해야 하기 때문에 어떤 Link 컴포넌트를 사용해야 할지 모르는 것이 좋습니다. shared/ui-system 라이브러리의 Link 컴포넌트 구현은 다음과 같습니다.

애플리케이션에서는 사용할 Link 컴포넌트를 주입합니다.

페이지 구성

Shell 라이브러리에서 페이지들을 가져와 라우트를 구성합니다. 예를 들어, Next.js로 만들어진 Class101 미국 스토어프론트 애플리케이션은

  • landing/shell-us-web 라이브러리에서 HomePage 컴포넌트를 가져와 / 경로에
  • account/shell-us-web 라이브러리에서 LoginPageRegisterPage 컴포넌트를 가져와 /login/register 경로에
  • browse/shell-us-web 라이브러리에서 SearchPage를 가져와 /search 경로에
  • product/shell-us-web 라이브러리에서 ProductDetailPage를 가져와 /product/:productId 경로에

각각 할당할 수 있습니다.

위 예시의 login.tsx 파일의 코드는 아래와 같습니다.

이렇게 애플리케이션이 간단하기 때문에 이후 마이크로 프론트엔드를 구성하기도 쉽습니다. 필요한 의존성을 정의하고 페이지를 옮겨 주기만 하면 됩니다.

점진적으로 이전하기

늘 그렇지만, 생각의 속도는 실행의 속도보다 훨씬 빠릅니다. 더군다나 개발자들은 항상 더 나은 코드와 구조에 대해 고민하고 있기 때문에 현재 상태에 만족하기란 쉽지 않습니다. 이런 아키텍쳐로 간다고 정해졌어도 실제로 전체 코드베이스가 리팩토링 되기까지는 짧게는 1년, 길면 2~3년 정도의 시간이 필요하겠죠. 때로는 답답하기도 할 겁니다. 언제쯤 끝이 나는지, 진행은 잘 되고 있는 건지 하는 생각이 들어서요.

하지만 생각해보면 지금까지 우리가 해온 일이 대부분 그랬습니다. 언제쯤 끝날지 싶던 쿠버네티스 이전도, 서버 사이드 렌더링도, 디자인 시스템 개발도, 파이프라인 구축도, 아폴로 도입도 꾸준히 하다 보니 잘 마무리됐고 이제는 원래부터 있던 것처럼 잘 쓰고 있죠.

이번 Feature/Shell 라이브러리 이전과 마이크로 프론트엔드 구성 작업 역시 비슷할 것 같습니다. 지금부터 조금씩 꾸준히 해나가면 미래의 우리 동료들은 프론트엔드 개발이 원래 이렇게 쉽고 간단하다고 생각하지 않을까요?

우리와 함께 더 좋은 서비스를 만들기 위해 고민하며 성장하고 싶다면 아래 링크를 통해 지원서를 보내주세요. 피플팀과의 티타임 및 채용 문의(recruit@101.inc)도 언제든 환영입니다. 😉

참고자료

--

--