본문 바로가기
버전: 11.x

HTTP 구독 링크

비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

httpSubscriptionLink서버 전송 이벤트(SSE)를 구독에 사용하는 종단 링크입니다.

SSE는 웹소켓 서버 설정보다 간편하므로 실시간 통신에 유용한 옵션입니다.

설정

정보

클라이언트 환경에서 EventSource를 지원하지 않는 경우 EventSource 폴리필이 필요합니다. React Native 관련 지침은 호환성 섹션을 참조하세요.

httpSubscriptionLink를 사용하려면 SSE를 구독에 사용하겠다는 의도를 명시적으로 전달하기 위해 splitLink를 사용해야 합니다.

client/index.ts
ts
import type { TRPCLink } from '@trpc/client';
import {
httpBatchLink,
httpSubscriptionLink,
loggerLink,
splitLink,
} from '@trpc/client';
const trpcClient = createTRPCClient<AppRouter>({
/**
* @see https://trpc.io/docs/v11/client/links
*/
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink(),
splitLink({
// uses the httpSubscriptionLink for subscriptions
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: `/api/trpc`,
}),
false: httpBatchLink({
url: `/api/trpc`,
}),
}),
],
});
client/index.ts
ts
import type { TRPCLink } from '@trpc/client';
import {
httpBatchLink,
httpSubscriptionLink,
loggerLink,
splitLink,
} from '@trpc/client';
const trpcClient = createTRPCClient<AppRouter>({
/**
* @see https://trpc.io/docs/v11/client/links
*/
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink(),
splitLink({
// uses the httpSubscriptionLink for subscriptions
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: `/api/trpc`,
}),
false: httpBatchLink({
url: `/api/trpc`,
}),
}),
],
});

이 문서는 httpSubscriptionLink의 구체적인 사용법을 설명합니다. 일반적인 구독 사용법은 구독 가이드를 참조하세요.

헤더 및 인증/인가

웹 애플리케이션

동일 도메인

웹 애플리케이션에서 클라이언트와 서버가 동일 도메인에 위치한 경우, 요청에 쿠키가 자동으로 포함됩니다.

크로스 도메인

클라이언트와 서버가 서로 다른 도메인에 있는 경우 withCredentials: true를 사용할 수 있습니다(MDN 문서 참조).

예시:

tsx
// [...]
httpSubscriptionLink({
url: 'https://example.com/api/trpc',
eventSourceOptions() {
return {
withCredentials: true, // <---
};
},
});
tsx
// [...]
httpSubscriptionLink({
url: 'https://example.com/api/trpc',
eventSourceOptions() {
return {
withCredentials: true, // <---
};
},
});

폴리필을 통한 커스텀 헤더

비웹 환경에서 권장

EventSource를 폴리필하고 eventSourceOptions 콜백을 통해 헤더를 설정할 수 있습니다.

tsx
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
import { EventSourcePolyfill } from 'event-source-polyfill';
import type { AppRouter } from '../server/index.js';
// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: 'http://localhost:3000',
// ponyfill EventSource
EventSource: EventSourcePolyfill,
// options to pass to the EventSourcePolyfill constructor
eventSourceOptions: async ({ op }) => {
// ^ Includes the operation that's being executed
// you can use this to generate a signature for the operation
const signature = await getSignature(op);
return {
headers: {
authorization: 'Bearer supersecret',
'x-signature': signature,
},
};
},
}),
false: httpBatchLink({
url: 'http://localhost:3000',
}),
}),
],
});
tsx
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
import { EventSourcePolyfill } from 'event-source-polyfill';
import type { AppRouter } from '../server/index.js';
// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: 'http://localhost:3000',
// ponyfill EventSource
EventSource: EventSourcePolyfill,
// options to pass to the EventSourcePolyfill constructor
eventSourceOptions: async ({ op }) => {
// ^ Includes the operation that's being executed
// you can use this to generate a signature for the operation
const signature = await getSignature(op);
return {
headers: {
authorization: 'Bearer supersecret',
'x-signature': signature,
},
};
},
}),
false: httpBatchLink({
url: 'http://localhost:3000',
}),
}),
],
});

활성 연결에서 설정 업데이트

httpSubscriptionLink는 SSE를 EventSource로 구현하여 네트워크 장애나 잘못된 응답 코드 같은 오류 발생 시 자동 재연결을 보장합니다. 그러나 EventSource는 인증이 만료되는 시나리오에서 특히 중요한 eventSourceOptions()url() 옵션을 재실행하여 설정을 업데이트하는 기능을 지원하지 않습니다.

이 제한을 해결하기 위해 httpSubscriptionLink와 함께 retryLink를 사용할 수 있습니다. 이 접근 방식은 업데이트된 인증 정보를 포함한 최신 설정으로 연결을 재설정합니다.

주의

연결을 재시작하면 EventSource가 처음부터 다시 생성되므로, 이전에 추적된 모든 이벤트가 손실됩니다.

tsx
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
retryLink,
splitLink,
} from '@trpc/client';
import {
EventSourcePolyfill,
EventSourcePolyfillInit,
} from 'event-source-polyfill';
import type { AppRouter } from '../server/index.js';
// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
false: httpBatchLink({
url: 'http://localhost:3000',
}),
true: [
retryLink({
retry: (opts) => {
opts.op.type;
// ^? will always be 'subscription' since we're in a splitLink
const code = opts.error.data?.code;
if (!code) {
// This shouldn't happen as our httpSubscriptionLink will automatically retry within when there's a non-parsable response
console.error('No error code found, retrying', opts);
return true;
}
if (code === 'UNAUTHORIZED' || code === 'FORBIDDEN') {
console.log('Retrying due to 401/403 error');
return true;
}
return false;
},
}),
httpSubscriptionLink({
url: async () => {
// calculate the latest URL if needed...
return getAuthenticatedUri();
},
// ponyfill EventSource
EventSource: EventSourcePolyfill,
eventSourceOptions: async () => {
// ...or maybe renew an access token
const token = await auth.getOrRenewToken();
return {
headers: {
authorization: `Bearer ${token}`,
},
};
},
}),
],
}),
],
});
tsx
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
retryLink,
splitLink,
} from '@trpc/client';
import {
EventSourcePolyfill,
EventSourcePolyfillInit,
} from 'event-source-polyfill';
import type { AppRouter } from '../server/index.js';
// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
false: httpBatchLink({
url: 'http://localhost:3000',
}),
true: [
retryLink({
retry: (opts) => {
opts.op.type;
// ^? will always be 'subscription' since we're in a splitLink
const code = opts.error.data?.code;
if (!code) {
// This shouldn't happen as our httpSubscriptionLink will automatically retry within when there's a non-parsable response
console.error('No error code found, retrying', opts);
return true;
}
if (code === 'UNAUTHORIZED' || code === 'FORBIDDEN') {
console.log('Retrying due to 401/403 error');
return true;
}
return false;
},
}),
httpSubscriptionLink({
url: async () => {
// calculate the latest URL if needed...
return getAuthenticatedUri();
},
// ponyfill EventSource
EventSource: EventSourcePolyfill,
eventSourceOptions: async () => {
// ...or maybe renew an access token
const token = await auth.getOrRenewToken();
return {
headers: {
authorization: `Bearer ${token}`,
},
};
},
}),
],
}),
],
});

연결 매개변수

EventSource 인증을 위해 httpSubscriptionLink에서 connectionParams를 정의할 수 있습니다(URL 일부로 전송되므로 다른 방법이 선호됨).

server/context.ts
ts
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
 
export const createContext = async (opts: CreateHTTPContextOptions) => {
const token = opts.info.connectionParams?.token;
const token: string | undefined
 
// [... authenticate]
 
return {};
};
 
export type Context = Awaited<ReturnType<typeof createContext>>;
server/context.ts
ts
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
 
export const createContext = async (opts: CreateHTTPContextOptions) => {
const token = opts.info.connectionParams?.token;
const token: string | undefined
 
// [... authenticate]
 
return {};
};
 
export type Context = Awaited<ReturnType<typeof createContext>>;
client/trpc.ts
ts
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
import type { AppRouter } from '../server/index.js';
// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: 'http://localhost:3000',
connectionParams: async () => {
// Will be serialized as part of the URL
return {
token: 'supersecret',
};
},
}),
false: httpBatchLink({
url: 'http://localhost:3000',
}),
}),
],
});
client/trpc.ts
ts
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
import type { AppRouter } from '../server/index.js';
// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: 'http://localhost:3000',
connectionParams: async () => {
// Will be serialized as part of the URL
return {
token: 'supersecret',
};
},
}),
false: httpBatchLink({
url: 'http://localhost:3000',
}),
}),
],
});

타임아웃 설정

httpSubscriptionLinkreconnectAfterInactivityMs 옵션을 통해 비활성 상태 타임아웃을 지원합니다. 지정된 시간 동안 메시지(핑 메시지 포함)를 수신하지 못하면 연결이 "연결 중" 상태로 표시되고 자동으로 재연결을 시도합니다.

타임아웃 설정은 서버 측에서 tRPC 초기화 시 구성됩니다:

server/trpc.ts
ts
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create({
sse: {
client: {
reconnectAfterInactivityMs: 3_000,
},
},
});
server/trpc.ts
ts
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create({
sse: {
client: {
reconnectAfterInactivityMs: 3_000,
},
},
});

서버 핑 설정

연결 유지 및 타임아웃 끊김 방지를 위해 서버에 주기적 핑 메시지 전송을 설정할 수 있습니다. 이 기능은 reconnectAfterInactivityMs 옵션과 함께 사용할 때 특히 유용합니다.

server/trpc.ts
ts
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create({
sse: {
// Maximum duration of a single SSE connection in milliseconds
// maxDurationMs: 60_00,
ping: {
// Enable periodic ping messages to keep connection alive
enabled: true,
// Send ping message every 2s
intervalMs: 2_000,
},
// client: {
// reconnectAfterInactivityMs: 3_000
// }
},
});
server/trpc.ts
ts
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create({
sse: {
// Maximum duration of a single SSE connection in milliseconds
// maxDurationMs: 60_00,
ping: {
// Enable periodic ping messages to keep connection alive
enabled: true,
// Send ping message every 2s
intervalMs: 2_000,
},
// client: {
// reconnectAfterInactivityMs: 3_000
// }
},
});

호환성 (React Native)

httpSubscriptionLinkEventSource API, Streams API, AsyncIterator를 사용하는데, React Native에서는 기본적으로 지원되지 않으므로 폴리필을 적용해야 합니다.

EventSource를 폴리필하려면 XMLHttpRequest API를 사용하는 폴리필보다 React Native가 노출한 네트워킹 라이브러리를 활용하는 것을 권장합니다. XMLHttpRequest를 사용하여 EventSource를 폴리필하는 라이브러리들은 앱이 백그라운드 상태였다가 복귀한 후 재연결에 실패합니다. rn-eventsource-reborn 패키지 사용을 고려하세요.

Streams API는 web-streams-polyfill 패키지로 폴리필할 수 있습니다.

AsyncIterator@azure/core-asynciterator-polyfill 패키지로 폴리필할 수 있습니다.

설치

필요한 폴리필을 설치하세요:

npm install rn-eventsource-reborn web-streams-polyfill @azure/core-asynciterator-polyfill

링크 사용 전(예: TRPCReact.Provider를 추가하는 위치)에 프로젝트에 폴리필을 추가하세요:

utils/api.tsx
ts
import '@azure/core-asynciterator-polyfill';
import { RNEventSource } from 'rn-eventsource-reborn';
import { ReadableStream, TransformStream } from 'web-streams-polyfill';
globalThis.ReadableStream = globalThis.ReadableStream || ReadableStream;
globalThis.TransformStream = globalThis.TransformStream || TransformStream;
utils/api.tsx
ts
import '@azure/core-asynciterator-polyfill';
import { RNEventSource } from 'rn-eventsource-reborn';
import { ReadableStream, TransformStream } from 'web-streams-polyfill';
globalThis.ReadableStream = globalThis.ReadableStream || ReadableStream;
globalThis.TransformStream = globalThis.TransformStream || TransformStream;

폴리필 추가 후에는 설정 섹션에 설명된 대로 httpSubscriptionLink 설정을 계속 진행할 수 있습니다.

ts
type HTTPSubscriptionLinkOptions<
TRoot extends AnyClientTypes,
TEventSource extends EventSourceLike.AnyConstructor = typeof EventSource,
> = {
/**
* EventSource ponyfill
*/
EventSource?: TEventSource;
/**
* EventSource options or a callback that returns them
*/
eventSourceOptions?:
| EventSourceLike.InitDictOf<TEventSource>
| ((opts: {
op: Operation;
}) =>
| EventSourceLike.InitDictOf<TEventSource>
| Promise<EventSourceLike.InitDictOf<TEventSource>>);
};
ts
type HTTPSubscriptionLinkOptions<
TRoot extends AnyClientTypes,
TEventSource extends EventSourceLike.AnyConstructor = typeof EventSource,
> = {
/**
* EventSource ponyfill
*/
EventSource?: TEventSource;
/**
* EventSource options or a callback that returns them
*/
eventSourceOptions?:
| EventSourceLike.InitDictOf<TEventSource>
| ((opts: {
op: Operation;
}) =>
| EventSourceLike.InitDictOf<TEventSource>
| Promise<EventSourceLike.InitDictOf<TEventSource>>);
};

서버 측 SSE 옵션

ts
export interface SSEStreamProducerOptions<TValue = unknown> {
ping?: {
/**
* Enable ping comments sent from the server
* @default false
*/
enabled: boolean;
/**
* Interval in milliseconds
* @default 1000
*/
intervalMs?: number;
};
/**
* Maximum duration in milliseconds for the request before ending the stream
* @default undefined
*/
maxDurationMs?: number;
/**
* End the request immediately after data is sent
* Only useful for serverless runtimes that do not support streaming responses
* @default false
*/
emitAndEndImmediately?: boolean;
/**
* Client-specific options - these will be sent to the client as part of the first message
* @default {}
*/
client?: {
/**
* Timeout and reconnect after inactivity in milliseconds
* @default undefined
*/
reconnectAfterInactivityMs?: number;
};
}
ts
export interface SSEStreamProducerOptions<TValue = unknown> {
ping?: {
/**
* Enable ping comments sent from the server
* @default false
*/
enabled: boolean;
/**
* Interval in milliseconds
* @default 1000
*/
intervalMs?: number;
};
/**
* Maximum duration in milliseconds for the request before ending the stream
* @default undefined
*/
maxDurationMs?: number;
/**
* End the request immediately after data is sent
* Only useful for serverless runtimes that do not support streaming responses
* @default false
*/
emitAndEndImmediately?: boolean;
/**
* Client-specific options - these will be sent to the client as part of the first message
* @default {}
*/
client?: {
/**
* Timeout and reconnect after inactivity in milliseconds
* @default undefined
*/
reconnectAfterInactivityMs?: number;
};
}