우리는 오랫동안 재생 엔진의 성능을 극대화하기 위해 자체적으로 만든 프로그래밍 언어인 Skew로 모바일 렌더링 아키텍처의 핵심 부분을 작성해 왔습니다. 여기, 단 하루의 개발 중단 없이 자동으로 Skew를 타입스크립트로 마이그레이션 한 방법을 소개합니다.
Skew는 피그마 초기에 사이드 프로젝트로 시작되었습니다. 당시에 Skew는 웹과 모바일을 모두 지원하는 프로토타입 뷰어를 구축해야 한다는 피그마의 중요한 요구사항을 충족해 주었습니다. 처음에는 뷰어를 빠르게 구현하기 위한 방법으로 시작했지만, 결과적으로 더 고도의 최적화와 빠른 컴파일 시간을 가능하게 하는 전체적인 compile-to-JavaScript 프로그래밍 언어로 발전했습니다. 그러나 수년에 걸쳐 프로토타입 뷰어에서 Skew로 점점 더 많은 코드가 쌓이면서, 신규 직원들은 적응하기 어려워졌고, 다른 코드 베이스와 쉽게 통합되지 않으며, 피그마 외부의 개발자 생태계가 부족하다는 것을 점차 깨닫게 되었습니다. 원래의 장점보다 확장으로 인한 고통이 더 커졌습니다.
우리는 최근에 Skew로 작성된 모든 코드들을 웹을 위한 업계 표준 언어인 타입스크립트로 마이그레이션 하는 과정을 끝냈습니다. 타입스크립트는 팀에 큰 변화를 가져왔으며, 아래 내용을 가능하게 했습니다.
- 정적 임포트와 네이티브 패키지 관리를 통해 내부 및 외부 코드와의 통합 간소화
- 린터, 번들러, 정적 분석기와 같은 도구들이 있는 거대한 개발자 생태계
- async/await과 같은 최신 자바스크립트 기능 사용과 유연한 타입 시스템 활용
- 신규 개발자들의 수월한 온보딩 및 다른 팀과의 마찰 감소
Skew로 작성된 코드.
타입스크립트로 작성된 코드.
이 마이그레이션은 다음의 세 가지 이유로 최근에서야 가능해졌습니다.
- 더 많은 모바일 브라우저들이 웹 어셈블리를 지원하기 시작
- Skew 엔진의 많은 핵심 컴포넌트를 C++ 엔진의 동일한 컴포넌트로 교체하여 타입스크립트 전환으로 인한 성능 손실의 최소화
- 팀 규모 증가로 개발자 경험에 집중할 수 있는 자원 할당 가능
광범위한 모바일 지원과 향상된 성능의 웹 어셈블리
처음 피그마의 모바일 코드 베이스를 만들 때, 모바일 기기들은 웹 어셈블리를 지원하지 않았고 큰 번들을 빠르게 불러올 수 없었습니다. 이것은 중심 C++ 엔진(웹 어셈블리를 컴파일하기 위해 필요한) 코드를 사용할 수 없다는 것을 의미했습니다. 동시에 타입스크립트는 초기 단계에 있었기 때문에, 정적 타입과 엄격한 타입 시스템을 통해 고급 컴파일러 최적화를 가능하게 했던 Skew에 비해 확실한 선택은 아니었습니다. 다행히도 웹 어셈블리는 2018년까지 광범위한 모바일 지원을 확보했고, 테스트에 따르면 2020년까지 안정적인 모바일 성능을 보여주었습니다.
Skew의 최적화에 따른 기타 성능 개선 사항
처음 Skew를 사용할 때는 몇몇 주요한 장점이 있었습니다. 상수 폴딩(constant folding)과 탈가상화(devirtualization) 같은 기존 컴파일러 최적화와 실제 정수 연산을 포함한 자바스크립트 코드를 생성하는 웹 특화 최적화가 포함되어 있었습니다. 이런 최적화에 더 시간을 투자할수록 오랫동안 직접 키워온 언어에서 벗어나기가 어려웠습니다. 예를 들어 2020년 벤치마크 결과, 사파리에서 타입스크립트를 사용하면 피그마 프로토타입을 로드하는 속도가 거의 두 배 가까이 느려졌을 것으로 나타났습니다. 사파리는 (현재도 여전히*) iOS에서 허용되는 유일한 브라우저 엔진이기 때문에 이는 방해 요소였습니다.
(iOS 17.4에서, Apple은 EU 사용자를 위해 다른 브라우저 엔진에 시스템을 개방했습니다. 전 세계 다른 사용자를 위한 유일한 브라우저 엔진은 여전히 WebKit이 유일합니다.)
웹 어셈블리가 광범위한 모바일 지원을 확보하고 나서 몇 년 후, Skew 엔진으로 작성된 많은 핵심 컴포넌트는 C++ 엔진의 동일한 컴포넌트로 교체하였습니다. 우리가 교체한 컴포넌트는 파일 로딩과 같이 가장 인기 있는 코드 경로들(code paths)이었기 때문에 타입스크립트로 전환해도 성능이 크게 저하되진 않았습니다. 이 경험을 통해 Skew의 최적화 컴파일러의 장점을 포기할 수 있다는 확신을 얻었습니다.
피그마의 프로토타이핑 및 모바일 팀의 성장
피그마의 초창기에는 소규모 팀으로 최대한 빠르게 개발해야 했기 때문에 자동화된 마이그레이션에 자원을 투입할 여유가 없었습니다. 프로토타이핑 및 모바일 팀의 확장으로 이 작업을 진행할 수 있게 되었습니다.
코드 베이스 변환
2020년에 이 마이그레이션에 대한 프로토타입을 처음 만들었을 때 타입스크립트를 사용한 벤치마크 성능은 거의 두 배 느려지는 것으로 나타났습니다. 웹 어셈블리 지원이 충분한 것을 확인하고 모바일 엔진의 핵심 기능을 C++로 옮긴 후, 우리는 오래된 프로토타입을 회사 Maker Week 동안 수정했습니다. 우리는 모든 테스트를 통과한 마이그레이션 작업을 시연했습니다. 수천 건의 개발자 경험 문제와 심각하지 않은 오류가 우리는 모든 Skew 코드를 안전하게 마이그레이션 하기 위한 대략의 계획을 세웠습니다.
목표는 간단했습니다. 모든 코드 베이스를 타입스크립트로 변환하는 것이었습니다. 각 파일을 수동으로 재작성할 수도 있었지만, 전체 코드 베이스를 다시 작성하기 위해 개발자들의 작업을 방해할 여유는 없었습니다. 더 중요한 것은, 사용자가 런타임 오류나 성능 저하를 겪는 것을 피하고 싶었습니다. 마이그레이션은 자동화했지만, 빠른 전환은 아니었습니다. 다른 “타입을 가진 자바스크립트” 언어를 타입스크립트로 옮기는 것과는 다르게 Skew는 실제로 의미론적인 차이가 있었기 때문에 즉시 타입스크립트로 전환하는 것이 어려웠습니다. 예를 들어, 타입스크립트는 우리가 파일을 불러오고 난 후 네임스페이스와 클래스를 초기화하므로 우리가 파일을 예상치 못한 순서로 불러온다면 런타임 에러가 발생할 수 있습니다. 반면 Skew는 로드 시 모든 심벌을 코드 베이스의 나머지 부분에서 바로 사용할 수 있게 하여 이러한 런타임 에러가 문제가 되지 않았습니다.
(Evan은 이 경험을 통해 몇 가지 교훈을 얻어 웹 번들러 esbuild를 만들었다고 합니다.)
우리는 개발자 작업 흐름에 지장을 주는 것을 최소화하고자 타입스크립트를 통해 만들어진 새로운 코드 번들을 점진적으로 배포하기로 하였습니다. 우리는 피그마의 전 CTO인 Evan Wallace가 몇 년 전에 시작한 작업을 기반으로 Skew 코드를 넣으면 타입스크립트 코드가 만들어지는 Skew-to-TypeScript 트랜스파일러를 개발하였습니다.
1단계: Skew로 작성, Skew로 빌드
우리는 기존 빌드 프로세스를 그대로 유지하면서 트랜스파일러를 개발했고, 새로운 코드 베이스가 어떻게 보일지 개발자들에게 보여주기 위해 타입스크립트 코드를 GitHub에 업로드했습니다.
원래의 Skew 파이프라인과 함께 타입스크립트 트랜스파일러 개발
2단계: Skew로 작성, 타입스크립트로 빌드
모든 단위 테스트를 통과하는 타입스크립트 번들을 만든 후, 타입스크립트 코드 베이스에서 직접 빌드된 코드를 실제 운영 환경에 점진적으로 배포하기 시작했습니다. 이 단계에서 개발자들은 여전히 Skew로 코드를 작성하고 있었고 트랜스파일러가 그 코드를 타입스크립트로 변환하고 GitHub에 실시간으로 업데이트하였습니다. 더 나아가 생성된 코드의 타입 에러들을 계속 수정하였습니다. 타입스크립트는 타입 에러는 있었지만 여전히 유효한 번들을 만들 수 있었습니다!
타입스크립트 컴파일러가 Skew 소스 코드에서 생성한 타입스크립트 코드 베이스를 운영 환경에 점진적으로 배포하기
3단계: 타입스크립트로 작성, 타입스크립트로 빌드
모두가 타입스크립트 빌드 프로세스를 거친 후, 우리는 이 타입스크립트 코드를 단일 진실 공급원(Single source of truth)으로 만들어야 했습니다. 모든 사람이 코드를 병합하지 않는 시간을 확인한 후, 자동 생성 프로세스를 중단하고 Skew 코드를 코드 베이스에서 삭제해서 개발자들이 타입스크립트로 코드를 작성하도록 했습니다.
타입스크립트 코드 베이스를 단일 진실 공급원으로 사용하기 위한 전환(cutover) 진행
이건 확실한 접근 방식이었습니다. Skew 컴파일러의 모든 작동을 완전히 제어할 수 있었기 때문에 1단계를 훨씬 수월하게 진행하였습니다. 이 단계부터 자유롭게 요구 사항에 맞게 Skew 컴파일러의 부분을 추가하거나 수정할 수 있었습니다. 점진적인 배포도 큰 효과를 발휘했습니다. 예를 들어, 타입스크립트 배포 중에 Smart Animate 기능의 문제를 내부적으로 발견 했을때, 제한적인 접근방식을 사용한 덕분에 배포를 빠르게 중단하고 문제를 수정하여 배포 계획을 재검토할 수 있었습니다.
또한 타입스크립트를 사용하기 위한 전환에 대해서도 충분히 공지하였습니다. 금요일 밤, 자동 생성 프로세스를 제거하고 모든 지속적인 통합 작업이 타입스크립트 파일에서 직접 실행되도록 하기 위해 필요한 모든 변경 사항들을 병합하였습니다.
트랜스파일러 작업에 대한 참고 사항
컴파일러가 어떻게 작동하는지 잘 모르시는 분들을 위한 조감도는 다음과 같습니다. 컴파일러는 프런트엔드와 백엔드로 구성됩니다. 프런트엔드는 입력된 코드를 파싱하고 이해하고 타입 검사, 구문(syntax) 검사와 같은 작업을 수행합니다. 프런트엔드는 이 코드를 중간 표현(intermediate representation, IR)으로 변환합니다. 중간 표현이란 원본 입력 코드의 의미(syntax)와 논리(logic)를 완전히 반영하되, 코드 재파싱에 대해 걱정할 필요가 없도록 구조화된 데이터 구조입니다.
컴파일러의 백엔드는 이 중간 표현을 다른 언어들로 변경합니다. 예를 들어 C의 백엔드는 일반적으로 어셈블리/기계어 코드를 생성하고, Skew 컴파일러의 백엔드는 엉망이고(mangled) 축소된 자바스크립트 코드를 생성 합니다.
트랜스파일러를 작성하는 과정은 처음에는 상대적으로 간단했습니다. 우리는 중간 표현에서 얻은 정보를 기반으로 적절한 코드를 생성하기 위해 자바스크립트 백엔드에서 많은 영감을 받았습니다. 끝으로 갈수록 더 추적하고 다루기 어려운 아래의 몇 가지 이슈들에 직면했습니다.
- 배열 구조 분해(array destructuring)의 성능 이슈: 자바스크립트 구조 분해 할당을 사용하지 않음으로써 최대 25%가량 성능이 향상되었습니다.
- Skew의 “탈가상화” 최적화: 탈가상화라는 컴파일러 최적화가 코드 베이스의 동작을 깨뜨리지 않도록 배포 과정에서 추가적인 조치를 취했습니다.
- 타입스크립트에서 초기화 순서의 중요성: 타입스크립트에서는 Skew와 달리 심벌 순서가 중요하므로, 트랜스파일러가 이 순서를 맞추는 코드를 생성해야 했습니다.
(트랜스파일러는 백엔드가 기계어가 아닌 사람이 읽을 수 있는 코드를 만드는 특별한 유형의 컴파일러입니다. 우리의 경우, 백엔드는 Skew 중간 표현으로 사람이 읽을 수 있는 타입스크립트를 생성해야 합니다.)
배열 구조 분해의 성능 이슈
일부 예제 프로토타입에서 Skew와 타입스크립트의 오프라인 성능 차이를 조사한 결과 타입스크립트에서 프레임 속도가 더 느리다는 것을 발견했습니다. 많은 조사 이후 근본적인 원인이 자바스크립트에서 배열 구조 분해 과정이 상당히 느리다는 것에서 기인함을 알게 되었습니다. const [a, b] = function_that_returns_an_array()
와 같은 연산을 수행하기 위해 자바스크립트는 배열에서 직접 인덱싱하는 대신 배열을 순회하는 이터레이터를 생성 합니다. 이 작업은 속도가 느립니다. 우리는 자바스크립트의 arguments
키워드에서 인수를 검색하기 위해 이 작업을 수행했는데, 이 때문에 특정 케이스에서 성능이 느려졌습니다. 해결책은 간단했습니다. 우리는 구조 분해를 하지 않고 인수 배열에서 직접 인덱스를 생성하였고 프레임당 지연 속도를 25% 가량 개선하였습니다!
Skew의 “탈가상화” 최적화
다른 이슈는 타입스크립트와 Skew가 클래스 메서드를 서로 다르게 처리하기 때문에 Smart Animate 배포 중 앞서 언급한 문제가 발생한 것입니다. Skew 컴파일러는 특정 조건에서 성능 최적화를 위해 함수가 클래스에서 빠져나와 전역 함수로 끌어올려지는 탈가상화라는 작업을 수행합니다.
(탈가상화에 대해 더 자세히 알고 싶다면 이 글을 읽어보세요.)
myObject.myFunc(a, b)
// 아래처럼 변합니다...
myFunc(myObject, a, b)
이 최적화는 Skew에서는 발생하지만, 타입스크립트에서는 발생하지 않습니다. Smart Animate 문제는 myObject
가 null이어서 발생하였습니다. 탈가상화된 호출은 잘 실행되었지만, 탈가상화 되지 않은 호출은 null 접근 예외를 발생시켰습니다. 이에 따라 동일한 문제가 있는 다른 호출 지점(call site)이 있는지 걱정하게 되었습니다.
우려를 해소하기 위해 탈가상화와 관련된 모든 함수에 로깅을 추가하여 이 문제가 프로덕션에서 발생한 적 있는지 확인했습니다. 잠깐 이 로깅을 활성화한 후 로그를 분석하여 문제가 있는 모든 호출 지점을 수정해서 타입스크립트 코드의 견고성을 더욱 확신하게 되었습니다.
타입스크립트에서 초기화 순서의 중요성
우리가 마주한 세 번째 이슈는 각 언어가 초기화 순서를 존중하는 방식입니다. Skew로 작성하면 코드 어디에서든 변수, 클래스와 네임스페이스, 함수 정의를 선언할 수 있고 언제 선언되었는지 신경 쓰지 않습니다. 하지만 타입스크립트에서는 전역 변수나 클래스 정의가 먼저 선언되었는지가 중요합니다. 정적 클래스 변수를 클래스 정의 전에 초기화하면 컴파일 타임 에러가 발생합니다.
트랜스파일러의 초기 버전은 네임스페이스를 사용하지 않고 타입스크립트 코드를 생성하여 이 문제를 해결했으며, 모든 함수를 전역 범위로 평면화했습니다. 이건 Skew의 동작과 비슷하지만, 코드의 가독성이 떨어졌습니다. 우리는 명확성과 정확성을 위해 적절한 순서로 타입스크립트 코드를 생성하도록 트랜스파일러의 일부를 재작업하고, 가독성을 위해 네임스페이스를 다시 추가했습니다.
이러한 어려움들이 있었지만 결국 모든 단위 테스트를 통과하고 Skew의 성능에 견줄 수 있는 타입스크립트 코드를 작성하는 트랜스파일러를 만들었습니다. 트랜스파일러를 수정하는 대신, Skew 소스 코드에서 수동으로 수정하거나 타입스크립트로 전환한 후 작은 문제들을 해결하기로 했습니다. 모든 수정 사항이 트랜스파일러에 있는 게 이상적이지만, 현실적으로 일부 변경 사항은 자동화할 가치가 없었고 이런 방식으로 문제를 수정해서 더 빠르게 진행할 수 있었습니다.
사례 연구: 소스 맵으로 개발자들을 계속 행복하게 하기
전체 과정에서 개발자의 생산성은 항상 최우선 과제였습니다. 우리는 타입스크립트로의 마이그레이션을 최대한 쉽게 만들고자 했으며, 이는 가동 중단 시간을 피하고 원활한 디버깅 환경을 조성하기 위해 할 수 있는 모든 노력을 기울였음을 의미합니다. 웹 개발자들은 주로 최신 웹 브라우저에 제공하는 디버거를 통해 디버깅을 진행합니다. 소스 코드에서 중단점을 설정하고 코드가 이 지점에 도달할 때 브라우저는 일시 정지되고 개발자는 브라우저의 자바스크립트 엔진의 상태를 조사할 수 있습니다. 우리의 경우, 개발자는 프로젝트의 단계에 따라 Skew 또는 타입스크립트에 중단점을 설정하고 싶어 할 것입니다. 중단점들이 Skew나 타입스크립트에 설정되어 있지만 브라우저는 그 자체로는 자바스크립트만 이해할 수 있습니다. 소스 코드에 중단점이 있는 경우 컴파일된 자바스크립트 번들에서 어디서 멈춰야 할지를 어떻게 알 수 있을까요? 여기서 소스 맵이 등장합니다. 소스 맵은 브라우저가 컴파일된 코드와 소스 코드를 연결하는 방법입니다. 이제 이 Skew 코드를 사용한 간단한 예제를 살펴보겠습니다.
def helper() {
return [1, 3, 4, 5];
}
def myFunc(myInt int) int {
var arrayOfInts List<int> = helper();
return arrayOfInts[0] + 1;
}
이 코드는 아래의 자바스크립트처럼 컴파일되고 경량화됩니다.
function c(){return [1,3,4,5];}function myFunc(a){let b=c();return b[0]+1;}
이 문법은 읽기 어렵습니다. 소스 맵들은 생성된 자바스크립트의 특정 섹션을 원본 코드(우리의 경우 Skew)의 특정 섹션으로 다시 매핑해줍니다. 코드 조각 사이의 소스 맵은 다음과 같은 매핑을 보여줍니다.
helper -> c
myInt -> a
arrayOfInts -> b
(소스 맵을 생성하고, 이해하고, 디버깅하는 더 많은 기술적인 내용은 소스 맵에 관한 이 아티클을 참고하세요.)
소스 맵은 보통 .map
이라는 파일 확장자를 가지고 있습니다. 한 소스 맵 파일은 최종 자바스크립트 번들과 연결되므로 자바스크립트 파일의 코드 위치를 주면, 자바스크립트의 소스 맵은 다음을 알려줍니다.
- 이 자바스크립트 섹션의 Skew 파일
- 자바스크립트의 이 부분에 해당하는 Skew 파일 내의 코드 위치
개발자가 Skew에서 디버거 중단점을 설정할 때마다, 브라우저는 이 소스 맵을 역으로 탐색하여 해당 Skew 라인에 해당하는 자바스크립트 부분을 찾아 그곳에 중단점을 설정합니다.
타입스크립트 마이그레이션에 이를 적용한 방법은 다음과 같습니다. 기존 인프라는 디버깅에 사용하기 위해 Skew에서 자바스크립트 소스 맵을 생성 했습니다. 하지만 마이그레이션 2단계에서 번들 생성 파이프라인은 완전히 달라져서 타입스크립트를 생성한 후 esbuild로 번들링을 했습니다. 만약 기존 인프라에서 나온 동일한 소스 맵을 사용 하려고 했었다면 자바스크립트와 Skew 코드가 정확하지 않게 매핑되어 개발자는 이 단계에서 코드를 디버깅할 수 없게 될 것입니다.
우리는 새로운 빌드 과정을 이용해 새로운 소스 맵을 만들어야 했습니다. 이 작업은 아래에 설명된 세 가지 작업이 포함되어 있습니다.
새로운 빌드 과정을 이용해 새로운 소스 맵을 만드는 과정의 도표
단계 1: 타입스크립트 생성 -> 자바스크립트 소스 맵 ts-to-js.map
. esbuild는 자바스크립트 번들을 생성할 때 이 맵을 자동으로 생성할 수 있습니다. 단계 2: Skew 생성 -> 각각의 Skew 소스 파일에 대한 타입스크립트 소스 맵. 우리가 파일의 이름을 file.sk
라고 정하면, 트랜스파일러가 소스 맵의 이름을 file.map
이라고 정합니다. Skew -> 자바스크립트 백엔드가 소스 맵을 생성하는 방법을 모방하여 우리의 타입스크립트 트랜스파일러를 구현했습니다. 단계 3: 이 소스 맵을 함께 구성하여 Skew에서 자바스크립트로의 맵을 생성 합니다. 이를 위해 우리는 빌드 과정에 다음과 같은 로직을 구현했습니다.
ts-to-js.map
의 모든 진입점 E
에 대해 아래와 같은 특성을 갖습니다.
- 이 진입점이 매핑되는 타입스크립트 파일을 결정하고 해당 소스 맵인
fileX.map
을 엽니다. - 이 소스 맵
fileX.map
에서E
의 타입스크립트 코드 위치를 찾아 해당하는 Skew 파일fileX.sk
의 코드 위치를 알아냅니다. - 이걸 우리의 최종 소스 맵의 새로운 진입점으로 추가합니다.
E
의 자바스크립트 위치와 Skew 코드 위치가 결합합니다. 최종 소스 맵이 완성되었으므로 이제 개발자 경험을 방해하지 않고 새로운 자바스크립트 번들을 Skew에 매핑할 수 있습니다.
사례 연구: 조건부 컴파일
Skew에서, 최상위 “if” 선언문은 조건부 컴파일을 허용하고 Skew 컴파일러에 전달된 “defines” 옵션을 통해 컴파일 타임 상수를 사용하여 조건을 지정합니다. 이를 통해 동일한 코드 베이스에 여러 가지 빌드 대상을 정의할 수 있습니다. 즉, 코드 베이스의 일부를 포함하거나 제외하여 다양한 용도로 사용할 수 있는 여러 번들을 만들 수 있습니다. 예를 들어 하나의 번들 변형은 사용자들에게 배포되는 실제 번들이 될 수 있고, 다른 번들은 단위 테스트만을 위해 사용될 수 있습니다. 이를 통해 특정 함수나 클래스들이 디버그나 릴리즈 빌드에서 다르게 구현되도록 지정할 수 있습니다.
더 명확하게 하자면, 아래와 같은 Skew 코드는 TEST
빌드에서는 다른 구현으로 정의됩니다.
if BUILD == "TEST" {
class HTTPRequest {
def send(body string) HTTPResponse {
# test-only implementation...
}
def testOnlyFunction {
console.log("hi!")
}
}
} else {
class HTTPRequest {
def send(body string) HTTPResponse {
# real implementation...
}
}
}
Skew 컴파일러에 BUILD: "TEST"
정의를 전달하면 아래와 같은 자바스크립트로 컴파일됩니다.
function HTTPRequest() {}
HTTPRequest.prototype.send = function(body) {
// 테스트 전용 구현
HTTPRequest.prototype.testOnlyFunction = function(body) {
console.log("hi!")
}
하지만 조건부 컴파일은 타입스크립트에 포함되지 않습니다. 대신 우리는 타입 검사 이후 빌드 단계에서 esbuild의 “defines”와 불필요한 코드 제거 기능을 사용하여 번들링의 일부로 조건부 컴파일을 수행해야 했습니다. 그래서 이 정의들은 타입 검사에 영향을 미칠 수 없었습니다. 따라서 “defines”는 더 이상 타입 검사에 영향을 미칠 수 없게 되었고, 위 예제와 같이 testOnlyFunction
메서드가 BUILD: 'TEST'
빌드에서만 정의된 코드는 타입스크립트에서는 존재할 수 없게 되었습니다.
우리는 이 문제를 아래와 같은 타입스크립트 코드로 변환하면서 해결하였습니다.
// esbuild 단계에서 정의된 값
declare const BUILD: string
class HTTPRequest {
send(body: string): HTTPResponse {
if (BUILD == "TEST") {
// test-only 구현
} else {
// 실제 구현
}
} testOnlyFunction() {
if (BUILD == "TEST") {
console.log("hi!")
} else {
throw new Error("Unexpected call to test-only function")
}
}
}
이는 기존 Skew 코드가 직접 컴파일된 것과 동일한 자바스크립트 코드로 컴파일됩니다.
function HTTPRequest() {}
HTTPRequest.prototype.send = function(body) {
// test-only 구현
}
HTTPRequest.prototype.testOnlyFunction = function(body) {
console.log("hi!")
}
안타깝게도, 최종 번들 크기는 조금 더 커졌습니다. 원래 한 컴파일 타임 방식에서만 사용할 수 있었던 일부 심벌들이 모든 방식에 존재하게 되었습니다. 예를 들어, testOnlyFunction
을 빌드 방식 BUILD
가 "TEST"
로 설정되었을 때만 사용했지만, 이 변화 이후로 해당 함수는 최종 번들에 항상 존재하게 되었습니다. 테스트 결과 번들 크기 증가는 수용할 수 있는 수준이었습니다. 그러나 트리 셰이킹을 통해 내보내지 않은 최상위 수준의 심벌을 제거할 수 있었습니다.
이제 타입스크립트로 시작되는 프로토타이핑 개발의 새로운 시대
우리는 모든 Skew 코드를 타입스크립트로 마이그레이션 하면서 피그마의 핵심 코드 베이스를 최신화하였습니다. 내외부 코드와 훨씬 더 쉽게 통합할 수 있는 길을 닦았을 뿐만 아니라 그 결과로 개발자들이 더 효율적으로 일하게 되었습니다. 초기에 Skew로 코드 베이스를 작성한 것은 당시 피그마의 요구 사항과 능력을 고려했을 때 좋은 결정이었습니다. 하지만, 기술은 계속 발전했고 우리는 기술의 발전 속도를 의심하지 않는 법을 배웠습니다. 타입스크립트가 훗날엔 맞는 선택이 아닐지라도 지금은 명백합니다. 이후에도 타입스크립트로 전환하는 모든 이점을 누리고 싶기 때문에 여기서 작업은 끝나지 않을 것입니다. 나머지 코드 베이스와의 통합, 훨씬 쉬워진 패키지 관리, 타입스크립트 생태계의 새로운 기능을 직접 사용할 수 있는 것과 같은 많은 미래의 가능성을 검토하고 있습니다. import resolutions, 모듈 시스템, 자바스크립트 코드 생성과 같은 타입스크립트의 다양한 측면을 많이 알게 되었으며 이런 내용을 잘 활용하고 싶습니다.
이 프로젝트에 기여한 Andrew Chan, Ben Drebing 및 Eddie Shiang에게 감사의 말씀을 전하고 싶습니다. 이런 작업이 마음에 드신다면 Figma에서 저희와 함께 작업해 보세요!