내 첫 프로젝트를 과도하게 엔지니어링했다: TypeScript와 Zig를 Bun으로 연결! 🚀
Source: Dev.to
안녕하세요, 안녕하세요! 다시 찾아온 오웬입니다!
한동안 활동을 쉬었는데, 다시 글을 올려야겠다고 생각했어요. 어떤 사람들은 코딩 프로젝트를 시작할 때 할 일 목록, REST API, 제너레이터 등을 먼저 만든다고 하죠. 저는 그 대신 바로 깊은 물에 뛰어들었습니다: 빠르게 뭔가를 만들고, 메모리 관리에 대해 배우며, 서로 다른 언어들이 어떻게 소통하는지 알아보고 싶었거든요.
그래서 Dunena를 만들었습니다—고성능 하이브리드 아키텍처 모노레포입니다. 웹 레이어에는 Bun과 TypeScript를 활용하고, 무거운 CPU 작업은 Zig를 통해 외부 함수 인터페이스(FFI)로 위임합니다. 또한 쿠버네티스에 배포했으며(아직 Docker와 K8s를 배우는 중입니다).
🔗 Dunena Repository on GitHub
🔗 Documentation
Dunena란?
핵심적으로 Dunena는 요청을 빠르고 효율적으로 처리하도록 설계된 백엔드 플랫폼입니다. 라우팅, WebSockets, pub/sub 서비스, 그리고 캐싱 레이어를 포함합니다.
진짜 마법은 내부에 있습니다: 저는 TypeScript의 빠른 개발 속도를 원했지만, Node/Bun이 압축, 블룸 필터, 복잡한 통계 계산과 같은 무거운 연산에 얽히는 것은 원하지 않았습니다.
기술 스택
- 런타임 및 모노레포 매니저: Bun 🥟
- 주요 언어: TypeScript (strict mode)
- 고성능 코어: Zig ⚡
- 데이터베이스: SQLite 🗄️
- 배포: Docker & Kubernetes 🐳
The Architecture
이 프로젝트에서 가장 멋지면서도 (가장 무서운) 부분은 FFI 브리지입니다. 여기서는 TypeScript에서 Zig의 거의 베어 메탈 실행으로 요청이 어떻게 전달되는지 보여줍니다.
Zig side
// zig/src/exports.zig
const std = @import("std");
// Export a function so Bun can read it via the C ABI
export fn compute_heavy_stats(input_val: i32) i32 {
// Imagine some incredibly complex, CPU‑blocking math here
var result = input_val * 42;
return result;
}TypeScript side
// packages/platform/src/bridge/ffi.ts
import { dlopen, FFIType, suffix } from "bun:ffi";
// Load the compiled Zig library
const path = `../../zig/zig-out/lib/libdunena_core.${suffix}`;
const { symbols } = dlopen(path, {
compute_heavy_stats: {
args: [FFIType.i32],
returns: FFIType.i32,
},
});
// Now I can call Zig directly from TypeScript!
export function runStats(input: number): number {
return symbols.compute_heavy_stats(input);
}클라이언트가 Bun 서버(apps/server/src/index.ts)에 요청을 보내면, 플랫폼이 API 라우팅을 처리하고 무거운 연산을 Zig에 넘긴 뒤 결과를 즉시 반환합니다.
방 안의 “AI” 코끼리
저는 AI 코딩 에이전트(Claude, Gemini)를 사용해 보일러플레이트 작업을 빠르게 하고, 모노레포 구조를 설정하며, 초기 파일 아키텍처를 스캐폴딩했습니다. AI 덕분에 빠르게 진행할 수 있었지만, 여전히 잔인하고 머리카락을 뽑을 정도로 힘든 디버깅과 수정 작업을 직접 해야 했습니다:
- AI가 Zig에서 데이터 타입을 변경했지만 TypeScript의 해당 FFI 정의를 업데이트하지 않으면, 서버가 메모리 접근 위반으로 크래시됩니다.
- 포인터 관리, 메모리 누수 방지, 그리고 연결된 SQLite 볼륨과 함께 Kubernetes를 작동시키는 작업은 세심한 인간의 감독이 필요했습니다.
이 경험을 통해 특히 저수준 시스템을 다룰 때 생성된 코드를 무조건 신뢰해서는 안 된다는 교훈을 얻었습니다.
가장 큰 도전 과제
1. 빈 공간을 가로지르는 메모리 관리
TypeScript와 Zig 사이에 데이터를 전달하는 것은 안전망이 없습니다. Bun의 가비지 컬렉터는 Zig의 메모리 관리에 대해 알지 못하므로 Zig에서 수동 할당과 defer 문을 배워야 했습니다.
2. Kubernetes에서 SQLite
Persistent Volume Claims를 통해 K8s pod에 SQLite(파일 기반 데이터베이스)를 배포하는 것은 단일 pod에 대해서는 작동하지만, 수평 확장을 하면 데이터베이스 락 문제가 발생할 수 있습니다.
다음은?
프로젝트는 아직 “완료” 단계가 아니다. 제가 혼자 유지보수하고 있지만 기여를 환영한다. 앞으로의 아이디어는 다음과 같다:
- 더 안전한 추상화를 통한 FFI 레이어 개선.
- 수평 확장을 위한 대안 스토리지 솔루션 탐색.
- 더 많은 고성능 Zig 모듈 추가 (예: 이미지 처리, 암호화).
연결해요!
커뮤니티의 피드백, 코드 리뷰, 혹은 조언을 언제든 환영합니다.
🔗 GitHub repository
🔗 Documentation
이렇게 언어를 섞어본 적이 있거나 FFI 경계에서 고민해 본 적이 있나요? 댓글로 알려주세요! 👇