개인정보처리방침© 2026 DEV BAK - 기술블로그. All rights reserved.
DEV BAK - 기술블로그
포스트 검색
Backend

tRPC v11이 Next.js App Router에서 REST를 대체하는 방식 — 코드 생성 없는 end-to-end 타입 안전성과 서버 스트리밍

REST API 엔드포인트를 하나 추가할 때마다 반복하는 작업이 있습니다. 서버에 라우트 파일을 만들고, 요청·응답 타입을 별도로 정의하고, 클라이언트 fetch 래퍼를 작성하고, 두 파일 사이에서 타입이 어긋나지 않았는지 직접 확인하는 과정입니다. 엔드포인트가 늘어날수록 이 관리 비용은 선형이 아니라 지수적으로 커집니다. 응답 타입 변경 하나가 fetch 래퍼, 호출 컴포넌트, 테스트 픽스처 수정으로 연쇄됩니다.

tRPC v11은 이 구조를 근본부터 바꿉니다. TypeScript 추론 엔진을 적극 활용해 서버 함수의 타입이 클라이언트로 그대로 흘러들어오는 구조를 만들고, Next.js App Router의 RSC 패러다임과 맞물려 서버에서 미리 가져온 데이터가 클라이언트 React Query 캐시로 자연스럽게 이어집니다.

v11에서 핵심적으로 달라진 점은 TanStack Query와의 통합 방식입니다. 기존의 tRPC 전용 훅 레이어가 사라지고, TanStack Query 네이티브 queryOptions / mutationOptions API를 직접 소비하는 구조로 전환됐습니다.

이 글에서는 tRPC v11을 Next.js App Router 환경에 세팅하는 방법부터, RSC에서 데이터를 프리패칭해 클라이언트 워터폴을 제거하는 패턴, SSE 기반 실시간 구독, 그리고 실무에서 흔히 빠지는 함정들까지 코드와 함께 짚어봅니다.


핵심 개념

Procedure와 Router — 서버 함수의 단위

tRPC에서 서버 로직의 최소 단위는 Procedure입니다. 세 종류가 있습니다.

종류 역할 HTTP 비유
query 데이터 읽기 GET
mutation 데이터 쓰기/변경 POST / PUT / DELETE
subscription 서버 → 클라이언트 이벤트 스트림 SSE

여러 Procedure를 Router로 묶고, 라우터를 중첩해 도메인별로 구조화할 수 있습니다.

typescript
// server/routers/post.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
 
export const postRouter = router({
  list: publicProcedure
    .input(z.object({ limit: z.number().min(1).max(100).default(10) }))
    .query(async ({ input, ctx }) => {
      return ctx.db.post.findMany({ take: input.limit });
    }),
 
  create: publicProcedure
    .input(z.object({ title: z.string().min(1), content: z.string() }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db.post.create({ data: input });
    }),
});
 
// server/routers/index.ts
export const appRouter = router({
  post: postRouter,
});
 
export type AppRouter = typeof appRouter;

이 AppRouter 타입 하나만 클라이언트로 임포트하면 모든 Procedure의 입출력 타입이 자동으로 추론됩니다. Codegen 단계가 없습니다.


Context — 요청별 데이터 주입

createTRPCContext는 각 요청마다 실행돼 Procedure에 주입할 객체(DB 연결, 인증 세션 등)를 만듭니다.

typescript
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { cache } from 'react';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
 
// App Router 환경에서는 cache()로 감싸 요청당 1회만 생성
export const createTRPCContext = cache(async () => {
  const session = await auth();
  return { db, session };
});
 
const t = initTRPC.context<typeof createTRPCContext>().create();
 
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
  if (!ctx.session) throw new TRPCError({ code: 'UNAUTHORIZED' });
  // session!으로 TypeScript에 non-null을 전달해 이후 ctx.session 접근의 타입을 확정
  return next({ ctx: { ...ctx, session: ctx.session! } });
});

createTRPCContext를 React cache()로 감싸지 않으면 동일 요청 내에서 DB 연결이 여러 번 만들어집니다. App Router 환경에서는 필수입니다.


v11의 React Query 통합 방식 변화

v10과 v11의 가장 큰 차이는 React Query와의 통합 방식입니다.

typescript
// v10 — tRPC 전용 훅 레이어
const { data } = trpc.post.list.useQuery({ limit: 10 });
 
// v11 — TanStack Query 네이티브 인터페이스
const trpc = useTRPC();
const { data } = useQuery(trpc.post.list.queryOptions({ limit: 10 }));

queryOptions()가 반환하는 객체는 TanStack Query 표준 QueryOptions 타입과 완전히 호환됩니다. 덕분에 캐싱, 무효화, 낙관적 업데이트, prefetchQuery 등 TanStack Query의 모든 기능을 별도 추상화 없이 그대로 쓸 수 있습니다.

패키지명도 변경됐습니다.

bash
# 기존 패키지 제거 후 신규 패키지 설치
npm uninstall @trpc/react-query
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query zod

@trpc/react-query를 제거하지 않은 채 @trpc/tanstack-react-query를 함께 설치하면 두 패키지가 충돌할 수 있습니다. package.json에서 구 패키지를 명시적으로 제거하는 것이 좋습니다.


서버 스트리밍 두 가지 축

tRPC v11은 두 종류의 스트리밍 링크를 제공합니다.

다이어그램 1

  • httpBatchStreamLink: 여러 쿼리를 하나의 배치로 묶되, 각 쿼리가 완료되는 순서대로 클라이언트에 전달합니다. 느린 쿼리가 빠른 응답을 블로킹하지 않습니다.
  • httpSubscriptionLink: SSE(Server-Sent Events) 기반 구독 링크입니다. SSE는 HTTP/1.1에서도 동작하며, HTTP/2는 브라우저당 동시 연결 제한(≈6개)을 다중화로 우회하는 부가적 이점을 줄 뿐입니다.

실제 클라이언트 설정에서는 splitLink로 구독과 일반 쿼리를 분기합니다. splitLink의 condition 콜백에서 op.type이 가질 수 있는 값은 'query' | 'mutation' | 'subscription' 세 가지입니다.

typescript
// lib/trpc/client.ts
import {
  createTRPCClient,
  httpBatchStreamLink,
  httpSubscriptionLink,
  splitLink,
} from '@trpc/client';
import type { AppRouter } from '@/server/routers';
 
export const trpcClient = createTRPCClient<AppRouter>({
  links: [
    splitLink({
      // op.type은 'query' | 'mutation' | 'subscription'
      condition: (op) => op.type === 'subscription',
      true: httpSubscriptionLink({ url: '/api/trpc' }),
      false: httpBatchStreamLink({ url: '/api/trpc' }),
    }),
  ],
});

실전 적용

공유 QueryClient 팩토리 — hydration의 핵심

RSC 프리패칭이 클라이언트 캐시로 이어지려면 서버와 클라이언트가 동일한 팩토리 함수로 생성된 QueryClient를 사용해야 합니다. 이 파일을 먼저 만들고 이후 모든 설정이 이를 참조하게 합니다.

typescript
// lib/trpc/query-client.ts
import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query';
 
export function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 30 * 1000,
      },
      dehydrate: {
        // pending 상태의 쿼리도 직렬화해 Suspense 경계와 호환
        shouldDehydrateQuery: (query) =>
          defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
      },
    },
  });
}

서버 전용 유틸리티 — HydrateClient와 createTRPCOptionsProxy

createTRPCOptionsProxy는 @trpc/tanstack-react-query에서 가져오는 서버 전용 함수입니다. HTTP를 거치지 않고 appRouter의 Procedure를 직접 호출해 QueryClient 캐시를 채웁니다. HydrateClient는 이 캐시를 직렬화해 클라이언트로 전달하는 서버 컴포넌트입니다.

typescript
// lib/trpc/server.ts
import { cache } from 'react';
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { makeQueryClient } from './query-client';
import { appRouter } from '@/server/routers';
import { createTRPCContext } from '@/server/trpc';
 
// 요청당 1회만 생성
export const getQueryClient = cache(makeQueryClient);
 
export function HydrateClient({ children }: { children: React.ReactNode }) {
  return (
    <HydrationBoundary state={dehydrate(getQueryClient())}>
      {children}
    </HydrationBoundary>
  );
}
 
export async function createServerTRPC() {
  const ctx = await createTRPCContext();
  const queryClient = getQueryClient();
  return createTRPCOptionsProxy({ ctx, router: appRouter, queryClient });
}

App Router 라우트 핸들러

typescript
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers';
import { createTRPCContext } from '@/server/trpc';
 
const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: createTRPCContext,
  });
 
export { handler as GET, handler as POST };

TanStack Query Provider

클라이언트 컴포넌트 트리의 최상단에서 QueryClientProvider와 TRPCProvider를 구성합니다. useTRPC는 Provider 컨텍스트에 의존하는 클라이언트 전용 훅이므로, 이 파일을 import하는 컴포넌트 역시 클라이언트 컴포넌트여야 합니다.

typescript
// components/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createTRPCReactContext } from '@trpc/tanstack-react-query';
import { trpcClient } from '@/lib/trpc/client';
import { makeQueryClient } from '@/lib/trpc/query-client';
import type { AppRouter } from '@/server/routers';
 
export const { TRPCProvider, useTRPC } = createTRPCReactContext<AppRouter>();
 
let browserQueryClient: QueryClient | undefined;
 
function getQueryClient() {
  if (typeof window === 'undefined') {
    // SSR: 매번 새로 생성
    return makeQueryClient();
  }
  // 브라우저: 싱글톤 유지 (React Suspense 재렌더 시에도 동일 인스턴스 사용)
  return (browserQueryClient ??= makeQueryClient());
}
 
export function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient();
  return (
    <QueryClientProvider client={queryClient}>
      <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
        {children}
      </TRPCProvider>
    </QueryClientProvider>
  );
}

RSC 프리패칭 — 클라이언트 워터폴 제거

App Router의 서버 컴포넌트에서 createServerTRPC()로 데이터를 미리 가져오면, HydrateClient가 캐시를 직렬화해 HTML과 함께 전달합니다. 클라이언트 컴포넌트가 마운트됐을 때 동일한 queryOptions로 조회하면 캐시 히트가 발생해 네트워크 요청이 발생하지 않습니다.

다이어그램 2

typescript
// app/posts/page.tsx — Server Component
import { createServerTRPC, HydrateClient } from '@/lib/trpc/server';
import PostList from './post-list';
 
export default async function PostsPage() {
  const trpc = await createServerTRPC();
  await trpc.post.list.prefetchQuery({ limit: 20 });
 
  return (
    <HydrateClient>
      <PostList />
    </HydrateClient>
  );
}
typescript
// app/posts/post-list.tsx — Client Component
// useTRPC는 Provider 컨텍스트에 의존하므로 반드시 클라이언트 컴포넌트에서 사용
'use client';
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '@/components/providers';
 
export default function PostList() {
  const trpc = useTRPC();
  const { data, isLoading } = useQuery(
    trpc.post.list.queryOptions({ limit: 20 })
  );
 
  if (isLoading) return <div>로딩 중...</div>;
 
  return (
    <ul>
      {data?.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Mutation — 입력 유효성 검사와 캐시 무효화

typescript
// app/posts/create-post-form.tsx
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '@/components/providers';
 
export default function CreatePostForm() {
  const trpc = useTRPC();
  const queryClient = useQueryClient();
 
  const createPost = useMutation(
    trpc.post.create.mutationOptions({
      onSuccess: () => {
        queryClient.invalidateQueries({
          queryKey: trpc.post.list.queryKey(),
        });
      },
    })
  );
 
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    createPost.mutate({
      title: formData.get('title') as string,
      content: formData.get('content') as string,
    });
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input name="title" placeholder="제목" required />
      <textarea name="content" placeholder="내용" />
      <button type="submit" disabled={createPost.isPending}>
        {createPost.isPending ? '저장 중...' : '게시'}
      </button>
      {createPost.isError && (
        {/* 프로덕션에서는 error.message를 그대로 노출하지 않고 사용자 친화적 메시지로 변환 권장 */}
        <p>오류가 발생했습니다. 잠시 후 다시 시도해 주세요.</p>
      )}
    </form>
  );
}

Zod 스키마에서 벗어난 값이 전달되면 서버가 BAD_REQUEST로 응답하고, 클라이언트의 createPost.error에 타입 안전하게 담깁니다. error.message는 서버 내부 오류 메시지를 그대로 담고 있을 수 있으므로, 사용자에게 보여줄 때는 별도의 메시지로 변환하는 것이 좋습니다.


SSE 구독 — 실시간 이벤트 피드

서버 쪽 subscription Procedure는 async function* 제너레이터로 이벤트를 yield합니다. while(true) + setTimeout 폴링 패턴은 SSE가 아닙니다. 실제 이벤트 기반 구독은 EventEmitter(또는 Redis pub/sub, DB change stream)에서 yield하고, 클라이언트 연결 해제 시 signal로 루프를 종료해야 합니다.

typescript
// server/routers/notification.ts
import { on, EventEmitter } from 'events';
import { router, protectedProcedure } from '../trpc';
 
// 실제 환경에서는 Redis pub/sub 또는 DB change stream 사용 권장
export const notificationEmitter = new EventEmitter();
 
export const notificationRouter = router({
  feed: protectedProcedure.subscription(async function* ({ ctx, signal }) {
    const userId = ctx.session.user.id;
 
    // 연결 시 미읽은 알림 즉시 전달
    const pending = await ctx.db.notification.findMany({
      where: { userId, read: false },
      orderBy: { createdAt: 'desc' },
      take: 5,
    });
    if (pending.length > 0) yield pending;
 
    // signal이 abort될 때(클라이언트 연결 해제)까지 이벤트 대기
    for await (const [notification] of on(
      notificationEmitter,
      `notification:${userId}`,
      { signal }
    )) {
      yield [notification];
    }
  }),
});
typescript
// components/notification-bell.tsx
'use client';
import { useSubscription } from '@trpc/tanstack-react-query';
import { useTRPC } from '@/components/providers';
 
export default function NotificationBell() {
  const trpc = useTRPC();
 
  const { data: notifications } = useSubscription(
    trpc.notification.feed.subscriptionOptions(undefined, {
      onData: (data) => console.log('새 알림:', data),
    })
  );
 
  return <span>알림 {notifications?.length ?? 0}개</span>;
}

다이어그램 3

SSE는 HTTP/1.1에서도 동작합니다. HTTP/2는 브라우저당 동시 연결 제한(≈6개)을 다중화로 우회하는 부가적 이점을 줄 뿐, SSE의 전제 조건이 아닙니다. 클라이언트 → 서버 방향 메시지가 필요 없는 알림, 실시간 대시보드, 주문 상태 업데이트 시나리오에서 WebSocket보다 설정이 단순해 실용적인 선택입니다.

단, Vercel 등 서버리스/엣지 함수 환경은 실행 시간 상한이 있어 장시간 SSE 연결을 유지할 수 없습니다. 자체 호스팅 서버 또는 Vercel의 Fluid compute 환경에서 사용하거나, 강한 실시간 요구사항이 있다면 별도 WebSocket 서버를 함께 고려해야 합니다.


장단점 분석

장점

항목 내용
코드 생성 불필요 GraphQL codegen 없이 TypeScript 추론만으로 end-to-end 타입 안전성 확보
React Query 네이티브 통합 queryOptions() 반환값을 useQuery에 직접 전달, TanStack Query 기능 전체 활용 가능
보일러플레이트 감소 API 라우트 파일, 타입 정의, fetch 래퍼를 별도로 작성할 필요 없음
RSC → 클라이언트 캐시 연속성 HydrateClient를 통해 서버 프리패치 데이터가 클라이언트 캐시에 그대로 전달
스트리밍 우선 아키텍처 httpBatchStreamLink로 느린 쿼리가 빠른 응답을 블로킹하지 않음
미들웨어 기반 인증/권한 protectedProcedure 패턴으로 인증 로직을 절차 단위로 재사용

단점 및 제약

항목 내용
TypeScript 전용 클라이언트·서버 모두 TypeScript 필수. Python, Go, Swift 등 다른 언어 클라이언트 불가
모노레포 선호 구조 AppRouter 타입 임포트를 위해 클라이언트·서버가 같은 저장소에 있어야 이점이 극대화됨
공개 API에 부적합 외부 서드파티가 소비하는 공개 API는 REST / OpenAPI가 적합
v10 → v11 마이그레이션 비용 패키지명 변경, 훅 사용 방식 변경, App Router 설정 구조 변경으로 수정 범위가 넓음
깊은 중첩 라우터의 TS 추론 지연 라우터·미들웨어가 깊어질수록 TypeScript LSP 응답이 느려질 수 있음
서버리스 환경의 SSE 제약 실행 시간 상한이 있는 서버리스 환경에서는 장시간 구독 유지 불가

실무에서 흔한 실수

1. Context 생성 시 cache() 누락

typescript
// 잘못된 예 — 요청마다 Context가 중복 생성될 수 있음
export const createTRPCContext = async () => { /* ... */ };
 
// 올바른 예 — 요청당 1회 보장
export const createTRPCContext = cache(async () => { /* ... */ });

2. 서버와 클라이언트 QueryClient 팩토리 불일치

providers.tsx에서 new QueryClient()를 직접 호출하면 서버 프리패치 데이터가 클라이언트 캐시로 전달되지 않습니다. 공유 makeQueryClient() 팩토리를 만들고 양쪽에서 참조해야 합니다.

3. v10 스타일 훅을 v11에서 그대로 사용

typescript
// v11에서 동작하지 않는 v10 패턴
const { data } = trpc.post.list.useQuery({ limit: 10 }); // ❌
 
// v11 올바른 패턴
const trpc = useTRPC();
const { data } = useQuery(trpc.post.list.queryOptions({ limit: 10 })); // ✅

4. HydrateClient 없이 prefetchQuery 호출

RSC에서 prefetchQuery를 호출했더라도 HydrateClient로 감싸지 않으면 캐시가 클라이언트로 전달되지 않아 동일 요청이 재발생합니다.

5. 구독 프로시저에서 signal 처리 누락

클라이언트가 연결을 끊었을 때 서버 루프가 종료되지 않으면 리소스 누수가 발생합니다. async function* ({ ctx, signal })에서 signal을 받아 for await ... of on(emitter, event, { signal }) 패턴으로 루프를 종료해야 합니다.


언제 tRPC 대신 다른 선택지를 고려할지

상황 권장 대안
외부 서드파티에 API를 노출해야 할 때 REST + OpenAPI, oRPC
기존 Express/Fastify 코드베이스를 유지하면서 타입 안전성만 더하고 싶을 때 ts-rest
Cloudflare Workers 같은 엣지 런타임 특화 경량 솔루션 Hono RPC
클라이언트가 TypeScript가 아닐 때 REST / GraphQL

마치며

tRPC v11이 이전 버전과 가장 크게 달라진 점은 TanStack Query와의 관계 재정립입니다. tRPC가 React Query를 추상화하는 게 아니라, tRPC가 TanStack Query의 표준 인터페이스인 queryOptions / mutationOptions를 생성해주는 역할로 물러납니다. TanStack Query의 모든 기능을 그대로 활용하면서, 네트워크 레이어의 타입 안전성은 tRPC가 담당하는 명확한 역할 분리입니다.

App Router 환경에서의 핵심 흐름은 세 단계입니다. createServerTRPC()로 서버에서 데이터를 프리패칭하고, HydrateClient로 캐시를 직렬화해 전달하고, 클라이언트 컴포넌트에서 useTRPC() + useQuery()로 캐시를 소비합니다. 이 흐름이 자리잡으면 REST API 라우트 파일, 타입 정의 파일, fetch 래퍼를 따로 관리할 필요가 없어집니다.

실제 프로젝트 구조를 빠르게 확인하고 싶다면 create-t3-app 스타터와 tRPC 공식 예제 레포가 좋은 참고점입니다. 공식 예제는 App Router + tRPC v11 + Prisma 조합의 실제 파일 구조를 그대로 확인할 수 있습니다.


참고 자료

  • Announcing tRPC v11 — tRPC 공식 블로그
  • Set up with Next.js App Router — tRPC 공식 문서
  • Set up with React Server Components — tRPC 공식 문서
  • Introducing the new TanStack React Query integration — tRPC 블로그
  • HTTP Batch Stream Link — tRPC 공식 문서
  • Subscriptions (SSE) — tRPC 공식 문서
  • Migrate from v10 to v11 — tRPC 공식 문서
  • tRPC 11 Setup for Next.js App Router 2025 — DEV Community
  • Mastering tRPC with React Server Components: The Definitive 2026 Guide — DEV Community
  • tRPC v11: What's New and Should You Upgrade? — PkgPulse Blog
  • tRPC v11 vs oRPC vs ts-rest: Type-Safe RPC for SaaS Boilerplates 2026 — StarterPick
  • When to Choose REST Over tRPC — Wisp CMS
  • Building Production-Ready tRPC APIs — InfoQ
  • tRPC GitHub Releases
#tRPC#Next.js#TypeScript#TanStack Query#App Router#End-to-End Type Safety
공유하기

목차

핵심 개념Procedure와 Router — 서버 함수의 단위Context — 요청별 데이터 주입v11의 React Query 통합 방식 변화서버 스트리밍 두 가지 축실전 적용공유 QueryClient 팩토리 — hydration의 핵심서버 전용 유틸리티 — HydrateClient와 createTRPCOptionsProxyApp Router 라우트 핸들러TanStack Query ProviderRSC 프리패칭 — 클라이언트 워터폴 제거Mutation — 입력 유효성 검사와 캐시 무효화SSE 구독 — 실시간 이벤트 피드장단점 분석장점단점 및 제약실무에서 흔한 실수언제 tRPC 대신 다른 선택지를 고려할지마치며참고 자료

추천 포스트

Cloudflare Durable Objects + Hono로 실시간 협업 기능 구현하기
Backend

Cloudflare Durable Objects + Hono로 실시간 협업 기능 구현하기

WebSocket은 연결이 유지되어야 한다. Worker는 요청이 끝나면 메모리가 사라진다. 이 두 조건이 충돌하는 지점에서 Durable Objects가 등장한다. 각 인스턴스가 전 세계에서 단 하나만 실행되며, 지속적인 인메모리 상태와 내장 SQLite 스토리지를…

2026년 07월 14일읽는 데 17분
Bun 2.0 완전 가이드 — 내장 SQLite·S3 클라이언트·HTML 번들러로 외부 패키지 없이 프로덕션 풀스택 서버 구축하기
Backend

Bun 2.0 완전 가이드 — 내장 SQLite·S3 클라이언트·HTML 번들러로 외부 패키지 없이 프로덕션 풀스택 서버 구축하기

Node.js 프로젝트에서 package.json 을 열어보면 항상 비슷한 패턴이 보입니다. better sqlite3 , @aws sdk/client s3 , webpack 혹은 vite , jest 또는 vitest ... 이런 의존성 하나하나가 쌓일수록 node…

2026년 07월 14일읽는 데 20분
Deno 2 실전 가이드 — Node.js 호환 모드·내장 KV·Queues로 외부 인프라 없이 서버리스 백엔드 구축하기
Backend

Deno 2 실전 가이드 — Node.js 호환 모드·내장 KV·Queues로 외부 인프라 없이 서버리스 백엔드 구축하기

Deno 2가 2024년 10월에 공개됐을 때 백엔드 개발자들이 가장 먼저 던진 질문은 하나였습니다. 기존 npm 생태계와 얼마나 자연스럽게 공존하는가. 이전 버전의 Deno는 "npm이 안 된다"는 이유 하나로 실무 후보에서 곧바로 제외됐으니까요. 이 글은 공식 문서…

2026년 07월 13일읽는 데 23분
Temporal 클러스터 없이 엣지에서 내결함성 워크플로를 — Cloudflare Workflows의 재시도·인간 승인·타임아웃 처리 방식
Backend

Temporal 클러스터 없이 엣지에서 내결함성 워크플로를 — Cloudflare Workflows의 재시도·인간 승인·타임아웃 처리 방식

서버리스 환경에서 장기 실행 작업을 다루다 보면 공통된 문제를 마주칩니다. 요청이 실패했을 때 처음부터 다시 시작해야 할까, 아니면 중단된 지점부터 재개할 수 있을까. 내구성 있는 실행(Durable Execution) 이라는 개념이 이 고민의 답인데, 전통적으로는 T…

2026년 07월 12일읽는 데 21분
Node.js 네이티브 TypeScript 실행 완전 가이드
Backend

Node.js 네이티브 TypeScript 실행 완전 가이드

기준 버전: Node.js 24 LTS. Node.js 26에서 달라지는 동작(enum 처리 변화 등)은 본문 중 별도 구획에서 다룹니다. node app.ts 가 그냥 된다는 게 어느 시점부터 사실이 되었습니다. 빌드 없이, 추가 도구 없이, 플래그 없이. Node.…

2026년 07월 12일읽는 데 19분
BullMQ로 구축하는 프로덕션 작업 큐 — 우선순위, 재시도 백오프, Dead Letter Queue를 TypeScript로 구현하기
Backend

BullMQ로 구축하는 프로덕션 작업 큐 — 우선순위, 재시도 백오프, Dead Letter Queue를 TypeScript로 구현하기

API 핸들러가 이메일 발송이나 결제 요청을 직접 처리하면, 외부 서비스 응답 지연이 곧 HTTP 타임아웃으로 이어집니다. 비동기 작업 큐는 이 결합을 끊습니다. API는 잡을 큐에 넣는 즉시 응답하고, 실제 처리는 백그라운드 워커가 맡습니다. 그런데 단순히 "큐에 넣…

2026년 07월 12일읽는 데 18분