0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · 강의

서버리스 환경에서 WebSocket 서비스 통합

서버리스 함수는 오래 유지되는 소켓을 보유할 수 없으므로 관리형 WebSocket 제공업체에 연결하는 방법을 배웁니다.

서버리스 환경에서 WebSocket 서비스 통합은(는) CoddyKit의 무료 Next.js 15 Fullstack (App Router + Server Actions) 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Next.js 15 Fullstack (App Router + Server Actions) 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Next.js 15 Fullstack (App Router + Server Actions) 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Serverless and WebSockets Don't Mix

Serverless functions (like Next.js API routes deployed to Vercel, AWS Lambda, or similar platforms) are designed to be stateless and short-lived. Each invocation boots, handles a request, and terminates.

WebSockets, on the other hand, require a persistent, long-lived TCP connection between client and server. This creates a fundamental mismatch:

  • Serverless runtimes have execution time limits (e.g. 10–30 seconds).
  • There is no persistent process to hold an open socket across requests.
  • Horizontal scaling means each new instance has no shared in-memory state.

The solution is to delegate real-time communication to a managed WebSocket provider — a dedicated service that maintains connections on your behalf while your serverless functions focus on business logic.

The Managed WebSocket Provider Pattern

Managed WebSocket providers expose an HTTP API that your serverless functions can call to push messages into persistent connections they maintain. Popular options include:

  • Pusher / Pusher Channels — battle-tested, generous free tier, first-class Next.js support.
  • Ably — low-latency pub/sub with edge-friendly token auth.
  • Soketi — open-source, self-hosted Pusher-compatible server.
  • PartyKit — built specifically for Next.js and Cloudflare Workers.

The typical flow is:

  • Client connects to the provider's WebSocket endpoint and subscribes to a channel.
  • Your Next.js Server Action or Route Handler calls the provider's REST API to publish an event.
  • The provider fans the event out to all subscribed clients instantly.

Your serverless function never touches a socket directly — it simply makes an HTTP POST.

Setting Up Pusher in a Next.js 15 Project

Install both the server-side SDK (used in Server Actions / Route Handlers) and the client-side SDK (used in Client Components):

npm install pusher pusher-js

Add your Pusher credentials to .env.local. Never expose the secret key to the browser — prefix only the public values with NEXT_PUBLIC_:

// .env.local
PUSHER_APP_ID="your_app_id"
PUSHER_KEY="your_key"
PUSHER_SECRET="your_secret"
PUSHER_CLUSTER="eu"

NEXT_PUBLIC_PUSHER_KEY="your_key"
NEXT_PUBLIC_PUSHER_CLUSTER="eu"

Creating a Singleton Pusher Server Instance

To avoid creating a new Pusher instance on every server-side call, export a singleton from a utility module. In Next.js 15 App Router the module is evaluated once per worker process (or edge invocation), so a module-level constant works perfectly.

This singleton is server-only — it imports the Node.js pusher package which must never reach the browser bundle. Mark the file accordingly with the Next.js convention.

// lib/pusher-server.ts
import Pusher from 'pusher';

// 'server-only' prevents accidental client-side imports
import 'server-only';

if (
  !process.env.PUSHER_APP_ID ||
  !process.env.PUSHER_KEY ||
  !process.env.PUSHER_SECRET ||
  !process.env.PUSHER_CLUSTER
) {
  throw new Error('Missing Pusher environment variables');
}

export const pusherServer = new Pusher({
  appId: process.env.PUSHER_APP_ID,
  key: process.env.PUSHER_KEY,
  secret: process.env.PUSHER_SECRET,
  cluster: process.env.PUSHER_CLUSTER,
  useTLS: true,
});

Publishing Events from a Server Action

Server Actions in Next.js 15 are perfect for triggering real-time events: the user submits a form, the action persists data, then immediately publishes to Pusher. All of this happens server-side — the client never sees the secret key.

The pusherServer.trigger(channel, event, data) call makes an HTTPS request to Pusher's API, which is well within serverless execution time limits.

// app/chat/actions.ts
'use server';

import { pusherServer } from '@/lib/pusher-server';

interface ChatMessage {
  id: string;
  user: string;
  text: string;
  sentAt: string;
}

export async function sendMessage(
  channelName: string,
  user: string,
  text: string
): Promise<void> {
  const message: ChatMessage = {
    id: crypto.randomUUID(),
    user,
    text,
    sentAt: new Date().toISOString(),
  };

  // Persist to your database here (e.g. Supabase, Prisma, etc.)
  // await db.messages.create({ data: message });

  // Publish to all subscribers on this channel
  await pusherServer.trigger(channelName, 'new-message', message);
}

Creating the Client-Side Pusher Hook

On the client, create a reusable React hook that wraps pusher-js. The hook should:

  • Initialise the Pusher client once (via useRef or module-level singleton).
  • Subscribe to the requested channel and event on mount.
  • Clean up the subscription on unmount to prevent memory leaks.

Using a module-level singleton for the PusherClient instance avoids reconnecting every time the hook re-renders.

// lib/pusher-client.ts
import PusherClient from 'pusher-js';

export const pusherClient = new PusherClient(
  process.env.NEXT_PUBLIC_PUSHER_KEY!,
  { cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER! }
);

// hooks/usePusherChannel.ts
import { useEffect } from 'react';
import { pusherClient } from '@/lib/pusher-client';

export function usePusherChannel<T>(
  channelName: string,
  eventName: string,
  onEvent: (data: T) => void
): void {
  useEffect(() => {
    const channel = pusherClient.subscribe(channelName);
    channel.bind(eventName, onEvent);

    return () => {
      channel.unbind(eventName, onEvent);
      pusherClient.unsubscribe(channelName);
    };
  }, [channelName, eventName, onEvent]);
}

Building the Real-Time Chat Client Component

Wire the custom hook into a Client Component. The component renders an optimistic list of messages and uses the Server Action to send new ones. Incoming Pusher events are appended to state — no polling required.

Mark the component with 'use client' since it uses browser APIs (useState, useEffect) and the Pusher SDK.

'use client';

import { useState, useCallback, useTransition } from 'react';
import { usePusherChannel } from '@/hooks/usePusherChannel';
import { sendMessage } from './actions';

interface Message {
  id: string;
  user: string;
  text: string;
  sentAt: string;
}

export function ChatRoom({
  channelName,
  currentUser,
}: {
  channelName: string;
  currentUser: string;
}) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleNewMessage = useCallback((msg: Message) => {
    setMessages((prev) => [...prev, msg]);
  }, []);

  usePusherChannel<Message>(channelName, 'new-message', handleNewMessage);

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!input.trim()) return;
    startTransition(() => sendMessage(channelName, currentUser, input));
    setInput('');
  }

  return (
    <div>
      <ul>
        {messages.map((m) => (
          <li key={m.id}><strong>{m.user}:</strong> {m.text}</li>
        ))}
      </ul>
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={(e) => setInput(e.target.value)} />
        <button type="submit" disabled={isPending}>Send</button>
      </form>
    </div>
  );
}

Authenticating Private Channels

Public Pusher channels are open to anyone who knows the channel name. For private rooms or per-user data, use private channels (prefixed with private-). These require a server-side authentication step.

Pusher's client SDK automatically requests a token from an auth endpoint before subscribing. Create this endpoint as a Next.js Route Handler:

// app/api/pusher/auth/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { pusherServer } from '@/lib/pusher-server';
import { auth } from '@/lib/auth'; // your session helper

export async function POST(req: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await req.text();
  const params = new URLSearchParams(body);
  const socketId = params.get('socket_id')!;
  const channelName = params.get('channel_name')!;

  // Optional: verify the user is allowed to join this channel
  const authData = pusherServer.authorizeChannel(socketId, channelName, {
    user_id: session.user.id,
    user_info: { name: session.user.name },
  });

  return NextResponse.json(authData);
}

Configuring the Client for Private Channels

Point the Pusher client SDK at your auth endpoint so it can obtain tokens before subscribing to private- channels. The channelAuthorization option specifies the URL and transport method.

Once configured, subscribing to a private channel looks identical to subscribing to a public one — the SDK handles the auth handshake transparently.

// lib/pusher-client.ts  (updated)
import PusherClient from 'pusher-js';

export const pusherClient = new PusherClient(
  process.env.NEXT_PUBLIC_PUSHER_KEY!,
  {
    cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
    channelAuthorization: {
      endpoint: '/api/pusher/auth',
      transport: 'ajax',
    },
  }
);

// Usage in a component — identical for public or private channels:
// usePusherChannel('private-room-42', 'new-message', handler);

Triggering Events from a Route Handler

Server Actions are ideal for form submissions, but sometimes you need to trigger events from an external webhook — for example, a payment provider notifying your app that a transaction completed. In that case, use a Route Handler instead of a Server Action.

The pattern is the same: validate the incoming request, then call pusherServer.trigger() to fan out the event to connected clients.

// app/api/webhooks/payment/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { pusherServer } from '@/lib/pusher-server';

interface PaymentPayload {
  orderId: string;
  status: 'paid' | 'failed';
  userId: string;
}

export async function POST(req: NextRequest) {
  // Validate webhook signature (provider-specific)
  const signature = req.headers.get('x-webhook-signature');
  if (!isValidSignature(signature, await req.text())) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  const payload: PaymentPayload = await req.json();

  // Notify the specific user on their private channel
  await pusherServer.trigger(
    `private-user-${payload.userId}`,
    'payment-update',
    { orderId: payload.orderId, status: payload.status }
  );

  return NextResponse.json({ received: true });
}

function isValidSignature(sig: string | null, _body: string): boolean {
  // Replace with real HMAC check for your provider
  return sig !== null;
}

Presence Channels: Knowing Who Is Online

Pusher presence channels (prefixed presence-) extend private channels with a member roster. Each authenticated subscriber is added to a shared list that all members can observe in real time.

This enables features like "3 people are viewing this document" or live user cursors. Key events fired by Pusher on the client:

  • pusher:subscription_succeeded — initial member list.
  • pusher:member_added — a new user joined.
  • pusher:member_removed — a user disconnected.

The auth endpoint is reused; the user data you pass in user_info becomes the member object visible to all subscribers — so only include non-sensitive fields like display names or avatars.

Knowledge Check: Triggering Events from Serverless

A Next.js 15 application is deployed to Vercel (serverless). A Server Action must notify all connected clients whenever a user submits a form. Which approach is correct?

Lesson Recap: WebSockets in a Serverless World

Here is what you learned in this lesson:

  • Serverless and long-lived sockets are incompatible — functions terminate too quickly to hold WebSocket connections.
  • Managed providers (Pusher, Ably, PartyKit) maintain persistent connections on your behalf. Your serverless code communicates with them via standard HTTPS calls.
  • Server singleton — export a single Pusher instance from a server-only module to prevent accidental client-side inclusion of your secret key.
  • Server Actions and Route Handlers both call pusherServer.trigger(channel, event, data) to publish events after business logic completes.
  • Private channels require a /api/pusher/auth Route Handler that validates the user session before issuing a subscription token.
  • Presence channels add a live member roster on top of private channel security, enabling online indicators and collaborative features.
  • Client hook — wrap pusher-js in a usePusherChannel hook that subscribes on mount and cleans up on unmount.

This pattern gives you reliable, scalable real-time features without managing any WebSocket infrastructure yourself.

자주 묻는 질문

“서버리스 환경에서 WebSocket 서비스 통합” 강의는 무료인가요?

네 — “서버리스 환경에서 WebSocket 서비스 통합” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Next.js 15 Fullstack (App Router + Server Actions) 강의 전체를 잠금 해제할 수 있습니다. Next.js 15 Fullstack (App Router + Server Actions) 강의에는 총 4개의 강의가 포함되어 있습니다.

“서버리스 환경에서 WebSocket 서비스 통합”에서 뭘 배우나요?

서버리스 함수는 오래 유지되는 소켓을 보유할 수 없으므로 관리형 WebSocket 제공업체에 연결하는 방법을 배웁니다. 브라우저에서 직접 실행하는 실습 코드로 Next.js 15 Fullstack (App Router + Server Actions)을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Next.js 15 Fullstack (App Router + Server Actions)을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Next.js 15 Fullstack (App Router + Server Actions)은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“서버리스 환경에서 WebSocket 서비스 통합” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Next.js 15 Fullstack (App Router + Server Actions) 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Next.js 15 Fullstack (App Router + Server Actions) 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 경로 처리기에서 보내는 서버 이벤트
  2. 서버리스 환경에서 WebSocket 서비스 통합
  3. 토큰 단위로 인공지능 응답 스트리밍하기
  4. 사용자 현황, 커서와 실시간 협업 상태
← Next.js 15 Fullstack (App Router + Server Actions)(으)로 돌아가기