Motion이 대규모 TypeScript 모노리포에서 C#/.NET으로 전환하기로 한 배경과 이유를 공유합니다. 생산성과 안정성, ORM 성숙도, 생태계, AI 코딩 친화성 관점에서 비교하며 배운 점을 정리합니다.
지난 거의 5년 동안 Motion은 거대한 TypeScript 모노레포로 운영되어 왔습니다. 주석과 node_modules 등을 제외해도 정점에서는 약 250만 줄에 달했습니다. 이를 관리하기 위해 Vercel의 훌륭한 빌드 시스템인 Turborepo를 사용했습니다.
이 글은 TypeScript를 싫어해서 쓰는 글이 아닙니다 — 오히려 정반대입니다! TypeScript가 없었다면 Motion은 지금까지 살아남지 못했을 겁니다. Motion은 스무 번이 넘는 피벗을 했습니다. TypeScript 덕분에 아이디어를 빠르게 검증할 수 있었고, 무엇보다 모든 개발자가 진정한 풀스택 방식으로 일할 수 있었습니다. 프런트엔드와 백엔드 사이에 코드를 신속하고 쉽게 공유할 수 있었고, 개발 환경을 크게 통일했으며, 물론 크고 활기찬 생태계의 이점도 누렸습니다.
우리의 프런트엔드는 React로 작성되어 있습니다. 모바일 앱? React Native. 데스크톱 앱? Electron. IaC(코드형 인프라)? 믿기 어렵겠지만 TypeScript입니다. 우리는 (지금도!) TypeScript의 열렬한 팬이었습니다.
하지만 시간이 지나며 상황이 변하기 시작했습니다.
어디서나 코드를 공유하겠다는 꿈은 끝내 완전히 실현되지 못했습니다. 처음부터 React Native와 나머지 생태계 사이에서 코드를 공유하는 데 문제가 있었습니다. 웹 앱의 나머지와 React, Tailwind 버전이 달랐고, 그 탓에 많은 핵심 라이브러리는 아예 공유하지 못했습니다. _공유_에 성공하더라도, 개발자들은 공유 라이브러리를 수정할 때 모바일을 종종 완전히 잊어버렸고, “이번엔 누가 모바일 앱을 깨뜨렸지?” 라는 숨바꼭질이 잦았습니다. 결국 성능이 중요한 일부 화면에서는 React Native를 포기해야 했습니다.
시간이 흐르며 대형 TypeScript 코드베이스가 겪는 전형적인 고통도 모두 겪었습니다. 언어 서버는 자주 크래시났고, CI 빌드는 끝없이 길어졌습니다(상당히 최적화한 파이프라인도 정기적으로 20분 이상 소요). 이 중 많은 부분은 언젠가 TS-Go로의 이전으로 해결될 겁니다 — 하지만 바로 이것이 TypeScript의 문제입니다. 너무 복잡한 것이 많고, 언젠가는(eventually) 좋아질 거라는 데 의심은 없지만, 급성장하며 스케일해야 하는 스타트업 입장에선 우리 운명을 스스로 통제할 수 없습니다.
우리는 (엄청나게 유능한) TypeScript 컴파일러 팀이 근본적인 변화를 해주길 기다려야 했고, (정말 열정적인) biome 팀이 특정 기능을 지원해 주길 기다려야 기본 린트 규칙을 CI 시간을 폭증시키지 않고 다시 활성화할 수 있었습니다.
모노레포가 컴파일이라도 되게 하려면 Zod v4가 나오길 기다려야 했습니다. v3에서는 TypeScript 컴파일러의 최대 재귀 깊이 문제와 맞닥뜨렸거든요! 그런데 애초에 왜 Zod가 필요할까요? 이런 의식(보일러플레이트)은 TypeScript에 런타임 타입이 없기 때문에 강요되는 것입니다.
ORM 쪽 이야기라고 더 낫지는 않습니다. 뼈아픈 교훈으로 알게 되었듯, Prisma는 where 구문이 undefined로 평가되면 전체 테이블을 삭제합니다. (그 외에도 정말 많은 버그를 겪었습니다.) Drizzle이 더 좋아 보였고, 우리는 몇 달 동안 스폰서도 했지만 아직 1.0도 아닙니다. 출시 후 몇 년간 버그로 고생하지 않는다는 보장은 어디에도 없죠.
생태계는 크고, 활기차고, 열정적입니다. 진심으로 웹을 더 낫게 만들고자 하는 사람들이 가득하고 최선을 다하고 있습니다. 하지만 결국 CTO인 제 입장에선 우리 회사의 운명을 남의 손에 맡길 수 없습니다. 이런 문제를 마주할 때마다 들던 무력감은 저를 잠식했고 — 특히 이런 문제들이 이미 성숙한 생태계에서는 대부분 해결되어 있다는 사실을 알기에 더 그랬습니다!
엔터프라이즈 세계에서 Java와 .NET이 군림하는 건 우연이 아닙니다. 런타임 타입은 기본입니다. 수백만 줄짜리 코드베이스에서 상대적으로 빠른 컴파일은 필수입니다. 수십 년에 걸쳐 성숙해진 ORM이 존재하고, 이는 기본 CRUD 작업에서는 개발자가 사실상 아무 것도 고민할 필요가 없음을 의미합니다: 실질적인 선택지는 _하나_뿐이니까요. 멀티스레딩이 다시 테이블에 올라오면서, 단일 스레드 이벤트 루프를 비워두기 위해 복잡한 워크플로 오케스트레이션 솔루션을 띄울 필요도 없습니다. 궁극적으로 이는 개발자가 수십 년 된 문제에 점점 기발한 해법을 짜내느라 애쓰는 대신, 새로운 문제 해결에 집중할 수 있음을 뜻합니다.
지루하냐고요? 네. 하지만 대규모 CRUD를 푸는 일이 흥미진진할 필요는 없습니다. 지루하고 안전해야 합니다. 지금은 2025년입니다 — 업계로서 우리는 버그 없이, 병목 없이, “스릴” 없이도 초당 수십만 쿼리를 안전하고 신뢰성 있게 처리하는 CRUD 앱을 스케일링하는 방법을 알고 있어야 합니다.
관 뚜껑을 닫게 만든 사건은 우리가 에이전틱 워크플로 시스템을 개발할 때 벌어졌습니다. 임의의 JavaScript를 실행할 수 있는 코드 블록이 필요했는데, 보안상 당연히 JavaScript를 실행하는 백엔드 컨테이너 안에서 그걸 수행할 수는 없었습니다. 마침 회사에서 큰(아직 공개되지 않은) 신규 이니셔티브도 추진 중이었습니다.
어쨌든 임의 코드 실행을 위해 다른 언어를 도입해야 했다면… 새 이니셔티브를 아예 그 새 언어로 시작할 수 있지 않을까? 그러자 어떤 언어로 갈 것인가라는 질문이 뒤따랐습니다.
두 해 전, 시카고에서 있었던(지금은 악명 높은) 오프사이트에서도 비슷한 기로에 섰었습니다. TypeScript는 진정으로 안전한 타입 시스템을 갖고 있지 않고 앞으로도 갖지 못할 겁니다. 그때도 이미 TypeScript 피로감이 있었습니다. 백엔드 개발자 다수가 새 언어로 옮기길 원했지만, 어떤 언어로 갈지에 대한 불꽃 튀는 논쟁 끝에 우리는 결국 TypeScript에 남았습니다. 이번에는 교훈을 얻었습니다: 투표는 없다! (농담입니다 — 실제로는 여섯 명 정도의 엔지니어와는 논의했지만, 완전히 공개된 포럼으로 만들고 싶지는 않았습니다.)
제가 세운 기준은, 가비지 컬렉션이 있고, 정적 타입이며, 성능이 좋고 생산성을 극대화하는 언어였습니다. 이 기준으로 진지하게 검토한 선택지는 C#과 Java 둘뿐이었습니다. 둘 다 크고 검증된 생태계를 갖고 있고, 극도로 성숙한 소프트웨어가 많습니다.
결국, 저는 C#을 선택했습니다. 직업적으로 써본 적이 없음에도요. 그 이유는 진심으로 Motion의 생산성이 훨씬 높아질 것이라 믿기 때문입니다:
Entity Framework
앞서 언급했듯, 우리가 겪은 답답할 정도로 단순한 문제들 중 상당수는 매우 견고하고 성능 좋은 ORM의 부재 때문이었습니다. 대부분의 B2B 소프트웨어는 기본 CRUD 작업을 중심으로 돌아가고, Motion도 예외가 아닙니다. 직접 만져보면서 저는 Entity Framework에 정말 감탄했습니다. Motion에서는 특히 중첩 모델에서의 소프트 삭제 처리가 늘 골칫거리였습니다. 하지만 EF Core의 전역 쿼리 필터를 쓰면 말 그대로 한 줄로 끝입니다. 더 고급 사례도 많습니다(테이블 스플리팅, 동시성 토큰, 네비게이션 프로퍼티). 하지만 저를 가장 인상 깊게 한 것은 변경 추적의 자연스러움이었습니다. Prisma나 Drizzle에서는 레포지토리 계층의 모든 메서드에 트랜잭션을 일일이 넘겨줘야 했지만, EF Core는 기본값이 바로 그런 동작입니다. 작업이 자연스럽게 컨텍스트를 변이시키고, 컨텍스트 저장만으로 모든 변경이 트랜잭션으로 커밋됩니다. 언제든 새 컨텍스트를 만들 수도 있고(혹은 기존 것을 저장할 수도) 있습니다.
TypeScript와의 유사성
두 언어는 같은 사람이 만들었습니다. 그래서 당연히 생김새와 느낌이 매우 비슷합니다. 많은 분들이 알아차리기 시작했고(심지어 마이크로소프트도 이렇게 광고합니다), 언어 구성 요소는 놀랍도록 닮아 있습니다. 둘 다 async/await, 비슷한 => 익명 함수 문법, nullable 타입 등 공통점이 많습니다. 아마 두 언어를 비교·대조하는 데 가장 좋은 사이트는 우리 시니어 스태프 엔지니어인 Charles Chen이 만든 이 페이지일 겁니다. 익숙함은 전환 과정에서 속도 저하를 최소화한다는 점에서 매우 중요했습니다.
생태계
C#은 다섯 번째로 인기 있는 프로그래밍 언어입니다(SQL, HTML, Bash 제외). TypeScript나 JavaScript보다는 덜 인기지만, 생태계 크기가 커질수록 선형적으로 이득이 늘어난다고 보진 않습니다. 오히려 로그 성장에 가깝습니다. 어느 시점을 넘으면 포화되고, 특히 많은 라이브러리의 품질이 솔직히 높지 않은 상황에서 라이브러리 하나 더 있다고 한계 효용이 커지진 않습니다. 그런 의미에서 기준은 이분법적입니다: 생태계가 충분히 큰가 —이지 _가장 큰가_가 아닙니다. 우리의 사용 사례에 비춰볼 때 C#은 충분히, 그것도 아주 넉넉히 합격점을 받았습니다. C#은 이벤트 소싱의 풍부한 역사, 액터 모델에 대한 세계적 수준의 지원, 정말 훌륭한 메시지 버스 등을 갖추고 있습니다.
AI 코딩
C#과 .NET은 AI 코딩 도구와 정말 잘 맞습니다. 일반적으로 가드레일이 강할수록 AI가 인간의 개입 없이 더 멀리 갈 수 있다고 느꼈습니다. 컴파일러 엄격함이 JavaScript나 Python보다 훨씬 강한 F#을 만져볼 때, 저는 클로드(Claude)에게 더 길고 복잡한 작업을 맡겨도 컴파일러가 흔한 문제를 대부분 잡아줄 거라 믿을 수 있음을 일관되게 경험했습니다. Jane Street 팟캐스트에서도 이 현상을 잠깐 언급합니다: 가드레일이 강할수록 AI가 더 멀리 갑니다. 코틀린과 엘릭서처럼 함수형 색채가 강한 언어들이 코드 생성에서 일관되게 좋은 성능을 보이는 것은 놀랍지 않습니다(C#도 상위 5위권!).
컴파일러의 엄격함을 떠나, C#은 Roslyn 컴파일러 API 덕분에 수백 개의 잘 정립되고 매우 성능 좋은 린트 규칙을 갖고 있습니다. 그중 하나는 모든 public 메서드와 필드에 구조화된 XML 문서를 요구하는데, 이는 AI 모델에 잘 먹히는 정석 경로로 알려져 있습니다. .NET 생태계 전반에 걸친 잘 만들어진 예제의 방대한 양과 결합하면, 제 매우 비과학적이고 감(바이브) 기반의 결론은 동일한 프롬프트 노력 대비 AI 모델이 TypeScript보다 C# 코드를 더 잘 작성한다는 것입니다.
.NET은 ‘힙’하지 않습니다. 그리고 그럴 만한 이유도 있죠. 오랫동안 윈도우 전용이었고, 꽤 고루하다는 평판이 있었습니다. 구글이 실리콘밸리(와 웹)를 휩쓸면서, 마운틴뷰발 기술이 사실상의 표준이 되었습니다.
하지만 .NET Core는 조용히, 꾸준히 진화해왔습니다. 지금의 C#은 예전과는 전혀 다릅니다. 모던하고, 리눅스 퍼스트이며, 오픈소스입니다. 엄청난 성능과 견고하고 성숙한 런타임을 갖추고 있습니다.
이건 다소 반대편에 선 의견일 수 있습니다. 하지만 열린 마음으로 통념에 도전하는 사고를 선호하는 개발자라면, Motion이 당신에게 맞는 곳일 겁니다. 우리는 채용 중입니다!
P.S. C#이 얼마나 생산적일 수 있는지 깨닫는 건 우리만이 아닙니다.