1인 개발자로서 6개월간 경험한 Bun vs Node
요약
1인 개발자가 6개월간 Bun과 Node.js를 실무에 사용하며 경험한 비교 분석입니다. Bun의 빠른 콜드 스타트와 TypeScript 기본 지원의 장점, 그리고 네이티브 의존성 문제로 인한 Node.js 유지 필요성을 다룹니다.
핵심 포인트
- Bun은 Node.js 대비 약 4배 빠른 콜드 스타트 속도를 제공함
- TypeScript와 .env 파일을 별도 설정 없이 즉시 실행 가능함
- 반복 실행이 잦은 스크립트에는 Bun이 유리함
- 네이티브 의존성 및 FFI 관련 오류 발생 시 Node.js 유지가 권장됨
-
6개월간 실제 사용 후 4개의 스크립트는 Bun으로 옮겼고, 6개는 Node에 남겨두었습니다.
-
Bun의 시작 속도는 4배 더 빠르며, 이는 하루에 200번씩 실행하는 스크립트에서 매우 중요합니다.
-
네이티브 의존성 (Native deps) 및 FFI (Foreign Function Interface) 문제로 두 번 오류가 발생하여, cron 작업은 Node에 그대로 두었습니다.
-
저의 규칙: 반복 실행이 잦은 루프(hot loops)는 Bun으로, 이상한 npm 패키지를 건드리는 것은 Node로 유지합니다.
6개월 전 저는 스튜디오의 모든 스크립트를 Node에서 실행했습니다. 오늘날 그중 4개는 Bun에서 실행되고 6개는 여전히 Node에서 실행되고 있으며, 저는 어떤 것을 무엇으로 사용할지에 대한 명확한 규칙을 가지고 있습니다. Bun이 문제를 일으켜 두 번이나 롤백해야 했던 상황을 포함하여 실제로 어떤 일이 있었는지 알려드리겠습니다.
왜 Bun을 시도했는가
저는 1인 스튜디오를 운영합니다. 제품 이미지를 생성하는 것부터 소셜 미디어 포스팅, 블로그 페이지 구축에 이르기까지 모든 것을 수행하는 약 30개의 스크립트를 가지고 있습니다. 대부분은 규모가 작습니다. 40개의 이미지를 크기 조정하는 스크립트, API를 호출하고 JSON을 작성하는 스크립트, 폴더 내 파일들에 템플릿을 적용하는 스크립트 같은 것들입니다.
작은 스크립트들의 특징은 제가 끊임없이 실행한다는 점입니다. 제품 출시 작업을 할 때 제 이미지 준비 스크립트는 하루에 약 200번 정도 실행됩니다. Node는 제 코드가 시작되기도 전에 매번 약 120밀리초(ms)의 시작 시간이 걸립니다. 이를 200배 하면, 저는 하루에 단지 Node가 깨어나기를 기다리는 데만 24초를 소비하게 됩니다. 아주 작게 들릴 수도 있습니다. 하지만 업무량이 많은 일주일 동안 이를 합산하면, Alt-Tab을 누르게 만들고 집중력을 흐트러뜨리는 실질적인 마찰(friction)이 됩니다.
제 컴퓨터에서 Bun의 콜드 스타트 (cold start)는 약 30밀리초(ms) 정도 걸립니다. 이는 대략 4배 더 빠른 속도입니다. 200번 실행하는 스크립트의 경우, 시작 속도 차이만으로도 하루에 약 18초를 절약할 수 있습니다. 이 역시 개별적으로 보면 작아 보이지만, 속도 차이는 모든 곳에서 나타납니다. 동일한 package.json에서 npm install이 거의 1분 가까이 걸렸던 반면, bun install은 몇 초 만에 끝납니다. TypeScript는 빌드 단계나 ts-node, tsx 없이도 직접 실행됩니다. 저는 그저 script.ts를 작성하고 bun script.ts를 실행하기만 하면 됩니다.
그 마지막 부분이 진정한 매력 포인트였습니다. Node와 TypeScript를 사용할 때 제가 느꼈던 마찰의 절반은 빌드 과정(build dance) 때문이었습니다. tsconfig를 설정하고, 러너(runner)를 선택하고, 끝이 없는 ESM 대 CommonJS 논쟁을 처리해야 했죠. Bun은 TypeScript와 JSX를 즉시 실행할 수 있으며, 제가 dotenv를 임포트하지 않아도 .env 파일을 읽어옵니다. 스크립트를 작성하고 바로 실행하기를 원하는 1인 개발자에게 이는 세 단계를 줄여줍니다.
그래서 테스트 삼아 마이그레이션할 4개의 스크립트를 선정했습니다. 두 개의 이미지 유틸리티, 하나의 API 페처(fetcher), 그리고 하나의 파일 템플릿 도구입니다. 이 중 어떤 것도 데이터베이스나 네이티브 바이너리(native binary)를 건드리지 않았습니다. 몇 개의 npm 패키지를 포함한 순수한 JavaScript와 TypeScript였죠. 만약 Bun이 어디에서든 승리한다면, 바로 여기서 첫 승리를 거둘 것이었습니다. 제가 이러한 도구들을 어떻게 구조화하는지에 대한 전체적인 모습이 궁금하시다면, Claude Blueprint에서 스튜디오 설정 전체를 다루고 있습니다.
내가 옮긴 4개의 스크립트와 그 이유
첫 번째 스크립트는 이미지 준비 도구였습니다. 폴더를 읽고, 크기를 조정하고, 이름을 변경하며, 제품 목록을 위한 변형 파일들을 생성합니다. 내부적으로는 Sharp를 사용합니다. 이 스크립트는 하루에 200번이나 실행해야 하는 문제 때문에 제가 가장 빠르게 만들고 싶었던 것이었습니다.
Sharp는 네이티브 의존성(native dependency)이라서 불안하기도 했지만(이 주의사항에 대해서는 나중에 더 자세히 다루겠습니다), Bun 환경에서 깔끔하게 설치되었고 문제없이 실행되었습니다. 전체 파이프라인이 눈에 띄게 빨라졌는데, 주로 시작 시간(startup time)과 더 빠른 파일 I/O 덕분이었습니다. Bun에는 제가 사용하던 Node의 fs 호출보다 더 빠르게 읽고 쓸 수 있는 내장 Bun.file API가 있습니다. 읽기 및 쓰기 부분을 이를 사용하도록 다시 작성했고, 실행 시마다 시간을 추가로 단축했습니다.
두 번째 스크립트는 몇 개의 엔드포인트(endpoint)에 접속하여 JSON을 병합하는 API 페처였습니다. Bun은 최신 Node와 마찬가지로 fetch가 내장되어 있어, axios나 node-fetch가 필요 없습니다. 이 스크립트는 거의 변경 사항 없이 옮겨졌습니다. 의존성 두 개를 삭제했는데도 그냥 잘 작동했습니다.
세 번째는 템플릿 폴더를 입력받아 블로그 페이지 스켈레톤 (skeleton)을 생성하는 파일 템플릿 도구였습니다. 순수하게 문자열 작업과 파일 쓰기만 수행하는 도구입니다. Bun의 더 빠른 파일 작업 (file operations) 덕분에 이 도구는 즉각적으로 느껴졌습니다. 대량의 배치를 처리할 때 약 3초 정도 걸렸으나, 이제는 1초 미만으로 단축되었습니다.
네 번째는 작은 로컬 테스트 러너 (test runner)였습니다. Bun은 저의 단순한 사례들에 충분히 Jest와 호환되는 내장 테스트 러너를 가지고 있습니다. 저는 Jest를 완전히 제거했습니다. 제 테스트 스위트 (test suite)의 시작 시간이 4초에서 1초 미만으로 줄어들었습니다. 도구가 너무 무거워서 테스트 작성을 피하는 1인 개발자에게, 빠르고 내장된 러너는 실제로 제가 더 많은 테스트를 작성하게 만들었습니다.
이 4가지 사례 전체를 관통하는 패턴은 명확합니다. 그것들은 핫 루프 (hot loops)이거나 제가 지속적으로 실행하는 것들이었으며, 이상한 것에 의존하지 않았습니다. 파일 I/O, fetch, 그리고 한두 개의 잘 작동하는 패키지만을 사용했습니다. Bun은 한 더미의 의존성 (dependencies)을 내장 기능 (built-ins)으로 대체했습니다. 저는 이 스크립트들을 통해 dotenv, node-fetch, 그리고 TypeScript 빌드 러너를 제거했습니다. 의존성이 적다는 것은 6개월 뒤에 업데이트할 때 고장 날 요소가 적다는 것을 의미합니다. 이러한 유지보수 절감은 제가 처음에는 과소평가했다가 나중에 감사하게 되는 부분입니다. 저는 Claude Blueprint 개요에서 바로 그 트레이드오프 (tradeoff)에 대해 작성한 바 있습니다.
Node에 남겨둔 6개의 스크립트
이제 숨 가쁘게 찬양하는 Bun 블로그 포스트에는 아무도 쓰지 않는 내용을 다루겠습니다. 6개의 스크립트는 Node에 남겨두었으며, 그중 2개는 Bun이 그것들을 망가뜨렸기 때문에 Node에 남겨두었습니다.
첫 번째 문제는 FFI였습니다. 저는 저수준 오디오 작업을 수행하기 위해 Node의 외부 함수 인터페이스 (FFI, Foreign Function Interface)를 통해 네이티브 라이브러리를 호출하는 스크립트를 가지고 있었습니다. Bun은 자체적인 FFI API인 bun:ffi를 가지고 있으며, 이는 진정으로 빠릅니다. 하지만 기존의 Node FFI 코드를 포팅한다는 것은 다른 API 표면 (API surface)에 맞춰 바인딩을 다시 작성해야 함을 의미했고, 그중 하나의 타입 매핑이 제가 예상한 대로 동작하지 않았습니다. 오후 내내 씨름하다가 설명할 수 없는 세그멘테이션 오류 (segfault)를 겪었고, 결국 해당 스크립트는 이미 Node에서 잘 작동하고 있으니 그대로 두기로 결정했습니다. 교훈을 얻었습니다: 만약 스크립트가 FFI를 사용하고 이미 잘 작동한다면, 속도가 핵심적인 목적이 아닌 이상 옮기지 마세요.
두 번째 문제는 Node의 ABI (Application Binary Interface)용으로 사전 빌드된 바이너리를 제공하지만 Bun용으로는 제공하지 않는 네이티브 의존성 (native dependency)이었습니다. 패키지는 설치되었지만, 제공된 바이너리가 Node 전용 심볼 (symbol)을 기대했기 때문에 런타임 (runtime)에 오류가 발생했습니다. Sharp는 유지 관리자들이 Bun을 대상으로 테스트하기 때문에 잘 작동했습니다. 하지만 이 다른 패키지는 그렇지 않았고, 저는 패치된 빌드를 직접 유지 관리할 생각이 없었습니다. 다시 Node로 돌아갔습니다.
나머지 4개의 스크립트는 지루한 이유들 때문에 Node에 남겨두었는데, 사실 그게 가장 좋은 이유들입니다. 그중 2개는 제가 건드리고 싶지 않은 서버에서 예약된 크론 잡 (cron jobs)으로 실행됩니다. 그것들은 잘 작동합니다. 몇 달 동안 수정 없이 실행되어 왔습니다. 시작 시간을 90밀리초 (milliseconds) 아끼기 위해 안정적인 크론 잡의 런타임을 교체하는 것은 나쁜 거래입니다. 위험은 실재하지만 보상은 아무것도 없습니다. 새벽 4시에 크론 잡을 기다리는 사람은 아무도 없기 때문입니다.
마지막 2개는 npm의 심층적인 영역에 있는 패키지들에 크게 의존하는 스크립트들입니다. 모듈 시스템을 영리하게 다루거나 특정 Node 내부 구조 (internals)를 가정하는 종류의 패키지들 말입니다. Bun의 Node 호환성은 좋고 빠르게 개선되고 있지만, "좋다"는 것이 "동일하다"는 뜻은 아닙니다. 패키지가 process.binding이나 문서화되지 않은 Node의 구석진 곳을 건드릴 때, 그 문제는 런타임에야 알게 됩니다. 제품 출시를 위해 의존하는 스크립트들에 대해서라면, 저는 갑작스러운 런타임 오류를 원하지 않습니다. 제가 검증하지 않은 의존성을 사용하는, 하중을 견뎌야 하는(load bearing) 모든 작업에 대해 Node는 안전한 기본값입니다.
저처럼 Shopify를 통해 스튜디오를 운영한다면, storefront API와 통신하는 스크립트들이 바로 제가 Node에 남겨둔 하중을 견뎌야 하는(load bearing) 작업들입니다. fetcher는 옮겼지만, publish 파이프라인은 그대로 유지했습니다.
제가 현재 사용하는 규칙
6개월이 지난 지금, 저는 더 이상 이 문제로 고민하지 않습니다. 저만의 체크리스트가 있으며, 이를 적용하는 데는 약 10초 정도밖에 걸리지 않습니다.
다음 세 가지 조건이 모두 충족된다면 Bun으로 옮기세요. 첫째, 실행 빈도가 높거나 시작 시간(startup) 및 파일 I/O 속도가 중요한 핫 루프(hot loop) 내에 있는 경우입니다. 둘째, 의존성(dependencies)이 Bun에 내장되어 있거나 Bun에서 테스트가 완료된 잘 알려진 패키지인 경우입니다. 셋째, 예상치 못한 런타임 에러(runtime error)가 발생했을 때 제품 출시(product drop)에 차질을 줄 만큼 하중을 견뎌야 하는(load bearing) 성격의 작업이 아닌 경우입니다.
다음 중 하나라도 해당한다면 Node에 그대로 두세요. 이미 잘 작동하고 있는 FFI를 사용하는 경우입니다. Bun 호환 바이너리(binary)를 제공하지 않는 네이티브 의존성(native dependency)을 사용하는 경우입니다. 아무도 기다리지 않는 안정적인 예약 작업(scheduled job)으로 실행되는 경우입니다. 또는 Node 내부 구조(internals)를 건드리는 npm 패키지에 의존하는 경우입니다.
솔직히 말해서, 제가 옮긴 4개의 스크립트 중 속도가 유의미한 것은 2개뿐입니다. 이미지 준비 도구와 템플릿 도구는 제품 출시(drop) 기간 동안 끊임없이 실행하기 때문에 실제로 체감할 수 있는 속도 향상을 얻었습니다. API fetcher와 테스트 러너(test runner) 역시 더 빠르긴 하지만, 거기서 얻은 이점은 순수 속도가 아니라 더 적은 의존성과 빌드 설정(build config)이 전혀 필요 없다는 점입니다. 이 차이를 명확히 구분하는 것이 중요합니다. Bun은 모든 것을 눈에 띄게 빠르게 만들어주는 마법의 가루가 아닙니다. 더 빠른 시작 시간, 더 빠른 설치, 더 빠른 파일 I/O, 그리고 유용한 내장 기능(built-ins)의 집합체입니다. 이러한 특정 요소들이 병목 현상(bottleneck)이 되는 지점에서 그 효과를 가장 크게 느낄 수 있습니다.
한 가지 더 실질적인 참고 사항이 있습니다. 저는 이유 없이 하나의 프로젝트 내에서 여러 런타임(runtime)을 혼용하지 않습니다. 제가 마이그레이션(migration)한 각 스크립트는 독립적(standalone)이므로, node_modules가 혼동되는 일은 없습니다. 만약 하나의 엔트리 포인트(entry point)를 가진 대규모 앱을 운영 중이라면, 어설프게 바꾸는 것보다 전체를 위해 하나의 런타임을 선택하는 것이 더 깔끔합니다. 저는 이러한 도구들을 작고 독립적으로 유지하는 방법을 Claude Blueprint 가이드에서 다루었으며, 이러한 작은 스크립트를 작성하는 습관 덕분에 Bun으로의 마이그레이션이 처음부터 위험 부담이 낮았습니다.
음성 생성이 필요한 미디어 작업의 경우, 저는 여전히 API를 통해 ElevenLabs를 호출하며, 해당 스크립트는 어떤 런타임에서 실행되는지 상관하지 않습니다. 런타임 선택은 스크립트가 실제로 로컬 작업(local work)을 수행할 때만 중요합니다.
결론 (Bottom Line)
하루 종일 실행하는 작은 스크립트들을 가진 1인 개발자라면 Bun을 시도해 볼 가치가 있습니다. 이점은 확실합니다: 4배 빠른 시작 속도, 거의 즉각적인 설치, 빌드 단계가 없는 TypeScript 지원, 그리고 의존성(dependencies)을 삭제할 수 있게 해주는 내장된 fetch, test, env 처리 기능입니다. 저는 4개의 스크립트를 옮겼고, 그렇게 하길 잘했다고 생각합니다.
하지만 아직 전면적인 교체 대상은 아닙니다. FFI(Foreign Function Interface)와 특이한 네이티브 의존성(native dependencies)은 까다로운 부분이며, 안정적인 크론 잡(cron jobs)을 바꿀 이유는 없습니다. 제 스크립트 중 6개는 Node에 그대로 남았으며, 이는 실패가 아니라 올바른 결정이었습니다.
솔직한 교훈은 속도와 단순함이 실제로 이득을 주는 스크립트들은 마이그레이션하고, 부하가 크거나 의존성이 많거나 이미 안정적인 것들은 그대로 두라는 것입니다. 이러한 분리 방식은 6개월 동안 아무런 후회 없이 유지되었습니다.
작고 교체 가능한 스크립트들로 어떻게 1인 스튜디오를 운영하는지에 대한 전체적인 그림을 알고 싶다면, Claude Blueprint에서 전체 설정을 살펴볼 수 있습니다. 먼저 한 번 쓰고 버릴 스크립트에 Bun을 적용해 보세요. 한 시간 안에 그것이 당신의 워크플로우(workflow)에 맞는지 알 수 있을 것입니다.
이 기사에는 제휴 링크가 포함되어 있습니다. 이 링크를 통해 가입하시면, 귀하에게 추가 비용 부담 없이 저에게 소정의 수수료가 지급될 수 있습니다. (광고)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기