본문으로 건너뛰기
Previous
Next
Elysia 완벽 가이드 — Bun 네이티브 초고속 TypeScript API 프레임워크

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 한계”뿐 아니라 타입·검증·문서화가 한 스키마에서 유지된다는 개발 속도와 오류 감소에도 있습니다.

요청 생명주기와 런타임 경계 (심화)

Elysia 앱은 Bun이 제공하는 HTTP 서버 추상화 위에서 동작하며, 각 요청은 대략 다음 순서를 따릅니다: 소켓 수락 → 라우트 매칭 →(플러그인 체인) onRequest/onBeforeHandle → 스키마 검증 → 핸들러 → 응답 직렬화 →(후처리 훅). 이 파이프라인을 염두에 두면 “어디서 시간이 새는지”를 계층별로 나눌 수 있습니다.

내부적으로 중요한 점은 세 가지입니다. 첫째, 검증은 핸들러 진입 전에 끝나므로, 비싼 I/O를 핸들러 첫 줄에 두기보다 명시적 서비스 레이어로 빼는 편이 테스트·재사용에 유리합니다. 둘째, decorate로 주입한 의존성은 요청 간 공유가 기본이므로, 요청 스코프 상태를 넣으면 동시 요청에서 섞입니다—요청 ID·사용자 컨텍스트derive와 함께 불변 스냅샷으로 다루는 패턴이 안전합니다. 셋째, Bun은 JSC + 네이티브 I/O 경로를 쓰므로 Node 전용 모듈·일부 fs/crypto 가정이 깨질 수 있습니다. CI에서 Bun으로만 돌리는 통합 테스트를 두면 “로컬(Node)에서는 되는데 스테이징(Bun)에서만 실패”를 줄일 수 있습니다.

프로덕션 배치에서는 단일 프로세스 리스너를 전제로 수평 확장(컨테이너 레플리카, 프로세스 매니저)을 쓰고, 세션·레이트 리밋은 Redis 등 외부 저장소에 두는 구성이 일반적입니다. 리버스 프록시 뒤에 둘 때는 X-Forwarded-* 신뢰 범위실제 클라이언트 IP 추출을 한 곳에서만 수행해 스푸핑 위험을 줄입니다.


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

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

10. 트러블슈팅

증상점검할 것
검증은 통과하는데 DB에서만 실패트랜잭션·풀 고갈, 타임아웃. decorate로 공유한 클라이언트가 요청마다 새로 만들어지지 않았는지 확인합니다.
스테이징에서만 5xxBun 버전·NODE_ENV·CORS·역프록시 헤더 차이. 동일 Docker 이미지로 재현합니다.
Eden Treaty 타입과 런타임 불일치응답 스키마 누락·any 핸들러. OpenAPI 플러그인과 함께 성공/실패 응답을 명시합니다.
WebSocket/SSE 끊김LB·CDN idle timeout, 업그레이드 허용, 스티키 세션 필요 여부.
메모리가 요청 수에 비례해 증가스트림·파일 핸들 미해제, 전역 캐시 무제한. finally와 백프레셔를 점검합니다.

우아한 종료: 배포 시 SIGTERM에서 새 요청 거부·in-flight 카운트 드레인을 하려면, Bun Server 참조를 유지해 stop()을 호출하는 패턴을 문서화합니다(런타임 버전별 API는 공식 문서로 확인).


11. 정리

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



자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Elysia는 Bun 네이티브 TypeScript 웹 프레임워크입니다. Eden Treaty, 플러그인, WebSocket·SSE, DB 연동, OpenAPI, 성능까지 실전 중심으로 정리합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

참고 자료

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Elysia 완벽 가이드 — Bun 네이티브 초고속 TypeScript API 프레임워크」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Elysia 완벽 가이드 — Bun 네이티브 초고속 TypeScript API 프레임워크」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Elysia, Bun, API, TypeScript, Performance 등으로 검색하시면 이 글이 도움이 됩니다.