Elysia 완벽 가이드 — Bun 네이티브 초고속 TypeScript API 프레임워크

Elysia 완벽 가이드 — Bun 네이티브 초고속 TypeScript API 프레임워크

이 글의 핵심

Bun 런타임에 최적화된 Elysia로 타입 안전한 API를 구축하는 방법을 다룹니다. 라우팅·검증·Eden Treaty·플러그인·실시간 통신·데이터베이스·OpenAPI 문서화까지 한 흐름으로 연결합니다.

이 글의 핵심

Elysia는 Bun에 맞춰 설계된 TypeScript 우선 웹 프레임워크입니다. Express나 Fastify가 범용 런타임(Node.js)을 넓게 지원하는 것과 달리, Elysia는 Bun의 성능 특성과 내장 API를 적극 활용해 지연 시간과 처리량에서 유리한 구조를 취합니다. 본 가이드는 단순한 문법 소개에 그치지 않고, 타입이 끝까지 유지되는 클라이언트 계약(Eden Treaty), 재사용 가능한 플러그인, 실시간 채널(WebSocket·SSE), 데이터베이스 계층과의 연동, OpenAPI 기반 문서 자동화까지를 실무 관점에서 연결합니다.

실무 관점: API 스펙 변경 시 클라이언트 빌드가 깨지는 문제는 대부분 “서버와 클라이언트의 타입이 별도로 관리되기 때문”입니다. Elysia는 서버 쪽 스키마를 단일 진실 공급원으로 두고 Eden으로 소비하므로, REST를 유지하면서도 RPC에 가까운 개발 경험을 얻을 수 있습니다.


1. 사전 요구 사항과 설치

  • Bun 최신 안정 버전(공식 문서 기준 설치)
  • TypeScript 프로젝트 또는 bun init으로 생성한 프로젝트
bun init
bun add elysia

개발 서버 실행은 애플리케이션 엔트리에서 Elysia 인스턴스를 만들고 listen으로 포트를 열면 됩니다. 아래는 최소 예시입니다.

// src/index.ts
import { Elysia } from 'elysia';

new Elysia()
  .get('/', () => 'OK')
  .listen(3000);

console.log('listening on :3000');

실행은 bun run src/index.ts 형태가 일반적입니다. Bun은 TypeScript를 별도 트랜스파일 없이 실행할 수 있어, 백엔드 프로토타이핑 속도가 빠릅니다.


2. 핵심 개념: 라우팅, 스키마, 컨텍스트

2.1 라우트와 HTTP 메서드

Elysia는 get, post, put, patch, delete 등 메서드 체인으로 라우트를 등록합니다. 경로 파라미터는 :id 형태로 선언하고, 핸들러 인자에서 구조 분해하여 사용합니다.

import { Elysia, t } from 'elysia';

const app = new Elysia()
  .get('/users/:id', ({ params }) => ({
    userId: params.id,
  }))
  .post(
    '/users',
    ({ body }) => ({ created: true, name: body.name }),
    {
      body: t.Object({
        name: t.String({ minLength: 1 }),
      }),
    },
  );

body, query, params, headers 등 검증 대상은 옵션 객체의 스키마로 선언합니다. Elysia는 런타임 검증정적 타입 추론을 동시에 제공하므로, 핸들러 내부에서 body의 타입이 좁혀진 상태로 다룰 수 있습니다.

2.2 t 스키마와 타입 안전성

t는 Elysia의 타입 스키마 빌더입니다. 문자열·숫자·객체·배열·유니온 등을 조합해 요청 단위로 검증 규칙을 명시합니다. 잘못된 요청은 프레임워크 레벨에서 거절되므로, 핸들러마다 수동 if 검증을 반복할 필요가 줄어듭니다.

2.3 인스턴스와 상태, 파생 값

.state(), .decorate() 등으로 애플리케이션 전역 상태나 의존성을 주입할 수 있습니다. 예를 들어 DB 커넥션 풀, 로거, 설정 객체를 한 번만 등록하고 모든 라우트에서 동일한 인터페이스로 접근하게 만들 수 있습니다. 이는 테스트 시 모킹 지점을 명확히 하고, 운영 환경별 설정 분기를 단순화합니다.


3. Eden Treaty: 엔드투엔드 타입 안정성

Eden Treaty는 Elysia 서버 타입을 클라이언트가 그대로 참조하도록 해 주는 클라이언트 유틸리티입니다. 패키지는 보통 @elysiajs/eden을 사용하며, treaty로 서버 앱 타입에 바인딩된 클라이언트를 생성합니다.

3.1 서버 측: 스키마가 곧 API 계약

서버에서 모든 라우트에 대해 요청·응답 스키마가 정의되어 있어야 Eden이 추론할 재료가 생깁니다. 암시적 any가 많은 핸들러는 Treaty 이점이 줄어듭니다.

3.2 클라이언트 측: 타입이 보장되는 호출

// client.ts (예: 프론트엔드 또는 다른 서비스)
import { treaty } from '@elysiajs/eden';
import type { App } from './server'; // Elysia 앱 타입 export

const client = treaty<App>('http://localhost:3000');

const { data, error } = await client.users.post({
  name: 'Alice',
});

if (error) {
  // 에러 응답 타입도 좁혀질 수 있음
}
// data는 성공 시 응답 타입과 일치

이 패턴의 실무적 이점은 다음과 같습니다. 첫째, API 변경이 클라이언트 컴파일 오류로 즉시 드러남니다. 둘째, OpenAPI를 수동으로 맞추지 않아도 “스키마 우선” 개발 흐름이 유지됩니다. 셋째, 프론트와 백을 다른 저장소로 나눈 경우에도 공유 패키지로 App 타입만 노출하면 동일한 효과를 낼 수 있습니다.

한계도 이해해야 합니다. 서버 타입을 클라이언트 빌드에 끌고 오면 빌드 그래프가 결합되므로, 버전 관리와 배포 순서 전략이 필요합니다. 또한 런타임은 여전히 네트워크 경계이므로, 타입이 맞아도 네트워크 오류·타임아웃 처리는 별도로 두어야 합니다.


4. 플러그인 시스템

Elysia는 .use()로 플러그인을 합성합니다. 플러그인은 라우트 prefix, 공통 훅, 스키마 확장을 캡슐화합니다.

import { Elysia } from 'elysia';

const authPlugin = new Elysia({ name: 'auth' })
  .derive(({ headers }) => {
    const token = headers.authorization?.replace('Bearer ', '');
    return { token };
  })
  .onBeforeHandle(({ token, set }) => {
    if (!token) {
      set.status = 401;
      return 'Unauthorized';
    }
  });

const app = new Elysia()
  .use(authPlugin)
  .get('/protected', () => 'secret');

플러그인을 나누는 기준은 다음과 같습니다. 인증·로깅·레이트 리밋처럼 여러 라우트에 공통으로 적용되는 횡단 관심사는 플러그인으로 두고, 도메인별 기능은 users, orders처럼 라우트 그룹으로 분리합니다. name을 부여하면 디버깅 시 어떤 플러그인이 연결되었는지 추적하기 쉽습니다.

운영 시에는 플러그인 순서가 요청 생명주기에 영향을 준니다. onBeforeHandle 체인 순서대로 실행되므로, 인증 실패 시 조기 반환이 되도록 인증 플러그인을 앞쪽에 두는 일이 많습니다.


5. WebSocket과 Server-Sent Events

5.1 WebSocket

실시간 양방향 통신에는 Elysia가 제공하는 .ws() 핸들러를 사용합니다(공식 문서의 WebSocket 패턴). 연결·메시지·종료를 라우트 스타일로 다룰 수 있어, 일반 HTTP 핸들러와 동일한 패턴으로 코드베이스를 유지하기 쉽습니다.

import { Elysia } from 'elysia';

const app = new Elysia().ws('/ws', {
  message(ws, message) {
    ws.send(`echo: ${message}`);
  },
});

실무 팁: WebSocket은 방화벽·프록시·로드 밸런서 환경에서 HTTP 업그레이드와 스티키 세션 이슈가 발생하기 쉽습니다. 스케일아웃 시에는 Redis 등으로 세션·룸 상태를 공유하거나, 메시지 브로커를 병행하는 설계를 검토합니다.

5.2 Server-Sent Events(SSE)

서버→클라이언트 단방향 푸시에는 SSE가 적합합니다. Elysia 생태계에서는 @elysiajs/stream 등 스트리밍 관련 플러그인을 활용하는 패턴이 문서화되어 있습니다. SSE는 HTTP/1.1에서도 동작하며, 재연결·이벤트 ID를 통한 재개를 표준으로 다룰 수 있어 알림·진행률 스트림에 잘 맞습니다.

SSE를 선택할 때는 동시 연결 수리버스 프록시 타임아웃을 확인해야 합니다. 예를 들어 Nginx는 proxy_read_timeout을 충분히 주지 않으면 장시간 스트림이 끊길 수 있습니다. WebSocket과 달리 브라우저의 EventSource API로 소비할 수 있어 클라이언트 구현이 단순해지는 경우가 많습니다.


6. 데이터베이스 통합

Elysia 자체에 ORM이 포함되어 있지는 않습니다. 대신 Bun과 궁합이 좋은 라이브러리를 플러그인 또는 decorate로 주입하는 방식이 일반적입니다.

6.1 Drizzle ORM 예시

Drizzle은 가볍고 SQL에 가까운 API로 타입 안전성을 제공합니다. drizzle-orm과 드라이버(postgres, bun:sqlite 등)를 설치한 뒤, 단일 DB 인스턴스를 Elysia에 붙입니다.

import { Elysia } from 'elysia';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { Database } from 'bun:sqlite';
import { users } from './schema';

const sqlite = new Database('app.db');
const db = drizzle(sqlite);

const app = new Elysia()
  .decorate('db', db)
  .get('/users', async ({ db }) => {
    return await db.select().from(users);
  });

6.2 Prisma·기타

Prisma를 쓸 경우에도 동일하게 클라이언트를 생성해 decorate('prisma', prisma) 형태로 주입하면 됩니다. 중요한 것은 요청당 연결 폭주를 막는 것입니다. 서버리스가 아닌 long-running 프로세스에서는 연결 풀을 공유하고, 서버리스(Bun on Lambda 등)라면 공식 문서에 맞는 연결 전략을 따릅니다.

트랜잭션 경계는 핸들러 안에서 명시적으로 열고, 실패 시 롤백·재시도 정책을 서비스 레이어에 모아 두는 편이 운영에 유리합니다.


7. OpenAPI 자동 생성

@elysiajs/openapi 플러그인을 사용하면 라우트에 붙은 스키마로부터 OpenAPI 스펙과 문서 UI(기본 Scalar 등)를 생성할 수 있습니다.

import { Elysia } from 'elysia';
import { openapi } from '@elysiajs/openapi';

const app = new Elysia().use(
  openapi({
    documentation: {
      info: {
        title: 'My API',
        version: '1.0.0',
      },
    },
  }),
);

// 이후 각 라우트에 summary, description, tags 등 메타데이터를 붙이면 문서 품질이 올라갑니다.

효과: 프론트엔드·외부 파트너와 계약을 맞출 때 JSON/YAML 스펙을 단일 산출물로 내보낼 수 있고, 내부적으로는 Eden Treaty와 함께 “코드가 곧 문서” 상태를 유지할 수 있습니다. 다만 민감한 내부 엔드포인트는 detail.hide 등으로 문서에서 제외하는 것이 안전합니다.


8. 성능 벤치마크와 기대치

정량 수치는 하드웨어·Bun 버전·벤치 시나리오(JSON 직렬화, DB 유무, 연결 수)에 따라 크게 달라집니다. 공개 벤치마크(TechEmpower 등)에서 Elysia·Bun 조합은 순수 JSON 응답·정적 라우팅에서 매우 높은 처리량을 보이는 경우가 많습니다. 다만 실제 서비스는 DB 쿼리·외부 API·직렬화 비용이 지배적이므로, 프레임워크만 바꿔 극적인 지연 개선이 나오지는 않을 수 있습니다.

실무에서는 다음을 권장합니다.

  • 프로파일링 우선: CPU vs I/O 병목을 구분한 뒤 프레임워크를 평가합니다.
  • 비교 시 동일 조건: keep-alive, 워커 수, body 크기를 맞춥니다.
  • Bun 업데이트 주기: 런타임 개선이 곧 처리량·GC 특성에 영향을 줍니다.

Elysia의 이점은 “순수 RPS 한계”뿐 아니라 타입·검증·문서화가 한 스키마에서 유지된다는 개발 속도와 오류 감소에도 있습니다.


9. 보안·운영 체크리스트

  • 입력 검증: t 스키마로 경계를 명확히 하고, 파일 업로드·쿼리스트링도 빠짐없이 다룹니다.
  • 인증·인가: 플러그인으로 토큰 검증 후 역할 기반 접근을 라우트 그룹에 적용합니다.
  • CORS: 브라우저 클라이언트를 붙일 때 @elysiajs/cors 등으로 출처를 제한합니다.
  • 헬스 체크: 로드 밸런서용 /health는 의존성(DB) 확인 여부를 정책에 맞게 포함합니다.
  • 로깅·트레이싱: 요청 ID·상태 코드·지연 시간을 구조화 로그로 남깁니다.

10. 정리

Elysia는 Bun 위에서 타입·검증·문서·클라이언트 계약을 한 줄기로 묶기 좋은 프레임워크입니다. Eden Treaty로 엔드투엔드 타입 안전성을 확보하고, 플러그인으로 횡단 관심사를 정리하며, WebSocket·SSE로 실시간 요구를 처리하고, Drizzle 등으로 데이터 계층을 붙인 뒤 OpenAPI로 문서를 자동화하는 흐름이 자연스럽게 이어집니다. 성능은 벤치마크보다 실제 병목 측정과 함께 평가하는 것이 바람직합니다.


참고 자료