본문 바로가기

프로그래밍/웹

혜움 레포트 프론트 개선 - 2

 지난 이야기...

 

안녕하세요. 혜움랩스에서 개발자로 일하고 있는 문현제입니다. 이번 글에서는 전편에 이어서 프론트 개선 작업을 하면서 마주쳤던 어려움들과 해결 과정에 대해서 적어보려고 합니다.

Frontend 마이그레이션 전략

장고템플릿과 Vue 코드가 강하게 결합된 기존 형태에서, 온전한 SPA로 프론트 앱을 어떻게 분리시킬지 결정이 필요했습니다. 기존에 동작하고 있는 서비스를 문제없이 사용자들에게 제공하면서 신규 프레임워크 Vue3도 적용할 수 있어야 했습니다.

여러 방식들을 리서치 해본 결과, path 별로 다른 앱을 보여주는 방식, 레거시 앱을 전부 신규 앱으로 이전하는 방식, 하나의 화면에 두 앱이 표시되는 multi-app 방식을 고려했습니다.

path별 앱 분리

path 별로 앱을 분리하는 방식은 신규 코드와 기존 코드의 간섭 없이 새로운 프로젝트를 시작할 수 있는 좋은 방법입니다. 다만 기존에 장고 템플릿으로 구현되어 있던 공통 컴포넌트, 인증 로직 등을 새로운 앱에 중복으로 구현 해주어야 하고, 추후 변경사항이 생겼을 때 수정작업도 두 번 해주어야 합니다. 또한 다른 앱이 제공하는 페이지로 이동하는 경우, SPA router를 사용할 수 없고 location 변경을 통해 페이지를 이동해야 합니다. 만약 앱 간 이동이 잦고, 앱 초기화 시에 인증 로직이 복잡하다면 화면을 이동할 때마다 로딩이 될 때까지 기다려야 해서 사용자 경험이 나빠질 수 있습니다.

Vue3로 일괄 마이그레이션

일괄 마이그레이션은 말 그대로 기존 코드를 전부 재작성하는 방식입니다. 작업이 전부 완료 되었을 때 하나의 앱으로 깔끔하게 정리되기 복잡도가 낮아집니다. 다만 기존 코드의 양이 많고 서비스의 기획이 잘 정리되어 있지 않은 현 상황에서, 이전 기능을 전부 파악하여 재작성하고 테스트 하는데에 너무 많은 시간이 걸리게 될 것 같았습니다. 또한, 이 방식은 중간에 배포가 불가능한 방식이기 때문에 정확히 산정할 수도 없는 오랜 기간동안 배포 없이 개발을 해야 합니다. 빠르게 급변하는 비지니스 요구사항을 대응하기 어렵기 때문에, 현실적으로 선택하기 어려운 방식이라고 판단했습니다.

multi-app 방식

multi-app 방식은 한 화면에 두 개의 SPA가 렌더링 되는 방식입니다. 이 구조에서는 두 SPA가 한 화면에서 동작하기 때문에 서비스의 복잡도가 매우 올라가게 됩니다. 두 앱에 존재하는 두 개의 router에서 모두 주소 변경이 일어날 수 있기 때문에, 각 앱의 주소가 어긋나지 않도록 동기화 해주는 작업이 필요합니다. 그리고 인증이나 테마 같은 정보도 각 앱에서 전부 접근할 수 있도록 설정해주어야 합니다. 다만 multi-app 구성을 완료 한 이후에는 모든 레거시 코드를 마이그레이션하지 않아도 배포가 가능하고, 중복 작업을 하지 않고 기존 기능 유지보수, 신규 기능 개발을 할 수 있는 장점이 있습니다.

결정을 할 때 중요하게 생각했던 점들은 다음과 같습니다.

  • 기존 서비스는 정상적으로 고객에게 제공되야 한다.
  • 최대한 빠른 시일 내에 배포가 가능해야 한다.
  • 레거시 코드의 수정은 최소화 해야 한다.
  • 변경에 따른 중복 작업을 최소화 해야 한다.

이런 점들을 고민하여 결국 세 번째 방식인 multi-app 구성으로 점진적인 마이그레이션을 진행하기로 했습니다.

Wrapper 컴포넌트 작성

<!-- LegacyWrapper.vue -->
<template>
  <div>
    <div id="app" />
  </div>
</template>

<script lang="ts">
import { defineComponent, onMounted } from 'vue';

import useRenderLegacyApp from './useRenderLegacyApp';
import useCommonScriptAndStylesheet from './useCommonScriptAndStylesheet';

export default defineComponent({
  name: 'LegacyWrapper',
  setup() {
    useCommonScriptAndStylesheet();

    onMounted(() => {
      useRenderLegacyApp();
    });

    return {};
  },
});
</script>

Vue3 앱 내부에서 레거시 앱을 렌더링하기 위한 Wrapper 컴포넌트입니다. 실제 코드는 조금더 세세한 동작을 처리하고 있지만 핵심 로직만 표현했습니다. Wrapper 컴포넌트는 SPA root로 사용하는 div 엘리먼트만 가지고 있고, Wrapper 컴포넌트가 마운트 된 이후 동적으로 번들링된 js 파일을 실행하는 구조입니다. 이 로직은 Vue3의 composition api를 사용하여 useRenderLegacyApp로 추상화 되어 있습니다.

레거시 앱을 표시해야하는 경우에는 이 Wrapper 컴포넌트를 이용하여 렌더링하고, 나머지는 Vue3로 신규 작성된 페이지를 렌더링하도록 하여서 App in App 구조의 microfrontend를 구현했습니다.

Route 동기화

multi-app 구조에서 각 앱에서 router를 별개로 사용하고, 각 router에서 주소 변경이 일어난다면 route 정보를 동기화 해주어야하는 문제가 발생합니다. 레거시 router에서 router push, replace, back 동작이 일어나는 경우, 신규 router에서 push, replace, back 등의 동작이 일어나는 경우, 브라우저 뒤로가기, 앞으로가기 등의 동작이 일어나는 경우 등 모든 케이스에서 문제 없이 화면이 표시되도록 해야합니다.

vue-router 라이브러리는 몇 가지 mode가 존재하는데, browser history api를 사용하는 history 모드, 모든 javascript 환경에서 작동하는 abstract 모드가 존재합니다. 마이그레이션 테스트를 해보면서 양쪽 router의 모드를 history로 설정하는 경우, 브라우저 뒤로가기에 대한 핸들러가 각각 작동하여 무한 루프가 발생하거나(!!) 정상적으로 화면이 표시되지 않는 문제가 있었습니다.

그래서 메인 앱인 Vue3쪽 router만 history 모드로 설정하고 레거시 앱의 router는 abstract 모드를 사용하여 신규 앱쪽 route 변경을 따라가며 동기화하도록 했습니다. 동기화를 위해 다른 앱의 router를 조작하는 경우, 라우터 조작이 연쇄적으로 일어나면서 무한 루프가 발생하는 경우가 있기 때문에, 연쇄된 라우터 조작을 막을 수 있도록 vue-router 코드를 전부 프로젝트 안에 복사하여 동기화를 위한 전용 메소드를 추가하여 해결했습니다.

ALB 504 에러해결

프론트쪽 구성 이외에도 nginx를 proxy로 사용하면서 겪은 문제 상황들이 있었는데요, 주로 AWS ALB 504 에러가 발생하는 문제였습니다. AWS ALB 504에러는 AWS LoadBalancer에서 뒤쪽에 있는 서버에서 응답을 제한시간 안에 받지 못하는 경우 발생합니다. 인프라를 구성하면서 이 ALB 504 에러를 발생시키는 두 가지 요인이 있었습니다.

여러 layer 간의 keepalive timeout 설정

네트워크 구성에서 앞 쪽에 있는 레이어의 keepalive timeout이 뒤 쪽에 있는 레이어의 keepalive timeout 보다 길게 설정되어 있는 경우 ALB 504 에러가 발생합니다.

예를 들어 AWS ALB의 keepalive timeout이 30초로 설정되어 있고, 뒤에 붙어있는 nginx의 keepalive timeout이 10초로 설정되어 있는 경우 앞 쪽에 있는 ALB는 연결이 살아있다고 판단하지만 nginx에서 연결을 끊어버린 경우가 발생합니다. 이 경우에는 ALB에서 해당 연결로 리퀘스트를 보내도 당연히 리스폰스를 받을 수 없기 때문에 ALB 504 에러가 발생합니다.

이를 방지하기 위해서는 앞쪽에 있는 레이어(위 예제에서는 ALB)의 keepalive timeout이 뒤쪽에 있는 레이어(위 예제에서는 nginx)의 keepalive timeout보다 짧게 설정되어야 합니다. 네트워크 레이어가 많다면 타임아웃이 10s-12s-14s-16s 이렇게 뒤쪽으로 갈 수록 길게 설정해야 문제가 발생하지 않습니다.

nginx upstream dynamic resolve 이슈

ALB 504를 발생시키는 또 다른 문제는 바로 nginx의 upstream 기능에 있었습니다. nginx의 upstream은 프록시 처리를 위한 좋은 기능입니다만, upstream의 대상으로 ip가 아닌 domain을 입력하는 경우 문제가 발생합니다. 왜냐하면 nginx에서는 upstream의 동작을 최적화하기 위해 첫 실행 시에 domain을 resolve하여 목적지 ip들을 캐싱하고 있기 때문입니다.

도메인에 연결된 ip목적지가 변경이 없다면 상관이 없겠으나 AWS LoadBalancer처럼 동적으로 목적지 ip가 계속 바뀌는 경우 문제가 됩니다. LB에서 할당이 해제된 ip로 요청이 전달되어서 응답을 받을 수 없거나, 심지어는 전혀 의도하지 않은 대상으로 요청이 전달되어 이상한 응답이 오는 경우도 발생합니다.

upstream의 도메인의 dynamic resolve기능을 사용하려면 유료 서비스(...)인 nginx plus를 사용하는 방법이 있습니다. 또 다른 방법으로는 taobao의 nginx 포크 프로젝트인 Tengine을 사용하는 것이 있습니다. Tengine에서는 기본적으로 dynamic resolve 기능을 제공합니다. 커뮤니티에서 작성한 nginx 모듈이 몇몇 존재하지만 기능 및 동작을 보장하는 라이브러리는 아니었습니다.

현재 구성에서 upstream 기능이 반드시 필요한 것은 아니어서 결국 upstream을 포기하고 inline으로 목적지 도메인을 처리하도록 nginx 설정을 수정하여 해결했습니다.

마치며

짧지 않은 기간 동안 배포와 롤백을 반복하며 좌절하기도 했지만, 이런 기술적인 과제는 항상 개발자를 성장시키는 좋은 동력인 것 같습니다. 많은 동료들의 도움을 받으며 기술적인 문제를 해결하는 과정은 정말 멋진 경험이었습니다.