DevInsight

개발의 판단과 맥락을 기록하는 곳

Tools
조회 2약 8분 읽기

타입스크립트 배포의 마지막 런타임을 지우는 실험

Rust 위에 SWC와 LLVM을 얹어 TypeScript를 곧바로 네이티브 실행 파일로 바꾸려는 시도는 단순한 성능 경쟁이 아니다. Node와 Electron 의존성을 덜어내고, 배포 단순화와 크로스플랫폼 전략을 다시 계산하게 만드는 도구로 읽을 만하다.

DevInsight 편집팀 발행

AI 보조 초안과 편집 검수를 거쳐 발행했습니다.

#TypeScript#Rust#Compiler#LLVM#SWC#Native Binary#Cross Platform#Developer Tools

TypeScript 배포에서 끝내 지워지지 않던 것은 코드가 아니라 런타임이었다. 애플리케이션을 빌드해도 마지막 순간에는 늘 Node가 남았고, 데스크톱으로 건너가면 Electron이 남았다. 개발자는 TypeScript를 썼지만, 운영자는 결국 JavaScript 런타임을 함께 품은 산출물을 다뤄야 했다. 이 익숙한 전제가 흔들리기 시작하면 이야기는 단순한 컴파일 속도 경쟁에서 끝나지 않는다. Rust 위에 SWC와 LLVM을 얹어 TypeScript를 곧바로 실행 파일로 바꾸려는 시도는, 성능 자랑보다 배포의 단위를 다시 정의해 보려는 실험에 가깝다.

이 실험이 흥미로운 이유는 “TypeScript도 native binary가 될 수 있다”는 문장 자체보다, 그 문장이 건드리는 오래된 비용 구조 때문이다. 서버 환경에서는 Node 버전 관리, 런타임 패치, 이미지 크기, cold start, OS별 패키징이 따라붙는다. Electron 계열에서는 앱 본체보다 런타임과 브라우저 엔진이 더 무겁게 느껴질 때가 많다. CLI 도구를 배포할 때도 상황은 비슷하다. 도구 하나를 설치시키기 위해 사용자는 먼저 Node 생태계에 발을 들여야 하고, 패키지 매니저와 환경 변수와 버전 충돌의 가능성을 함께 안게 된다. TypeScript가 편한 언어라는 사실과, TypeScript 소프트웨어를 배포하는 일이 가볍다는 사실은 전혀 같은 말이 아니었다.

그래서 native TypeScript 컴파일러라는 발상은 질문을 바꾼다. “JavaScript를 얼마나 빠르게 실행할 수 있는가”가 아니라 “애초에 JavaScript 런타임이 꼭 필요한가”로. 이 차이는 생각보다 크다. 전자는 기존 질서 안에서의 최적화다. 후자는 배포 모델, 운영 모델, 지원 모델 전체를 흔든다. Node 기반 서버를 더 빠르게 띄우는 것과, Node 자체를 산출물에서 제거하는 것은 조직이 느끼는 비용 항목이 다르다. 전자는 밀리초 단위의 개선으로 측정되지만, 후자는 장애 패턴과 지원 문서와 설치 경험의 형태를 바꾼다.

여기서 Rust, SWC, LLVM의 조합은 상징적이다. SWC는 TypeScript와 JavaScript 문법을 빠르게 파싱하고 변환하는 전방의 엔진으로 읽을 수 있고, LLVM은 그 결과를 실제 기계 수준 산출물로 밀어붙이는 후방의 공장으로 볼 수 있다. 이 둘을 Rust가 묶으면 기대되는 그림은 분명하다. 언어 프론트엔드는 현대 JavaScript 문법의 현실을 따라가고, 백엔드는 검증된 native toolchain의 이점을 빌린다. 개발자가 쓰는 문법은 그대로 두되, 최종 배포물은 더 이상 해석기 의존적이지 않은 형태로 만들겠다는 구성이다. 이때 핵심은 Rust가 빠르다는 데만 있지 않다. 시스템 프로그래밍 언어의 메모리 안전성과 배포 친화성이, JavaScript 생태계가 오랫동안 외부에 빚지고 있던 마지막 단계를 흡수한다는 데 있다.

하지만 이 장면을 너무 빨리 낭만으로 읽으면 곤란하다. TypeScript는 문법만 많은 언어가 아니다. 타입 시스템은 런타임에 사라지고, 실제 실행 모델은 여전히 JavaScript의 의미론 위에 서 있다. 문제는 여기서 시작된다. TypeScript를 실행 파일로 바꾼다는 말은, 단순히 문법을 번역하는 일 이상을 뜻한다. eval, 동적 import, reflection 성격의 메타프로그래밍, 런타임에 객체 구조를 유연하게 바꾸는 코드, Node 내장 모듈과의 결합, 파일 시스템과 이벤트 루프에 대한 전제 같은 것들이 전부 판정 대상이 된다. JavaScript가 자유로웠던 이유와, native compiler가 어려운 이유는 거의 같은 목록을 공유한다.

가장 자주 터질 법한 지점은 경계가 흐린 코드다. 예를 들어 애플리케이션 로직 자체는 충분히 정적이지만, 라이브러리 의존성이 런타임의 특수 기능을 전제로 하는 경우가 많다. 오늘날 TypeScript 생태계의 상당수 패키지는 “컴파일 타임에 타입이 있다”는 점보다 “실행 시 Node가 있다”는 점을 더 강하게 믿고 설계되어 있다. 경로 해석, 환경 변수 로딩, 동적 플러그인 발견, CommonJS와 ESM의 뒤섞인 호환층, 네이티브 모듈 바인딩, 긴 예외 스택 포맷 같은 자잘한 기대들이 모여 있다. 이런 의존성은 앱 코드 안에서는 잘 보이지 않는다. 그래서 native binary 전략을 검토할 때 개발팀이 가장 먼저 해야 하는 일은 “TypeScript를 얼마나 썼는가”를 세는 것이 아니라 “Node를 얼마나 믿고 있었는가”를 밝히는 일이다.

이 지점에서 native compiler는 두 부류의 팀에게 전혀 다른 의미를 가진다. 첫째는 배포 단순화가 절박한 팀이다. 내부 CLI, 단일 목적 서버, 에지 환경에 가까운 유틸리티, 고객사 설치형 도구, CI에서 잠깐 실행되는 작업기처럼 런타임 설치와 버전 관리가 번거로운 경우다. 여기서는 실행 파일 하나로 끝난다는 사실이 성능 수치보다 더 큰 설득력을 가진다. 둘째는 크로스플랫폼 전략을 다시 써야 하는 팀이다. Linux 컨테이너만 보던 시절에는 Node 런타임을 번들에 안고 가는 것이 큰 문제가 아니었지만, Windows 배포, macOS 서명, ARM 지원, 작은 컨테이너 이미지, 제한적인 런타임 환경까지 엮이기 시작하면 이야기가 달라진다. 컴파일러가 플랫폼별 산출물을 책임져 준다면, 언어 선택과 운영 선택 사이의 간격이 눈에 띄게 줄어든다.

특히 도구 시장에서는 이 변화가 실감나게 다가온다. 개발자 도구는 대개 “한 번 설치해서 자주 실행”하는 성격이 강하다. 여기서 시작 시간, 설치 단순성, 외부 의존성 최소화는 기능 몇 개보다 더 중요하게 평가되기도 한다. npm install -g가 익숙한 사람에게는 큰 차이가 없어 보일 수 있다. 하지만 조직 단위로 보면 얘기가 달라진다. 보안 팀은 패키지 매니저 경로를 관리하고 싶어 하고, 플랫폼 팀은 사내 표준 이미지에 Node 버전을 얹고 싶지 않아 하며, 현업 팀은 도구 하나 때문에 런타임 충돌을 겪고 싶지 않아 한다. 실행 파일 하나는 기술적으로는 소박하지만, 운영 조직에게는 매우 강한 약속이다. 설치 방법이 문서보다 짧아지고, 장애 원인이 언어 생태계가 아니라 바이너리 자체로 수렴하기 시작한다.

그렇다고 언제나 더 좋은 선택은 아니다. JavaScript 런타임이 제공하는 넓은 호환성과 성숙한 디버깅 경험은 여전히 막강하다. 배포 전에 모든 의존성과 의미론을 정적으로 닫아야 하는 모델은, 빠른 실험과 플러그인 확장에 불리할 수 있다. Node 위에서 돌아가는 프로그램은 이상하게도 자주 “미완성인 채로도” 잘 굴러간다. 패치를 밀어 넣기 쉽고, 스크립트를 주입하기 쉽고, 모듈을 갈아 끼우기 쉽다. 반면 native binary는 더 많은 것을 빌드 시점에 확정해야 한다. 그 결과는 예측 가능성과 교환된다. 어떤 팀에게는 바로 그 예측 가능성이 필요하지만, 어떤 팀에게는 지나치게 비싼 제약이 된다.

운영 신호도 달라진다. 런타임 기반 애플리케이션에서 자주 보는 문제는 버전 드리프트와 환경 차이다. 로컬에선 되는데 서버에선 안 되고, 특정 Node minor에서만 경고가 터지고, 새 base image가 들어오면서 원인을 알 수 없는 실패가 생긴다. native binary로 넘어가면 이런 문제 중 일부는 줄어들 수 있다. 대신 새로운 종류의 신호가 중요해진다. 빌드 재현성, 타깃 아키텍처별 차이, 링크 단계의 실패, 시스템 호출 수준에서의 호환성, 바이너리 크기, 서명과 검증 체계가 그 자리를 차지한다. 즉 문제는 사라지지 않는다. 문제의 위치가 이동할 뿐이다. 이 이동이 반가운지 아닌지는 조직의 역량 분포에 달려 있다. JavaScript 플랫폼 팀이 강한 조직과, 컴파일러/빌드 인프라 감각이 있는 조직은 같은 변화를 전혀 다르게 받아들인다.

이런 종류의 도구를 판단할 때 종종 빠지는 함정이 하나 있다. “더 빠르면 언젠가 이긴다”는 믿음이다. 실제로는 더 빠른 컴파일러나 더 빠른 실행 엔진만으로 생태계가 바뀌는 경우는 드물다. 개발자가 움직이는 이유는 성능 그래프보다 책임 경계가 선명해질 때가 많다. 배포물을 전달하는 방식이 단순해지고, 고객 환경에서 재현이 쉬워지고, 설치 실패가 줄어들고, 보안 검토의 범위가 좁아질 때 전환은 힘을 얻는다. native TypeScript 컴파일러가 진짜 파고드는 지점도 거기다. Node를 대체하는 데 성공하느냐보다, “TypeScript 소프트웨어를 배포하는 방식”을 기존보다 덜 피곤하게 만들 수 있느냐가 더 중요하다.

그래서 이 실험을 바라볼 때는 언어 경쟁보다 제품 형태의 변화를 먼저 보는 편이 낫다. 서버리스 함수는 어떨까. 아주 작은 단위의 작업을 짧게 띄우고 버리는 모델이라면, 런타임 초기화 비용과 배포 패키지 크기, cold start의 체감이 함께 문제 된다. 작은 실행 파일이 직접 올라갈 수 있다면 유리한 그림이 나온다. 반대로 긴 생명주기의 웹 서버에서 이미 Node 운영 체계가 잘 굴러가는 팀이라면, 전환 비용이 이득을 삼킬 수 있다. CLI는 또 다르다. 설치형 도구는 native 배포가 즉각적인 장점으로 읽히지만, 플러그인 생태계와 스크립팅 확장성을 포기하기 어렵다면 완전한 치환은 쉽지 않다. 데스크톱은 더 복잡하다. Electron 의존성을 덜고 싶은 욕망은 매우 크지만, UI 렌더링과 웹 기술 활용이라는 이점을 잃지 않으면서 어느 정도까지 경량화할 수 있는지가 관건이 된다.

결국 핵심은 “JavaScript를 native로 만든다”가 아니라 “어떤 종류의 TypeScript를 native로 만들 수 있는가”다. 이 질문은 꽤 보수적으로 다뤄야 한다. 타입 안전한 백엔드 유틸리티, 계산 중심 로직, 파일 입출력이 명확한 도구, 제한된 표준 라이브러리 위에서 움직이는 서비스는 비교적 좋은 후보가 된다. 반면 생태계 의존성이 짙고, 런타임 훅과 동적 행위를 적극적으로 쓰는 앱은 고생할 가능성이 높다. 좋은 판단은 화려한 데모보다 지원 대상의 경계를 얼마나 솔직하게 그리는가에서 나온다. 컴파일되는 것과 운영 가능한 것은 다르고, 운영 가능한 것과 팀이 유지할 수 있는 것은 또 다르다.

예를 들어 팀이 검토 단계에서 확인해야 할 질문은 대개 이런 식이다. 현재 코드가 TypeScript인지 여부보다, 실제로 Node API에 얼마나 깊게 기대고 있는가. third-party 패키지 중 실행 시점의 동적 해석에 의존하는 것은 무엇인가. 빌드 산출물을 어느 플랫폼까지 책임질 것인가. 오류가 났을 때 개발자는 JavaScript 스택트레이스 대신 어떤 디버깅 경험을 얻게 되는가. 보안 패치의 단위는 런타임이 아니라 컴파일러와 링크 체인 쪽으로 옮겨가는데, 그 변화에 맞는 운영 습관이 있는가. 이 질문들은 기술 선택처럼 보이지만 사실은 운영 모델 선택에 가깝다.

짧은 예시 하나만 들어보자. 지금까지는 이런 배포가 익숙했다.

npm ci npm run build node dist/server.js

native binary 전략은 이 마지막 줄을 바꾸려는 시도다.

./server

표면적으로는 단 한 줄 차이다. 그러나 이 차이 뒤에는 런타임 설치 여부, 컨테이너 이미지 구성, 진단 포인트, 패치 프로세스, 지원 문서, 고객 환경 가정이 전부 얽혀 있다. 그래서 이 한 줄은 사소하지 않다. 소프트웨어를 전달하는 방식 자체를 다른 층위로 옮긴다.

이 실험을 섣불리 혁명으로 부르기보다, 배포의 언어를 다시 쓰게 만드는 계기로 보는 편이 더 정확하다. TypeScript는 오랫동안 개발 경험의 언어였고, 배포 경험의 언어는 아니었다. 개발자는 타입과 tooling의 혜택을 누렸지만, 운영은 여전히 JavaScript 런타임의 규칙 안에서 움직였다. native compiler가 던지는 메시지는 그 분리를 당연하게 여기지 말자는 쪽에 가깝다. 언어가 좋아진 만큼 산출물도 달라질 수 있지 않겠느냐는, 꽤 늦었지만 한 번쯤 나왔어야 할 질문이다.

흥미로운 것은 이 흐름이 JavaScript 생태계를 부정하지 않는다는 점이다. 오히려 너무 성공한 생태계이기 때문에, 이제는 런타임의 범용성이 항상 최선인지 다시 따져볼 여지가 생겼다. 모든 프로그램이 범용 VM 위에서 배포될 필요는 없다. 어떤 프로그램은 스크립트로 남는 편이 좋고, 어떤 프로그램은 실행 파일이 되는 편이 낫다. 언어 하나가 두 세계를 모두 커버할 수 있다면, 선택지는 더 넓어진다. 그때 중요한 것은 어느 쪽이 우월한가가 아니라, 어느 쪽이 특정 문제에 더 적은 마찰을 남기느냐다.

마지막으로 기억할 것은 기대의 방향이다. 이런 도구를 볼 때 사람들은 종종 “Node의 시대가 끝나는가” 같은 큰 질문부터 던진다. 실제로 더 유용한 질문은 훨씬 작다. 설치가 번거로운 내부 도구 하나를 단일 실행 파일로 바꿀 수 있는가. 고객사 환경에서 Node 의존성 때문에 벌어지던 지원 비용을 줄일 수 있는가. 서버리스 워크로드의 cold start와 번들 크기를 동시에 다르게 볼 수 있는가. 크로스플랫폼 배포에서 런타임 포함 여부를 다시 계산할 수 있는가. 변화는 늘 이런 작은 이득에서 시작된다.

TypeScript 배포의 마지막 런타임을 지우려는 실험은 그래서 상징보다 실무에 가깝다. 속도가 아니라 책임의 경계를 바꾸고, 문법이 아니라 산출물의 성격을 바꾸며, 개발 편의의 언어를 운영 단순성의 언어로 밀어 올리려 한다. 성공 여부는 컴파일러가 얼마나 똑똑한가만으로 결정되지 않는다. 어디까지를 지원하고 어디서부터는 선을 긋는지, 그 경계를 얼마나 정직하게 제시하는지가 더 중요하다. 그 선이 분명해질수록 TypeScript는 더 이상 JavaScript를 위한 상위 문법이 아니라, 자기 이름으로 배포 전략을 갖는 언어에 가까워진다.

댓글

댓글을 읽어오는 중입니다.

같이 읽으면 좋은 글

방금 읽은 주제와 이어지는 글을 골랐습니다.

Tools 전체 보기

이전 글

비밀을 깃에 남기고 평문은 지우는 API 클라이언트의 조건

다음 글

에이전트의 블랙박스를 노트북 위로 끌어내리는 법

DevInsight Digest

새 글이 쌓이면, 피드에서 바로 이어 읽으세요.

과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.

RSS 구독