- Figma는 모바일 렌더링과 프로토타입 뷰어에 쓰던 자체 언어 Skew가 온보딩·통합·생태계 측면에서 한계에 부딪히자 전체 Skew 코드를 TypeScript로 이전함
- 전환의 전제는 모바일 브라우저의 WebAssembly 지원 확대, 핵심 경로의 C++ 엔진 대체, 프로토타이핑·모바일 팀 성장으로 마련됨
- 마이그레이션은
Write Skew, build Skew→Write Skew, build TypeScript→Write TypeScript, build TypeScript의 3단계로 진행됐고, Skew-to-TypeScript 트랜스파일러가 개발 흐름을 유지함 - 실제 이전 과정에서는 배열 구조 분해 성능, 가상 호출 제거(devirtualization) 차이, 초기화 순서, 소스맵 연결, 조건부 컴파일 부재 같은 호환성 문제가 드러남
- 최종적으로 단위 테스트를 통과하고 Skew와 비슷한 성능의 TypeScript 코드베이스를 확보했으며, Figma는 내부·외부 코드 통합과 패키지 관리, TypeScript 생태계 활용을 다음 과제로 보고 있음
Skew에서 TypeScript로 옮긴 이유
- Skew는 Figma 초기의 사이드 프로젝트에서 시작됐고, 웹과 모바일을 모두 지원하는 프로토타입 뷰어를 빠르게 만들기 위한 요구를 충족함
- JavaScript로 컴파일되는 언어로 발전하면서 고급 최적화와 빠른 컴파일 시간을 제공했지만, 프로토타입 뷰어에 코드가 누적될수록 유지 비용이 커짐
- 신규 입사자가 익히기 어려움
- 나머지 코드베이스와 쉽게 통합되지 않음
- Figma 밖의 개발자 생태계가 없음
- TypeScript 전환으로 기대한 효과는 개발 속도와 협업 범위 확대였음
- 정적 import와 네이티브 패키지 관리로 내부·외부 코드 통합 간소화
- 린터, 번들러, 정적 분석기 등 대규모 개발자 커뮤니티가 만든 도구 활용
- async/await 같은 최신 JavaScript 기능과 더 유연한 타입 시스템 사용
- 신규 개발자 온보딩과 다른 팀의 참여 장벽 완화
전환이 최근에야 가능해진 조건
- Figma가 모바일 코드베이스를 처음 만들 당시에는 모바일 브라우저가 WebAssembly를 지원하지 않았고, 큰 번들을 성능 좋게 로드하기도 어려웠음
- TypeScript도 초기 단계였기 때문에, 정적 타입과 더 엄격한 타입 시스템을 갖춘 Skew가 당시에는 더 현실적인 선택지였음
- WebAssembly는 2018년까지 모바일에서 광범위한 지원을 얻었고, Figma 테스트 기준으로 2020년에는 모바일 성능이 신뢰 가능한 수준에 도달함
- Skew의 초기 강점은 상수 접기, 가상 호출 제거 같은 전통적 컴파일러 최적화와 실제 정수 연산을 쓰는 JavaScript 생성 같은 웹 특화 최적화였음
- 2020년 벤치마크에서는 Safari에서 TypeScript로 Figma 프로토타입을 로드하면 거의 2배 느려질 수 있음이 확인됐고, iOS에서 Safari가 유일하게 허용된 브라우저 엔진이라는 점이 전환을 막는 요인이었음
- iOS 17.4에서 Apple은 EU 사용자에게 다른 브라우저 엔진을 열었지만, 그 외 지역 사용자에게는 WebKit이 계속 유일한 브라우저 엔진임
- 이후 Skew 엔진의 핵심 구성요소 다수가 C++ 엔진의 대응 구성요소로 대체되면서 TypeScript 이전의 성능 위험이 줄어듦
- 파일 로딩 같은 가장 뜨거운 코드 경로가 포함됨
- TypeScript로 옮겨도 성능 손실을 감당할 수 있다는 확신을 얻음
- 프로토타이핑·모바일 팀이 더 큰 조직으로 성장하면서 자동 마이그레이션과 개발자 경험 개선에 자원을 투입할 수 있게 됨
3단계 코드베이스 전환
- 2020년 첫 마이그레이션 프로토타입에서는 TypeScript 성능이 거의 2배 느렸음
- WebAssembly 지원과 모바일 엔진의 C++ 전환이 충분해진 뒤, Figma는 Maker Week 동안 기존 프로토타입을 고쳐 모든 테스트를 통과하는 작동 마이그레이션을 시연함
- 목표는 전체 코드베이스를 TypeScript로 바꾸는 것이었지만, 수동 재작성은 개발 속도 저하와 런타임 오류, 성능 저하 위험을 만들 수 있었음
- Skew와 TypeScript는 단순한 “타입 있는 JavaScript” 계열 간 전환보다 의미 차이가 컸음
- TypeScript는 파일을 import한 뒤에야 namespace와 class가 초기화됨
- import 순서가 예상과 다르면 런타임 오류가 날 수 있음
- Skew는 로딩 시 모든 심볼을 런타임에서 코드베이스 전체에 사용할 수 있게 함
- Figma는 Evan Wallace가 과거 시작한 작업을 바탕으로 Skew-to-TypeScript 트랜스파일러를 개발함
-
Phase 1: Skew를 작성하고 Skew를 빌드
- 기존 빌드 프로세스를 유지한 채 트랜스파일러를 개발함
- 생성된 TypeScript 코드를 GitHub에 체크인해 개발자들이 새 코드베이스 형태를 확인할 수 있게 함
-
Phase 2: Skew를 작성하고 TypeScript를 빌드
- 단위 테스트를 모두 통과하는 TypeScript 번들이 생성된 뒤, 프로덕션 트래픽을 TypeScript 코드베이스 빌드로 점진 배포함
- 개발자는 여전히 Skew를 작성했고, 트랜스파일러가 Skew 코드를 TypeScript로 변환해 GitHub의 TypeScript 코드를 갱신함
- 생성 코드의 타입 오류도 계속 수정함
- TypeScript는 타입 오류가 있어도 유효한 번들을 생성할 수 있었음
-
Phase 3: TypeScript를 작성하고 TypeScript를 빌드
- 모든 사용자가 TypeScript 빌드 프로세스를 거친 뒤, TypeScript 코드를 개발의 단일 진실 공급원으로 전환함
- 코드 병합이 없는 시점을 찾아 자동 생성 프로세스를 중단하고, 코드베이스에서 Skew 코드를 삭제함
- 금요일 밤에 자동 생성 제거와 CI 작업의 TypeScript 파일 직접 실행 변경을 병합함
- 점진 배포 중 Smart Animate 기능의 파손을 내부에서 발견했고, 게이트 방식 배포 덕분에 롤아웃을 끄고 수정한 뒤 계획을 재검토할 수 있었음
트랜스파일러에서 드러난 문제
- 컴파일러는 보통 프런트엔드와 백엔드로 구성됨
- 프런트엔드는 입력 코드를 파싱하고 타입 검사·문법 검사를 수행한 뒤 중간 표현(IR)으로 변환함
- 백엔드는 IR을 다른 언어로 변환함
- Skew 컴파일러의 백엔드는 난독화·축소된 JavaScript를 생성함
- 트랜스파일러는 백엔드가 사람이 읽을 수 있는 코드를 생성하는 특수한 컴파일러이며, Figma는 Skew IR에서 사람이 읽을 수 있는 TypeScript를 생성해야 했음
- 초기 구현은 Skew의 JavaScript 백엔드에서 많은 영감을 얻어 비교적 수월했지만, 후반에는 추적과 처리가 어려운 문제가 여럿 발생함
-
배열 구조 분해 성능
- 샘플 프로토타입에서 Skew와 TypeScript의 오프라인 성능 차이를 조사하던 중 TypeScript의 프레임률이 낮게 나옴
- 원인은 JavaScript의 배열 구조 분해였음
const [a, b] = function_that_returns_an_array()같은 연산에서 JavaScript는 배열을 직접 인덱싱하지 않고 iterator를 구성해 순회함- Figma는 JavaScript의
arguments키워드에서 인자를 가져오기 위해 이 방식을 사용했고, 특정 테스트 케이스에서 성능이 느려짐 - 구조 분해 대신 arguments 배열을 직접 인덱싱하는 코드를 생성해 프레임당 지연 시간을 최대 25% 개선함
-
Skew의 가상 호출 제거 최적화
- Skew 컴파일러는 특정 조건에서 class 내부 함수를 밖으로 빼 전역 함수로 올리는 devirtualization 최적화를 수행함
myObject.myFunc(a, b)가myFunc(myObject, a, b)형태가 될 수 있음- TypeScript는 이 최적화를 하지 않음
- Smart Animate 파손은
myObject가 null인 상황에서 발생함 - devirtualized 호출은 정상 실행됐지만, non-devirtualized 호출은 null 접근 예외를 냄
- Figma는 같은 문제가 있는 호출 지점을 찾기 위해 devirtualization에 참여할 수 있는 모든 함수에 로깅을 추가함
- 짧은 기간 로깅을 켠 뒤 프로덕션 로그를 분석하고 문제 호출 지점을 수정함
-
초기화 순서 차이
- Skew에서는 변수, class, namespace, 함수 정의를 코드 어디에 선언해도 선언 순서를 신경 쓰지 않음
- TypeScript에서는 전역 변수나 class 정의의 초기화 순서가 중요함
- class 정의보다 static class 변수를 먼저 초기화하면 컴파일 타임 오류가 됨
- 초기 트랜스파일러는 namespace를 쓰지 않고 모든 함수를 전역 스코프로 평탄화해 Skew와 비슷한 동작을 유지함
- 결과 코드는 읽기 어려웠고, 이후 트랜스파일러를 고쳐 올바른 순서로 TypeScript 코드를 출력하고 가독성을 위해 namespace를 다시 추가함
- 최종적으로 단위 테스트를 통과하고 Skew 성능과 맞는 TypeScript 코드를 컴파일할 수 있는 트랜스파일러를 만들었음
- 일부 작은 문제는 트랜스파일러에 새 수정을 넣기보다 Skew 원본 코드에서 수동 수정하거나 TypeScript 전환 이후 수정함
소스맵으로 디버깅 경험 유지
- Figma는 TypeScript 전환 중에도 개발자가 중단 없이 디버깅할 수 있도록 소스맵을 중요하게 다룸
- 브라우저 디버거는 JavaScript만 이해하지만, 개발자는 Skew나 TypeScript 소스에 breakpoint를 설정함
- 소스맵은 컴파일된 JavaScript 위치와 원본 소스 위치를 연결함
- 예시로
helper → c,myInt → a,arrayOfInts → b같은 매핑이 가능함
- 예시로
- 일반적으로
.map확장자의 소스맵 파일 하나가 최종 JavaScript 번들과 연결됨 - 기존 인프라는 Skew → JavaScript 소스맵을 생성했지만, Phase 2에서는 Skew → TypeScript → esbuild 번들링 흐름으로 파이프라인이 바뀜
- 기존 소스맵을 그대로 쓰면 JavaScript와 Skew 코드 사이 매핑이 틀어지고, 개발자가 디버깅할 수 없게 됨
- 새 빌드 프로세스는 3단계로 소스맵을 구성함
- Step 1: esbuild가 TypeScript → JavaScript 소스맵
ts-to-js.map생성 - Step 2: 트랜스파일러가 각 Skew 파일에 대해 Skew → TypeScript 소스맵 생성
- Step 3: 두 소스맵을 합성해 Skew → JavaScript 최종 소스맵 생성
- Step 1: esbuild가 TypeScript → JavaScript 소스맵
- 최종 소스맵으로 새 JavaScript 번들을 Skew 코드에 매핑할 수 있어 Phase 2에서도 개발자 경험을 유지함
TypeScript에서 조건부 컴파일 처리
- Skew는 최상위
if문과 컴파일러에 전달하는defines옵션으로 조건부 컴파일을 지원함 - 이 기능으로 같은 코드베이스에서 서로 다른 번들을 만들 수 있었음
- 사용자에게 배포되는 실제 번들
- 단위 테스트 전용 번들
- debug 또는 release 빌드에서 다른 구현을 쓰는 함수나 class
- TypeScript에는 조건부 컴파일이 없기 때문에, Figma는 타입 검사 이후 esbuild의
defines와 dead code elimination 기능을 사용해 번들링 단계에서 조건부 컴파일을 수행함 - 이 방식에서는
defines가 타입 검사에 영향을 줄 수 없음BUILD == "TEST"일 때만 존재하는testOnlyFunction같은 코드는 TypeScript에서 그대로 존재할 수 없음
- 해결책은 class와 method를 항상 정의하고, method 내부에서
BUILD값에 따라 분기하는 형태로 변환하는 것이었음- 테스트 전용이 아닌 빌드에서
testOnlyFunction이 호출되면Unexpected call to test-only function오류를 던짐
- 테스트 전용이 아닌 빌드에서
- 최종 JavaScript는 원래 Skew 코드가 생성하던 JavaScript와 같게 컴파일될 수 있었음
- 다만 일부 심볼이 원래는 특정 컴파일 모드에만 존재했지만 변경 뒤 모든 모드에 존재하게 되어 최종 번들이 약간 커짐
- 테스트에서 번들 크기 증가는 수용 가능한 수준이었고, export되지 않은 최상위 심볼은 tree-shaking으로 제거할 수 있었음
전환 이후의 방향
- 모든 Skew 코드를 TypeScript로 옮기면서 Figma의 핵심 코드베이스 하나가 현대화됨
- 내부·외부 코드와의 통합이 쉬워졌고, 개발자는 더 효율적으로 작업할 수 있게 됨
- 당시 Figma의 요구와 역량에서는 Skew로 코드베이스를 시작한 결정이 적절했음
- 기술이 성숙하면서 과거에는 적절하지 않았던 TypeScript가 현재는 적절한 선택지가 됨
- Figma는 이후 작업으로 다음 가능성을 탐색 중임
- 나머지 코드베이스와의 통합
- 훨씬 쉬운 패키지 관리
- 활발한 TypeScript 생태계의 새 기능 직접 사용
- 전환 과정에서 import 해석, 모듈 시스템, JavaScript 코드 생성 등 TypeScript의 여러 측면을 학습함