매일 배운 것을 이해한만큼 정리해봅니다.
회사에서 Frontend Mastery에서 발행하는 글을 요약하고 토론하는 스터디를 만들었다.
이번주 주제는 A Guide to frontend migrations
, 스터디에서는 프론트엔드 마이그레이션을 위한 가이드
로 제목을 정했다. 전문을 번역했다.
프론트엔드 세계는 매우 빠르게 혁신하고 있습니다. 리액트 상태 관리의 새로운 물결 아티클에서 새로운 문제 해결 방식들이 우리가 느끼는 공통적인 페인 포인트를 어떻게 해결하고 우리의 삶을 더 쉽게 만드는지 살펴보았습니다.
상용 가능한 새로운 툴이나 패턴들이 소개되고 있음에도 불구하고, 개발자들에게 ‘레거시’ 기술이나 패턴을 다루는 일은 일상적인 일입니다.
대규모 프로젝트에서는 새로운 툴과 코딩 패턴을 점진적으로 채택합니다.
최근 작업을 진행하지 않은 프로젝트는 여러 차례에 걸쳐 다양하게 중첩된 접근 방식과 툴들이 반영되어 있는 경우가 대부분입니다.
우리가 하나의 특정 문제를 해결하기 위해 한 개 이상의 툴이나 패턴을 적용하기 시작하면 그 때부터 복잡도는 배가 되기 시작합니다. 사용자가 불필요하게 동일한 문제를 여러 번 해결하는 코드를 다운로드하고 실행해야 하기 때문에 성능도 저하됩니다.
규모에 맞게 심플하고 빠른 프론트엔드 프로젝트를 유지하기 위해서는 툴, 패턴 및 프레임워크 간 마이그레이션을 적극적으로 관리해야 합니다.
그렇다면 프론트엔드 툴과 프레임워크 간 마이그레이션을 하는 증명된 베스트 프랙티스는 어떤 것들이 있을까요?
이 가이드에서는 먼저 프론트엔드 프레임워크 마이그레이션에 대한 전략을 살펴본 후 프론트엔드 환경에서 새로운 라이브러리와 툴들의 채택을 관리하는 원칙들을 구체화 해보겠습니다.
프론트엔드 세계에 오래 머물다보면, 어떤 시점에는 마이그레이션 프로젝트를 진행하게 될 것입니다. 가장 흔한 2가지 경우를 살펴봅시다.
이 방식은 대규모 방식의 마이그레이션이며 가장 복잡한 방식이라고 볼 수 있고 다음과 같은 경우일 때 진행할 수 있습니다.
새로운 툴이나 패턴을 도입하게 되었을 때 우리가 일하는 방식이 훨씬 쉬워질 수 있습니다. 작은 프로젝트들의 경우, 빠르게 마이그레이션하는 것은 처리 가능하므로 큰 문제가 되지 않습니다.
규모가 큰 프로젝트의 경우에는 이에 따른 전략이 필요합니다. 새롭게 기술을 채택하는 것으로 인해 새로운 컨셉, 코딩 패턴 혹은 폴더 구조 등 새로운 접근 방식에 맞게 변경하는 등의 또 다른 작업을 필요로 할 수 있기 때문입니다.
타당한 기술 채택이라고 판단할 수 있는 일반적인 상황들을 살펴 보겠습니다.
워터폴(폭포수)개발론 대 애자일 개발론의 비유는 점진적 마이그레이션 방법과 빅뱅 마이그레이션 방법 사이의 트레이오프를 이해하는 좋은 틀입니다.
워터폴 방법론은 대체적으로 예측 가능한 프로젝트일 경우 잘 동작합니다. (위에서 언급했던 Flow에서 Typescript로 마이그레이션 하는 경우도 그러합니다.)
빅뱅 방법론은 변경 사항을 쌓아 올린 후 단번에 업데이트합니다. 이 말은 변경사항이 원자적(atomic)이라는 것인데, 코드 베이스가 장기적으로 과도기 상태에 있지 않도록 합니다.
이 접근법은 위험하긴 합니다. 왜냐면 오랜 기간 동안 어둠(내부적으로 여기에만 매달려서 작업하는 기간)에 머무를 수 있기 때문입니다. 우리가 꽤 긴 기간에 걸쳐 마이그레이션에 시간과 노력을 다해서 변경사항을 내놓기 전까지는 어떤 것도 바뀌지 않는다고 추정해야 합니다.
반대로 애자일 방법론은 빠른 피드백 주기와 작은 이터레이션을 통해서 진행할수록 우리가 잘 모르는 것들을 배워나갈 수 있도록 합니다. 이는 점진적으로 진행하는 방식에서 또 다른 이점입니다.
이 방법론의 위험은 과도기 상태에서 얼마나 머물러야할지 불명확하다는 것입니다. 이렇게 되면 우리는 머리 속에 2개의 다른 접근 방식을 나누고 작업해야 합니다. 복잡성의 주요한 원인이 되는 것이죠.
현실적으로는 다른 우선 순위 높은 일들이 자주 등장하고, 마이그레이션에 들여야 하는 노력을 멈춰야 할 때가 있습니다. 이런 일이 반복되면 누구도 만지고 싶지 않아하는 캐캐묵은 코드가 만들어지고 무언가 변경하기가 위험해집니다. 점진적인 방법은 효율적이지만 적극적으로 관리하지 않으면 부정적인 측면이 강해집니다.
매우 크고 활발하게 피처 개발이 되는 코드 베이스에서 마이그레이션을 위해 모든 작업을 멈추는 것은 실행하기 매우 어렵습니다. 그렇기 때문에 점진적인 마이그레이션을 기본으로 가져가는 것이 대부분 상황에서 실용적인 선택일 것입니다.
가끔 완전히 다른 프레임워크로 마이그레이션을 해야하거나 기존 프로젝트에 레거시 프레임워크를 통합해야 할 때가 있습니다. 여기서는 Backbone SPA을 React로 작성된 기존 프로젝트에 마이그레이션하는 것을 예제로 다루겠습니다.
여기에 관해서는 흥미로운 토론이 매우 많습니다. 어떤 사람들은 당신의 소프트웨어를 절대로 재작성하지 말라는 입장을 취하는 것으로 유명합니다. 반면 어떤 사람들은 가끔 처음부터 다시 써내려가는 것이 좋다고 생각하며 그 의견에 동의하지 않습니다.
여기서 이 토론에 개입하지는 않겠습니다. 그러나 새로운 프론트엔드 프레임워크로 마이그레이션할 때 놓치게 되는 몇 가지 점들에 대해서 알아보겠습니다.
2가지 일반적인 프로그레시브 마이그레이션 전략을 살펴봅시다.
이 방법은 탑다운(하향식) 접근 방식으로 주로 백엔드 서비스 마이그레이션에 쓰이는 strangler pattern을 각색한 것이라고 생각하면 됩니다.
먼저 어플리케이션의 쉘을 새로운 스택으로 바꾸고 라우트 레벨에서 하나씩 레거시 페이지를 이동시키는 방법입니다. 예시에서 Shell
은 페이지 렌더링을 관리하는 컨테이너를 가리킵니다. 라우터, 탑레벨 페이지 레이아웃 컴포넌트 등을 말합니다. 또한 공유 데이터 계층(해당하는 경우)과 분석 및 모니터링 등을 포함하도록 확장할 수 있습니다.
레거시 Backbone SPA를 리액트로 이주시키는 예시를 살펴 보겠습니다.
기존의 리액트 어플리케이션이 셋팅되어 있다고 가정하고, 우리는 이제 백본 페이지를 리액트 컴포넌트로 렌더시키는 추상화 작업을 진행해보겠습니다.
아래는 어떤 식으로 보이게 될지에 대한 의사코드입니다.
const LegacyBackbonePage = ({ pageKey }) => {
// 필요한 모든 이펙트에 동작
// 예) 레거시를 리액트 컴포넌트로 보낼 수 있는 이벤트를 구독하는 것
// can also render any common infrastructure components and set up analytics etc
// ..
// 페이지 렌덕 되었을 때 백본 SPA가 동작할 수 있도록 엘리먼트 컨테이너를 렌더
// 레거시 백본 앱에서는 보통 <body> 부분
return <div id="legacy-backbone-root" />
}
바깥 쪽에서 보면 리액트 컴포넌트입니다. 이 컴포넌트는 내부에서 어떤 설정을 할 수 있고, 백본 페이지 쪽으로 렌더시키기 위한 엘리멘트 컨테이너를 렌더시킵니다.
모든 것을 함께 담기 위해서 많은 경우 레거시 코드를 리액트 레포의 같은 디렉토리에 복사해오는 것이 쉽습니다. 셋업은 쉬운 부분입니다. 운이 좋다면 레거시 페이지 작동 방식에 의존해 페이지들이 “바로 작동” 할 수도 있습니다. 그러나 대부분의 경우는 그렇지 않습니다.
이러한 구조의 레거시 마이그레이션의 어려운 부분은 한 번 레거시 코드에서 엘리먼트를 차지하고 자기 자신을 렌더시키기 시작하면 하나 하나 해결해야 하는 이상한 버그가 발생한다는 것입니다.
보통의 경우, 클라이언트 사이드의 라우터에 충돌이 있을 때 중간에서 중재가 필요한 문제가 발생합니다. 사이드 이펙트를 발생시키는 글로벌 스타일이나 스크립트를 관리하는 것 뿐만 아니라 이러한 어려움들은 레거시 코드를 리액트쪽으로 로딩하는 결과로 동작하게 됩니다.
우선 이런 문제들은 제쳐두도록 하겠습니다.
이 접근 방식은 쉘 부분이 새로운 스택으로 이주하게 되면 그 이후 혜택을 받을 수 있습니다. 그리고 레거시와 새로운 구조 사이의 명확한 경계선을 만들어줍니다. 피처 플래그 단위로 컨트롤할 수 있도록 라우팅 레벨에서 새롭게 이주한 페이지들을 점차적으로 릴리즈를 할 수 있게 됩니다.
이 방법은 바텀업(상향식) 접근 방식으로 어플리케이션의 쉘 부분과 진입점이 동일하게 유지되면서 안에서부터 바깥쪽으로 레거시가 조각마다 교체되는 것입니다.
백본에서 리액트로 마이그레이션하는 예시로 돌아가서, 백본 코드에서 새로운 리액트 root들을 만들고 페이지의 섹션에 렌더시키는 방법을 마련하는 것이 이러한 방법입니다.
// .. 이런 저런 임포트
// 리액트 컴포넌트를 백본 쪽으로 임포트
const ComponentWrittenInReact = require('/path/to/cool-new-component')
// ...
// 레거시 백본 어플리케이션 코드 덩어리들
// ...
// 엘리먼트 콘테이너에 새로운 컴포넌트를 렌더
ReactDOM.render(
// JSX가 아니므로 imperative API를 사용
React.createElement(
ComponentWrittenInReact,
// 레거시에서 전달한 컴포넌트 props, 핸들러 그리고 함수들을 전달
{ someData, someCallback }
),
containerElement // 리액트가 렌더하면서 차지하게 될 루트 HTML 엘리먼트
)
이 부분에서 우리는 초기 데이터와 리액트 컴포넌트에서 호출할 수 있는 함수 등을 전달합니다.
현실적으로 레거시로 작성된 코드 작성된 컴포넌트와 새롭게 작성된 코드 사이에서 조화롭게 커뮤니케이션하는 것은 쉽지 않은 일입니다. 하나 간단한 방법이 있다면 커스텀 이벤트를 사용하는 것인데, 이는 계속 디커플링(쪼개기)를 유발합니다. 이렇게 되면 디버깅하는 것이 고통스러워질 수 있습니다.
바깥에서 안으로 마이그레이션 하는 방식은 우리가 새로운 프레임워크로 마이그레이션 하는 것이 확실하다면 더 나은 방법일 수 있습니다. 가장 먼저 초기 셸과 인프라를 구축해야 하기 때문에 항상 실현 가능한 방법이 아닐 수도 있습니다. 그러나 한 번 작업해두면 일찍이 새로운 프레임워크의 혜택을 받을 수 있기 때문에 덕을 보게 됩니다. 이것이 어떤 모멘텀이 되어서 오래된 것과 새로운 것 사이에 라우터 레벨의 명확한 경계선을 가질 수 있게 됩니다.
안에서 바깥으로 마이그레이션 하는 방식은 완전한 마이그레이션을 진행할지 말지 탐색하는 과정에서 좋은 방법입니다. 배경이 되는 쉘 구조나 라우팅 같은 인프라를 건드리지 않고 쉽게 시작할 수 있는 방법이기 때문입니다. 실험적으로 어떤 섹션을 교체해볼 수 있고 라우트 단위로 계속 교체를 시도할 수 있습니다. 안에서 바깥으로 마이그레이션하는 실험의 결과로 새로운 프레임워크가 가치있다고 느낀다면, 탑다운(상향식) 마이그레이션 방식으로 전환하여 마이그레이션을 계속 진행할 수 있습니다.
어떤 경우든 테스팅은 쉽지 않습니다. 페이지나 컴포넌트가 올바르게 그려졌는지와 실제 인스턴스 상에서 잘 동작하는지 e2e 테스트에 기반해서 확인하는 것이 효율적일 때가 많습니다.
런타임 중에 껐다가 켰다가 할 수 있는 피처 플래그 툴을 사용해서 새롭게 이주한 조각들을 연결하는 것도 방법입니다. 이러한 방법들을 통해서 긴 기간에 걸쳐서 어둠에 머물지 않게 해주고 점차적으로 마이그레이션한 조각들을 릴리즈 할 수 있습니다.
우리는 장기적으로 우리 삶을 쉽게 만들어 줄 수 있는 새로운 툴이나 패턴들을 도입하려고 합니다. 이 때 중단기적인(그리고 종종 무한정까지 가는) 과도기가 함께 합니다.
여기 대규모 프로젝트에서 일반적으로 놓치는 것들이 있습니다:
{ Button as RenamedButton }
같은 방식으로 이름을 재작성하는 방식은 컨텍스트를 잃게 됩니다.꽤 오랜 기간에 걸쳐 형성된 대규모 프론트엔드 코드 베이스에는 다른 도구들과 코딩 패턴들 그리고 심지어는 프레임워크까지 다량의 과도기 상태가 누적되어 있을 수 있습니다. 이는 장기적으로 더 나은 방식으로 코드를 작성하기 위한 노력의 일환으로 어떤 방식을 새롭게 도입하게 되는 것임으로 피할 수 없는 현상입니다.
그렇기 때문에 대규모 프론트엔드 코드 베이스에서 프론트엔드 구조의 복잡도를 관리하고 성능과 관련된 악몽을 피하기 위해서 다양한 마이그레이션 과정을 관리하는 것이 매우 중요합니다.
오늘 쿨하게 보이는 접근 방식이 내일의 레거시가 될 수 있습니다. 새로운 패턴이나 툴을 코드 베이스에 도입하게 될 때마다 마이그레이션과 예전 방식을 사용하지 않도록 하는 계획을 세우면 유용합니다.
큰 규모의 프레임워크 마이그레이션의 경우, 만약 해당 프레임워크 도입이 확실하다면 보통 탑다운(상향식) 바깥에서부터 안으로 진행하는 것이 낫습니다.
바텀업(하향식) 안에서 바깥으로 진행하는 방식은 어플리케이션 내 작은 조각들을 새로운 프레임워크로 하나씩 옮겨보면서 마이그레이션 진행을 할지 말지 탐색하는 단계에서 더 잘 작동합니다.
이 가이드에서 다룬 것 외에도 많은 방법들이 있을 것입니다. 마이크로 프론트엔드에 대한 글도 점차적으로 커지고 있는 프레임워크의 마이그레이션을 핸들링하는데 잠재적인 도움이 될 것입니다.