회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
*FEConf2023에서 발표한 <FEConf 2023 [B4] React Native, Metro를 넘어서>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 Metro에서 ESBuild로 번들러를 전환하게 된 배경을 소개하고 나아가 번들러가 무엇인지와 번들러의 역할을 수행하기 위한 기능인 Resolution과 Load, Optimization을 알아봅니다. 2회에서는 Optimization의 Minification과 Tree Shaking를 소개하고, 토스에서 Metro를 ESBuild로 바꿨던 여정을 이야기합니다. 본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다. 발표 자료는 FEConf2023 홈페이지에서 다운로드할 수 있습니다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
*FEConf2023에서 발표한 <FEConf 2023 [B4] React Native, Metro를 넘어서>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 Metro에서 ESBuild로 번들러를 전환하게 된 배경을 소개하고 나아가 번들러가 무엇인지와 번들러의 역할을 수행하기 위한 기능인 Resolution과 Load, Optimization을 알아봅니다. 2회에서는 Optimization의 Minification과 Tree Shaking를 소개하고, 토스에서 Metro를 ESBuild로 바꿨던 여정을 이야기합니다. 본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다. 발표 자료는 FEConf2023 홈페이지에서 다운로드할 수 있습니다.
▶2024년 FEConf 라이트닝 토크 발표자 모집 중
FEConf2023에서 발표된 ‘React Native, Metro를 넘어서/박서진 토스 프론트엔드 개발자
‘React Native, Metro를 넘어서’ 1회에서는 번들러가 무엇인지와 번들러의 역할로 Resolution, Load, Optimization에 관해 소개했습니다. 1부 마지막에서 파일 크기를 줄이기 위한 Optimizaton을 간단히 이야기하고, 파일 크기를 줄이는 테크닉에는 크게 Minification과 Tree Shaking이 있다고 말씀드렸죠. 이 글인 2부에서는 Metro와 ESBuild의 차이를 조금 더 명확하게 알기 위해서, 이 각각의 테크닉이 구체적으로 어떤 역할을 하는지 살펴보겠습니다. 그리고 토스에서 Metro를 ESBuild로 바꾼 여정도 소개합니다.
먼저 Minification의 Compression입니다. Compression은 말 그대로 소스 코드에서 압축할 수 있는 텍스트를 최대한 압축하는 걸 말합니다. 예를 들어서 ‘undefined’라고 하는 값은 ‘void 0’이라고 표현했을 때 세 글자가 줄게 됩니다. 비슷하게 ‘2 + 3’을 ‘5’로 줄인다면 파일 크기를 좀 더 줄일 수 있습니다. 그 외에 조건문이나 아니면 ‘Infinity’ 같은 코드도 특수한 처리를 하면 짧게 줄일 수 있습니다. 이 외에도, 이것보다 사실 훨씬 복잡한, 그렇지만 동일한 코드 결과물을 내는 다양한 압축 방법론이 있습니다. 궁금하신 분들은 ‘Terser’라고 하는 유명한 오픈소스 프로젝트를 참고하시기를 바랍니다.
더 나아가서, Minification은 두 번째로 Mangling이라고 하는 테크닉이 있습니다. 이것은 변수나 클래스 아니면 함수의 이름을 바꿈으로써 파일 크기를 줄이는 테크닉입니다. 개발자들은 소스 코드를 작성할 때 읽는 사람이 이해하기 쉽도록 다소 길더라도 좀 더 설명적인 이름을 붙이고는 합니다.
하지만, 이렇게 붙여진 이름은 결과물의 파일 크기가 굉장히 커지도록 만듭니다. 그래서 번들러는 기본적으로 이렇게 이름을 짧게 바꿔줌으로써 파일 크기를 줄여줍니다. 아래 예시에서도 ‘num 1’과 ‘num 2’를 각각 ‘n’과 ‘r’로 바꿈으로써 상당히 줄어든 것을 볼 수가 있습니다.
번들러가 만든 배포 결과물을 보는 경우가 많을 것 같은데, 파일을 열어보면 이렇게 이름이 읽기 어려운 형태로 많이 바뀌어 있는 것을 보실 수가 있습니다. 파일 크기를 줄이기 위해서입니다.
Tree Shaking은 기존의 Minification과는 약간 접근이 다릅니다. 기존의 Minification은 코드 동작을 최대한 유지한 상태로 코드를 압축하는 데에만 머물렀다면 Tree Shaking은 import 된 코드 가운데 안 쓰는 코드를 선제적으로 삭제하는 테크닉입니다. 안 쓰는 코드를 삭제한다니까 정말 좋을 것 같은데, 실제로는 Tree Shaking은 굉장히 어려운 작업입니다.
왜 어려운지 두 가지 예시를 통해서 이거 한번 살펴보겠습니다. 아래 예시를 보시면, 먼저, 첫 번째 코드는 ‘toss/utils’ 함수 라이브러리에서 ‘add’라고 하는 함수를 가져와서 쓰는 코드입니다. 이 코드를 살펴보면 ‘add’ 함수 말고도 다른 함수들은 안전하게 삭제할 수 있을 것 같이 생겼습니다.
두 번째 코드는 약간 다릅니다. 이 코드는 어떤 라이브러리를 초기화하는 코드를 import를 하고 있지만, 그런데 그 import 문에서 아무것도 가져오고 있지 않습니다. 여기서 두 번째 코드에서는 아무것도 가져오고 있지는 않으니, 그냥 import 문을 삭제해 버려도 되는지 의문이 생깁니다. 왠지 안 될 것 같습니다.
두 번째 예시를 보면 약간씩 불안해지기 시작합니다. ‘toss/utils’에서 ‘add’를 가져오는 첫 번째 예시에서도 함부로 뭔가 안 쓸 것 같이 보이는 코드를 지웠다가 예기치 못한 문제가 발생하면 어떻게 할지 굉장히 불안해집니다. 그래서 Tree Shaking은 번들러가 구현하는 데 있어서 굉장히 어려운 점들이 많은 기능일 것이라고 추측할 수 있습니다.
그래서 일반적으로는 라이브러리를 만들 때 번들러가 안전하게 코드를 삭제시켜 줄 수 있도록 ‘sideEffects’라고 하는 필드를 사용하는데, ‘sideEffects’는 번들러가 함부로 지워서는 안 되는 그런 파일 목록을 지정합니다.
예를 들어서, 번들러는 ‘sideEffects’가 false이면, 그 라이브러리 안에서 어떤 파일이 안 쓰였다고 하면 무조건 안전하게 지울 수가 있습니다. 그렇지 않고 ‘sideEffects’에 지우면 안 되는 그런 파일 목록이 지정되어 있다고 한다면, 그 파일들은 import 됐는데, 안 쓰고 있다고 하더라도 지워지지는 않습니다.
게다가 이 예시는 Tree Shaking을 가장 쉽게 쓸 수 있는 힌트입니다. 실제 번들러는 이것보다 훨씬 더 복잡한 과정을 거쳐서 Tree Shaking을 수행하기 때문에 번들러 입장에서는 구현하기가 상당히 까다로운 기능이라고 할 수 있습니다.
그래서 앞서 봤던 코드들 가운데, 코드를 그냥 단순히 더 짧게 만드는 Minification은 대부분의 번들러에서 ‘Terser’와 같은 오픈소스 라이브러리를 사용해서 기본적으로 지원합니다.
반면 Tree Shaking은 상당히 구현하기가 까다로운 어떤 그런 기능이기 때문에 일부 번들러에서는 지원하지 못하고 있습니다. 대표적으로 Metro는 아직 Tree Shaking을 지원하고 있지 않습니다. 경우에 따라서 JavaScript 파일 크기가 커질 수 있습니다. 그런데 이제 저희가 옮기려고 하는 ESBuild는 이 기능을 기본으로 지원합니다.
지금까지 번들러가 하는 세 가지의 역할을 살펴보았습니다. 어려운 내용들도 나왔기 때문에 한 번 정리하고 넘어가겠습니다. 번들러는 하나의 JavaScript 번들을 만들기 위해서 Resloution, Load, Optimization의 세 가지 역할을 합니다.
먼저 Resloution은 import되는 그런 모호한 파일 경로들을 정확한 파일 경로로 바꿔줍니다. 그래서 번들링된 결과물에 어떤 파일들이 포함되는지를 정확하게 알 수 있도록 해줍니다.
다음으로 Load는 그 파일들이 TypeScript나 Flow처럼 JavaScript가 아니라고 하더라도 JavaScript로 변환해 주는 역할을 합니다. 그래서 Resloution이랑 Load를 하면 번들에 어떤 파일들이 포함되는지를 전부 알 수 있고 그것을 모두 JavaScript로 변환시키는 것이므로, 완벽한 번들을 만들 수가 있습니다.
그런데 그 번들 자체만으로는 번들의 결과가 상당히 크게 되니까 이를 최적화하는 작업이 필요한데, Optimization에서 Minification 그리고 Tree Shaking을 통해서 결과물의 크기를 줄이게 됩니다.
지금까지 살펴봤던 것과 같이 Metro와 ESBuild는 모두 번들러로서 기본적으로 Resloution, Load, 그리고 Optimization 세 가지 역할을 한다고 할 수가 있습니다. 이 각각의 역할들에 대한 설정들을 동일하게 맞춘다고 한다면 Metro가 하는 역할을 거의 그대로 ESBuild로 옮길 수 있다는 말이 됩니다. 그래서 토스 프론트 플랫폼 팀에서는 이렇게 생각했습니다. “기본적으로 Metro랑 ESBuild는 모양만 다를 뿐 똑같은 역할을 하는데 설정을 거의 그대로 옮긴다면 실제로 React Native에서도 ESBuild를 사용할 수 있지 않을까?”
이런 생각에서 출발해서 실제로 레퍼런스를 찾아보았습니다. ‘react-native-esbuild’라고 하는 레포지토리가 있었습니다. 실제로 Metro에서 사용하는 굉장히 다양한 설정들을 ESBuild에서도 사용할 수 있도록 옮긴 것이었습니다.
이 레포지토리를 참조해서 설정을 적용한다면 동일하게 옮길 수 있을 거라는 확신이 있었습니다. 그래서 토스 팀에서는 이 레포스토리의 소스 코드를 살펴보면서 어떤 코드들이 있는지 분석했습니다.
저희가 분석 과정에서 알게 된 점을 이야기하려는데요.. 코드의 ‘line by line’까지는 아니고 번들러의 세 가지 역할인 Resloution, Load, 그리고 Optimization 각각의 관점에서 Metro의 설정을 어떻게 ESBuild로 옮겼는지, 그 핵심적인 코드들을 위주로 소개해 드리려고 합니다. 혹시라도 설명을 들어보시고 더 궁금하신 점이 생기셨다면, 깃허브의 ‘react-native-esbuild’ 레포지토리를 참고하시기 바랍니다.
먼저 Resolution입니다. Resolution 관점에서 Metro랑 ESBuild가 가장 달랐던 점은 확장자입니다. 웹에서는 ‘.ts’, ‘.tsx’, ‘.js’처럼 굉장히 단순한 확장자만을 사용하는 반면, React Native에서는 조금 다르게 ‘.ios.js’, ‘android.js’, ‘.native.js’ 같은 확장자를 사용하곤 했습니다. iOS에서만 실행되는 코드는 ‘ios.js’라고 명시하는 식입니다.
그래서 ESBuild에서도 이 특별한 확장자들에 대응해야 합니다. 이 부분은 다행히도 앞서 소개해 드렸던 ‘resolveExtensionstensions’ 설정으로 대응할 수 있었습니다. 예를 들어, 첨부한 소스 코드에서와 같이 ‘resolveExtensionstensions’를 작성하면 ‘.ios’에서 ‘.native’ 그리고 그렇게 해서 없는 것까지 순서대로 찾는 식이었습니다.
다음으로 신경 써야 했던 것은 Load입니다. React Native에서는 특이한 문법들을 많이 사용합니다. 예를 들어서 React Native의 코어는 Flow를 사용합니다. 그리고 또 애니메이션을 사용하는 React Native의 ‘reanimated’라고 하는 라이브러리에서는 ‘worklet’이라고 하는 특수한 함수를 정의합니다.
이런 문법에 대응하기 위해서는 특별하게 Load를 하는 방법을 정의를 해야 했습니다. 아래 소스 코드를 한번 살펴보시면, 모든 프로젝트 파일을 대상으로 Flow 문법을 포함한 경우에는 Babel로 컴파일을 넣어서 Flow 문법을 실행할 수 있는 JavaScript 코드로 바꿔주었습니다. 또 ‘reanimated’의 ‘worklet’ 함수를 사용하는 경우에는 특수한 플러그인을 이용해서 함수를 컴파일했습니다. 이렇게 Flow와 TypeScript 그리고 ‘worklet’ 함수에 대응하도록 하니까 특수한 문법에도 안전하게 대응할 수 있었습니다.
특별하게 설정하지 않아도 자연스럽게 이득을 볼 수 있는 경우도 있었습니다. 바로 Optimization입니다. Metro는 이제 ESbuild는 Metro와 다르게 Tree Shaking을 지원하기 때문에 안 쓰는 import들을 지움으로써 파일 크기를 크게 감소시킬 수 있었습니다.
Minification, 다시 말해서 파일 크기 압축의 경우에는 양쪽 번들러에서 모두 다 지원했습니다. 그래서 토스에서는 ESBuild를 도입하는 것만으로도 JavaScript의 파일 크기를 크게 줄일 가 있었습니다.
그 외로 Metro가 특별하게 해주는 것들 몇 개를 대응을 해야 했습니다.
대표적으로는 ‘InitializeCore.js’라는, React Native의 코어 영역을 초기화시켜 주는 스크립트를 어떤 코드보다 먼저 실행시켰습니다. 그리고, ‘hermes’ 엔진 같은, 엔진에서 지원되지 않는 JavaScript의 내장 객체 혹은 API를 위해서 ‘Polyfill’를 추가했습니다.
가장 기억에 남는 결과는 빌드가 굉장히 일관적으로 변했다는 것입니다. Metro는 번들러를 구현하는 과정에서 내부적으로 많은 캐싱 로직을 쓰고 있었습니다. 그런데 캐싱이 제때 풀리지 않는 경우가 많아서 수동으로 ‘--reset-cache’라고 하는 옵션을 이용해서 빌드를 해줘야 했습니다. 하지만 ESBuild는 원래부터 워낙 빠르기도 하고, 캐싱이 문제가 되지는 않아서 더 이상 빌드할 때 해당 옵션을 계속 염두에 두지 않아도 됐습니다. 개발 경험이 굉장히 좋아진 것입니다.
두 번째는 빌드 속도가 변했습니다. Metro는 JavaScript로 구현이 되어 있고 ESBuild만큼 동시성을 사용하고 있지도 않아서 빌드 속도가 많이 느렸습니다. 그런데 ESBuild는 Go 언어를 사용하고 있고 Goroutine을 이용해서 동시성을 강력하게 사용하고 있기 때문에 빌드 속도를 거의 10분의 1 가까이 줄일 수 있었습니다. 개발 사이클이 많이 짧아지게 된 것입니다.
마지막으로는, ESBuild에서는 Metro와 다르게 Tree Shaking을 기본적으로 지원했습니다. 그래서 안 쓰는 코드를 훨씬 더 적극적으로 삭제하는 게 가능해졌습니다. 덕분에 토스에서는 React Native의 JavaScript 파일 크기를 크게 줄일 수 있었습니다.
대표적으로 토스에서 사용하고 있는 ‘내 포인트 서비스’의 React Native 번들의 경우, hermes 바이트 코드 기준으로 원래 Metro에서는 1.8MB 정도 되는 꽤 큰 크기의 사이즈였는데 ESBuild에서는 0.6MB 정도로 크기를 크게 줄일 수 있었습니다.
지금까지 어떻게 토스가 Metro에서 ESBuild로 옮기자는 결정하게 됐으며, 대략적으로나마 어떻게 코드를 옮길 수 있었는지에 대해서 소개해 드렸습니다.
글을 마치기 전에, 지금까지 알아본 내용을 정리해보겠습니다.
먼저, 번들러에 대해서 많이 알아보았습니다. 번들러는 쪼개진 파일을 하나로 합치고 최적화하는 도구입니다. 그것을 위해서는 파일 경로를 정확하게 찾는 Resolution, 그리고 TypeScript나 Flow처럼 비표준 코드를 바로 실행할 수 있는 JavaScript로 바꿔주는 Load 방법을 설정해 줘야 했습니다. 그리고 Optimization 작업으로 결과물을 최적화시켜 줘야 했습니다.
그리고 Metro와 ESBuild는 모두 번들러라고 하는 점에서 앞서 언급한 세 가지 부분에 대한 설정을 그대로 옮기면 똑같이 동작할 거라고 생각할 수 있었습니다. 그래서 앞서 ‘react-native-esbuild’라고 하는 프로젝트를 소개해 드리면서 Resolution과 Load 관점에서 핵심적인 설정들을 설명해 드렸습니다.
그 결과로 토스팀에서는 일관적인 빌드 그리고 큰 속도 개선, 마지막으로 Tree Shaking을 통한 파일 크기 최적화를 얻을 수 있었습니다.
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.