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

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

마이크로 프론트엔드

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

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

팀별로 작은 코드베이스

독립적인 배포와 롤백

모노레포 with Nx

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

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

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

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

Nx Basics

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

Feature Library

클래스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

위에서 한 페이지에 사용되는 모든 컴포넌트들이 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 라이브러리입니다. (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

예를 들어, 위 로그인 페이지의 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

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

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

의존 관계 정리

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

도메인의 분리

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

공유 도메인

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 컴포넌트를 주입합니다.

페이지 구성

  • 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 파일의 코드는 아래와 같습니다.

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

점진적으로 이전하기

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

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

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

참고자료

CLASS101’s mission is to build the world where everyone can live doing what they truly love.