실시간 웹 통신: 긴/짧은 폴링, WebSocket 및 SSE 설명 + Next.js 코드

Linda Hamilton
풀어 주다: 2024-09-23 22:30:32
원래의
639명이 탐색했습니다.

Real-Time Web Communication: Long/Short Polling, WebSockets, and SSE Explained + Next.js code

뒷이야기: 예상치 못한 인터뷰 질문

몇 달 전 저는 중간 프론트엔드 직무에 대한 기술 면접을 진행 중이었습니다. 일이 순조롭게 진행되다가 갑자기 당황스러운 질문을 받게 되었습니다.

"필요한 것을 찾을 때까지 매 순간 확인하는 지속적인 커뮤니케이션 형태가 필요하다고 상상해 보세요.

예를 들어 전자상거래 설정에서와 같이 결제가 성공적으로 이루어졌는지 계속 확인하고 싶습니다. 어떻게 접근하시겠습니까?"

저는 "그 문제를 처리하려면 WebSocket을 구현하면 될 것 같아요."라고 조심스럽게 대답했습니다.

면접관님이 웃으셨습니다. "그것도 좋은 해결책이지만 상황에 따라 더 나은 다른 옵션도 있습니다."

이제 우리는 Long Polling, Short Polling, WebSockets을 포함하여 실시간 통신에 대한 다양한 접근 방식에 대해 대화를 나눴고, 마지막으로 서버 전송 이벤트(SSE)는 결제 예시와 같이 단방향 데이터 스트림에 가장 적합한 선택입니다.

또한 서버 리소스를 소모하지 않고 지속적이면서도 가벼운 요청을 처리하기 위해 올바른 데이터베이스를 선택하는 방법에 대해서도 논의했습니다. 그런 맥락에서 이러한 유형의 요청을 관리하는 데 있어 간편성과 효율성으로 유명한 Redis가 등장했습니다.

이 대화가 마음에 맴돌았습니다. 저는 WebSocket이 많은 관심을 받고 있지만 이해하면 실시간 통신 관리 방식을 최적화할 수 있는 다양한 기술이 있다는 것을 깨달았습니다. 오늘 저는 이 네 가지 접근 방식, 각각의 사용 시기, 장단점을 명확하고 흥미로운 방식으로 분석하고 싶습니다. 결국에는 SSE(Server-Sent Events)가 단방향 실시간 통신에 적합한 이유를 확실하게 이해하게 될 것입니다.

시작하기 전에 채팅을 진행하고 몇 달 후 이 기사를 작성하도록 영감을 준 숙련된 수석 소프트웨어 엔지니어인 Marcos에게 큰 감사를 드립니다. 일자리를 얻지는 못했지만 정말 감사했습니다! :)


실시간 통신의 네 가지 방법

SSE 예시를 시작하기 전에 인터뷰에서 논의한 네 가지 방법을 자세히 살펴보겠습니다.

1. 단기 폴링

단기 폴링은 아마도 가장 간단한 방법일 것입니다. 정기적으로 서버에 "새 데이터가 있습니까?"라고 요청하는 작업이 포함됩니다. 서버는 새로운 내용이 있든 없든 현재 상태로 응답합니다.

장점:

  • 구현 용이
  • 기존 HTTP 요청과 함께 작동

단점:

  • 리소스가 많이 소모됩니다. 새로운 데이터가 없는 경우에도 자주 요청하고 계십니다.
  • 서버 부하와 네트워크 트래픽이 증가할 수 있어 결제 상태 업데이트 등 잦은 확인으로 인해 비효율적이 됩니다.

최적의 용도: 약 1분마다 업데이트되는 주식 시장 가격과 같은 소규모, 빈도가 낮은 데이터 업데이트.

2. 장기 폴링

긴 폴링은 짧은 폴링을 한 단계 더 발전시킵니다. 클라이언트는 서버에 반복적으로 정보를 요청하지만 서버가 즉시 응답하는 대신 새 데이터를 사용할 수 있을 때까지 연결을 유지합니다. 데이터가 다시 전송되면 클라이언트는 즉시 새 연결을 열고 프로세스를 반복합니다.

장점:

  • 서버는 필요할 때만 응답하므로 짧은 폴링보다 효율적입니다. 정말 빠릅니다.
  • 브라우저 및 HTTP/HTTPS 프로토콜과 호환됩니다.

단점:

  • 여전히 연결을 반복적으로 다시 열어야 하므로 시간이 지남에 따라 비효율성이 발생하고 리소스 비용이 많이 듭니다.
  • 짧은 폴링보다 약간 더 복잡합니다.

최적의 용도: 실시간 통신이 필요하지만 WebSocket/SSE가 과도할 수 있는 상황(예: 채팅 애플리케이션).

3. 웹소켓

WebSocket은 클라이언트와 서버 간의 전이중 통신을 제공하는 보다 현대적인 솔루션입니다. 연결이 열리면 양방향 통신을 정의하는 연결을 다시 설정하지 않고도 양측에서 자유롭게 데이터를 보낼 수 있습니다.

장점:

  • 최소한의 지연 시간으로 진정한 실시간 커뮤니케이션을 제공합니다.
  • 양방향 커뮤니케이션(예: 실시간 게임, 채팅 앱)에 적합합니다.

단점:

  • 폴링이나 SSE보다 구현이 더 복잡합니다.
  • WebSocket은 개방형 연결을 유지하여 리소스를 소비할 수 있으므로 단방향 통신이나 빈도가 낮은 업데이트에 항상 이상적인 것은 아닙니다.
  • 방화벽 구성이 필요할 수 있습니다.

최적의 용도: 멀티플레이어 게임, 공동 작업 도구, 채팅 애플리케이션 또는 실시간 알림과 같이 지속적인 양방향 통신이 필요한 애플리케이션

4. 서버 전송 이벤트(SSE)

마지막으로 결제 예시의 주인공인 Server-Sent Events(SSE)를 살펴보겠습니다. SSE는 서버가 클라이언트에 업데이트를 보내는 단방향 연결을 만듭니다. WebSocket과 달리 이는 단방향입니다. 즉, 클라이언트가 데이터를 다시 보내지 않습니다.

장점:

  • 뉴스 피드, 주식 시세 표시기 또는 결제 상태 업데이트와 같은 단방향 데이터 스트림에 적합합니다.
  • WebSocket보다 가볍고 구현이 더 간단합니다.
  • 기존 HTTP 연결을 사용하므로 지원이 잘되고 방화벽 친화적입니다.

단점:

  • 양방향 통신에는 적합하지 않습니다.
  • 일부 브라우저(특히 이전 버전의 IE)는 SSE를 완전히 지원하지 않습니다.

최적의 용도: 클라이언트가 실시간 점수, 알림, 결제 상태 예시 등의 데이터만 수신하면 되는 실시간 업데이트.


SSE 실행: Next.js를 사용한 실시간 결제 상태

문제의 핵심을 살펴보겠습니다. Server-Sent Events(SSE)를 사용하여 실시간 결제 프로세스를 시뮬레이션하기 위해 간단한 Next.js 앱을 만들었습니다. 단방향 통신을 설정하여 결제 상태를 확인하고 결제 성공 또는 실패 시 사용자에게 알리는 방법을 정확하게 보여줍니다.

Next에 설정하는 것은 일반 js와 약간 다르게 작동하기 때문에 약간 머리가 아프므로 나중에 감사할 수 있습니다!

설정은 다음과 같습니다.

프런트엔드: 거래 제어 구성요소

다음 구성 요소에는 실제 게이트웨이 API(Pix, Stripe 및 신용 카드 결제 실패)에서 발생하는 다양한 유형의 거래를 시뮬레이션하기 위한 버튼을 표시하는 간단한 UI가 있습니다. 이 버튼은 SSE를 통해 실시간 결제 상태 업데이트를 실행합니다.

여기서 SSE의 마법이 일어납니다. 결제가 시뮬레이션되면 클라이언트는 SSE 연결을 열어 서버의 업데이트를 수신합니다. 보류 중, 전송 중, 결제 완료, 실패 등 다양한 상태를 처리합니다.

"use client";

import { useState } from "react";
import { PAYMENT_STATUSES } from "../utils/payment-statuses";

const paymentButtons = [
  {
    id: "pix",
    label: "Simulate payment with Pix",
    bg: "bg-green-200",
    success: true,
  },
  {
    id: "stripe",
    label: "Simulate payment with Stripe",
    bg: "bg-blue-200",
    success: true,
  },
  {
    id: "credit",
    label: "Simulate failing payment",
    bg: "bg-red-200",
    success: false,
  },
];

type transaction = {
  type: string;
  amount: number;
  success: boolean;
};

const DOMAIN_URL = process.env.NEXT_PUBLIC_DOMAIN_URL;

export function TransactionControl() {
  const [status, setStatus] = useState<string>("");
  const [isProcessing, setIsProcessing] = useState<boolean>(false);

  async function handleTransaction({ type, amount, success }: transaction) {
    setIsProcessing(true);
    setStatus("Payment is in progress...");

    const eventSource = new EventSource(
      `${DOMAIN_URL}/payment?type=${type}&amount=${amount}&success=${success}`
    );

    eventSource.onmessage = (e) => {
      const data = JSON.parse(e.data);
      const { status } = data;

      console.log(data);

      switch (status) {
        case PAYMENT_STATUSES.PENDING:
          setStatus("Payment is in progress...");
          break;
        case PAYMENT_STATUSES.IN_TRANSIT:
          setStatus("Payment is in transit...");
          break;
        case PAYMENT_STATUSES.PAID:
          setIsProcessing(false);
          setStatus("Payment completed!");
          eventSource.close();
          break;
        case PAYMENT_STATUSES.CANCELED:
          setIsProcessing(false);
          setStatus("Payment failed!");
          eventSource.close();
          break;
        default:
          setStatus("");
          setIsProcessing(false);
          eventSource.close();
          break;
      }
    };
  }

  return (
    <div>
      <div className="flex flex-col gap-3">
        {paymentButtons.map(({ id, label, bg, success }) => (
          <button
            key={id}
            className={`${bg} text-background rounded-full font-medium py-2 px-4
            disabled:brightness-50 disabled:opacity-50`}
            onClick={() =>
              handleTransaction({ type: id, amount: 101, success })
            }
            disabled={isProcessing}
          >
            {label}
          </button>
        ))}
      </div>

      {status && <div className="mt-4 text-lg font-medium">{status}</div>}
    </div>
  );
}

로그인 후 복사

백엔드: Next.js의 SSE 구현

서버 측에서는 SSE를 통해 정기적인 상태 업데이트를 전송하여 결제 프로세스를 시뮬레이션합니다. 거래가 진행됨에 따라 고객은 결제가 아직 보류 중인지, 완료되었는지, 실패했는지에 대한 업데이트를 받게 됩니다.

import { NextRequest, NextResponse } from "next/server";

import { PAYMENT_STATUSES } from "../utils/payment-statuses";

export const runtime = "edge";
export const dynamic = "force-dynamic";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function GET(req: NextRequest, res: NextResponse) {
  const { searchParams } = new URL(req.url as string);
  const type = searchParams.get("type") || null;
  const amount = parseFloat(searchParams.get("amount") || "0");
  const success = searchParams.get("success") === "true";

  if (!type || amount < 0) {
    return new Response(JSON.stringify({ error: "invalid transaction" }), {
      status: 400,
      headers: {
        "Content-Type": "application/json",
      },
    });
  }
  const responseStream = new TransformStream();
  const writer = responseStream.writable.getWriter();
  const encoder = new TextEncoder();
  let closed = false;

  function sendStatus(status: string) {
    writer.write(
      encoder.encode(`data: ${JSON.stringify({ status, type, amount })}\n\n`)
    );
  }

  // Payment gateway simulation
  async function processTransaction() {
    sendStatus(PAYMENT_STATUSES.PENDING);

    function simulateSuccess() {
      setTimeout(() => {
        if (!closed) {
          sendStatus(PAYMENT_STATUSES.IN_TRANSIT);
        }
      }, 3000);

      setTimeout(() => {
        if (!closed) {
          sendStatus(PAYMENT_STATUSES.PAID);

          // Close the stream and mark closed to prevent further writes
          writer.close();
          closed = true;
        }
      }, 6000);
    }

    function simulateFailure() {
      setTimeout(() => {
        if (!closed) {
          sendStatus(PAYMENT_STATUSES.CANCELED);

          // Close the stream and mark closed to prevent further writes
          writer.close();
          closed = true;
        }
      }, 3000);
    }

    if (success === false) {
      simulateFailure();
      return;
    }

    simulateSuccess();
  }

  await processTransaction();

  // Return the SSE response
  return new Response(responseStream.readable, {
    headers: {
      "Access-Control-Allow-Origin": "*",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no",
      "Content-Type": "text/event-stream; charset=utf-8",
      "Cache-Control": "no-cache, no-transform",
      "Content-Encoding": "none",
    },
  });
}
로그인 후 복사

또한 다음 콘텐츠가 포함된 .env.local 파일을 추가하세요.

NEXT_PUBLIC_DOMAIN_URL='http://localhost:3000'
로그인 후 복사

이 경우 WebSocket보다 SSE가 필요한 이유는 무엇입니까?

이제 구현 방법을 살펴보았으니, 왜 WebSocket 대신 SSE를 사용하는지 궁금하실 것입니다. 이유는 다음과 같습니다.

  • 단방향 통신: 우리 시나리오에서 클라이언트는 결제 상태에 대한 업데이트만 수신하면 됩니다. 클라이언트가 지속적으로 서버에 데이터를 다시 보낼 필요가 없으므로 SSE의 단순성이 완벽하게 들어맞습니다.
  • 경량: SSE는 단일 HTTP 연결을 사용하여 업데이트를 스트리밍하므로 전이중 통신을 유지하는 WebSocket에 비해 리소스 효율성이 더 높습니다.
  • 방화벽 친화적: SSE는 일반적으로 방화벽에서 열려 있는 HTTP를 통해 실행되기 때문에 다양한 네트워크 환경에서 작업하기가 더 쉽습니다. 반면 WebSocket 연결에서는 때때로 문제가 발생할 수 있습니다.
  • 브라우저 지원: SSE는 WebSocket만큼 널리 지원되지는 않지만 최신 브라우저에서 지원되므로 단방향 데이터가 필요한 대부분의 사용 사례에서 안정적입니다.

결론: 도구를 알아두세요

그 인터뷰 질문은 놀라운 학습 경험으로 바뀌었고, 긴 폴링, 짧은 폴링, WebSocket 및 SSE 간의 미묘한 차이점에 눈을 뜨게 되었습니다. 각 방법에는 시간과 장소가 있으며, 실시간 커뮤니케이션을 최적화하려면 언제 어떤 방법을 사용해야 하는지 이해하는 것이 중요합니다.

SSE는 WebSocket만큼 화려하지는 않을 수도 있지만 효율적인 단방향 통신에 있어서는 전자상거래 결제 예시와 마찬가지로 작업에 완벽한 도구입니다. 다음에 실시간 업데이트가 필요한 것을 구축할 때는 기본적으로 WebSocket을 사용하지 말고 SSE의 단순성과 효율성을 고려해 보세요.

실시간 커뮤니케이션 기술에 대한 심층 분석을 통해 다음 프로젝트나 까다로운 인터뷰 질문에 대비할 수 있기를 바랍니다!


손을 더럽히자

Next.js + TypeScript 예제 저장소: https://github.com/brinobruno/sse-next
Next.js + TypeScript 예제 배포: https://sse-next-one.vercel.app/

참고자료

더 깊은 통찰력을 얻을 수 있는 몇 가지 권위 있는 출처와 참고 자료는 다음과 같습니다.

WebSocket 및 SSE 문서:

MDN 웹 문서: WebSockets API
MDN 웹 문서: 서버 전송 이벤트 사용

다음 API 경로

Next.js: API 경로


연결하자

연결을 원하실 경우를 대비해 관련 소셜 미디어를 공유해 드리겠습니다.
깃허브
링크드인
포트폴리오

위 내용은 실시간 웹 통신: 긴/짧은 폴링, WebSocket 및 SSE 설명 + Next.js 코드의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
저자별 최신 기사
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!