몇 달 전 저는 중간 프론트엔드 직무에 대한 기술 면접을 진행 중이었습니다. 일이 순조롭게 진행되다가 갑자기 당황스러운 질문을 받게 되었습니다.
"필요한 것을 찾을 때까지 매 순간 확인하는 지속적인 커뮤니케이션 형태가 필요하다고 상상해 보세요.
예를 들어 전자상거래 설정에서와 같이 결제가 성공적으로 이루어졌는지 계속 확인하고 싶습니다. 어떻게 접근하시겠습니까?"
저는 "그 문제를 처리하려면 WebSocket을 구현하면 될 것 같아요."라고 조심스럽게 대답했습니다.
면접관님이 웃으셨습니다. "그것도 좋은 해결책이지만 상황에 따라 더 나은 다른 옵션도 있습니다."
이제 우리는 Long Polling, Short Polling, WebSockets을 포함하여 실시간 통신에 대한 다양한 접근 방식에 대해 대화를 나눴고, 마지막으로 서버 전송 이벤트(SSE)는 결제 예시와 같이 단방향 데이터 스트림에 가장 적합한 선택입니다.
또한 서버 리소스를 소모하지 않고 지속적이면서도 가벼운 요청을 처리하기 위해 올바른 데이터베이스를 선택하는 방법에 대해서도 논의했습니다. 그런 맥락에서 이러한 유형의 요청을 관리하는 데 있어 간편성과 효율성으로 유명한 Redis가 등장했습니다.
이 대화가 마음에 맴돌았습니다. 저는 WebSocket이 많은 관심을 받고 있지만 이해하면 실시간 통신 관리 방식을 최적화할 수 있는 다양한 기술이 있다는 것을 깨달았습니다. 오늘 저는 이 네 가지 접근 방식, 각각의 사용 시기, 장단점을 명확하고 흥미로운 방식으로 분석하고 싶습니다. 결국에는 SSE(Server-Sent Events)가 단방향 실시간 통신에 적합한 이유를 확실하게 이해하게 될 것입니다.
시작하기 전에 채팅을 진행하고 몇 달 후 이 기사를 작성하도록 영감을 준 숙련된 수석 소프트웨어 엔지니어인 Marcos에게 큰 감사를 드립니다. 일자리를 얻지는 못했지만 정말 감사했습니다! :)
SSE 예시를 시작하기 전에 인터뷰에서 논의한 네 가지 방법을 자세히 살펴보겠습니다.
단기 폴링은 아마도 가장 간단한 방법일 것입니다. 정기적으로 서버에 "새 데이터가 있습니까?"라고 요청하는 작업이 포함됩니다. 서버는 새로운 내용이 있든 없든 현재 상태로 응답합니다.
장점:
단점:
최적의 용도: 약 1분마다 업데이트되는 주식 시장 가격과 같은 소규모, 빈도가 낮은 데이터 업데이트.
긴 폴링은 짧은 폴링을 한 단계 더 발전시킵니다. 클라이언트는 서버에 반복적으로 정보를 요청하지만 서버가 즉시 응답하는 대신 새 데이터를 사용할 수 있을 때까지 연결을 유지합니다. 데이터가 다시 전송되면 클라이언트는 즉시 새 연결을 열고 프로세스를 반복합니다.
장점:
단점:
최적의 용도: 실시간 통신이 필요하지만 WebSocket/SSE가 과도할 수 있는 상황(예: 채팅 애플리케이션).
WebSocket은 클라이언트와 서버 간의 전이중 통신을 제공하는 보다 현대적인 솔루션입니다. 연결이 열리면 양방향 통신을 정의하는 연결을 다시 설정하지 않고도 양측에서 자유롭게 데이터를 보낼 수 있습니다.
장점:
단점:
최적의 용도: 멀티플레이어 게임, 공동 작업 도구, 채팅 애플리케이션 또는 실시간 알림과 같이 지속적인 양방향 통신이 필요한 애플리케이션
마지막으로 결제 예시의 주인공인 Server-Sent Events(SSE)를 살펴보겠습니다. SSE는 서버가 클라이언트에 업데이트를 보내는 단방향 연결을 만듭니다. WebSocket과 달리 이는 단방향입니다. 즉, 클라이언트가 데이터를 다시 보내지 않습니다.
장점:
단점:
최적의 용도: 클라이언트가 실시간 점수, 알림, 결제 상태 예시 등의 데이터만 수신하면 되는 실시간 업데이트.
문제의 핵심을 살펴보겠습니다. 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> ); }
서버 측에서는 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는 WebSocket만큼 화려하지는 않을 수도 있지만 효율적인 단방향 통신에 있어서는 전자상거래 결제 예시와 마찬가지로 작업에 완벽한 도구입니다. 다음에 실시간 업데이트가 필요한 것을 구축할 때는 기본적으로 WebSocket을 사용하지 말고 SSE의 단순성과 효율성을 고려해 보세요.
실시간 커뮤니케이션 기술에 대한 심층 분석을 통해 다음 프로젝트나 까다로운 인터뷰 질문에 대비할 수 있기를 바랍니다!
Next.js + TypeScript 예제 저장소: https://github.com/brinobruno/sse-next
Next.js + TypeScript 예제 배포: https://sse-next-one.vercel.app/
더 깊은 통찰력을 얻을 수 있는 몇 가지 권위 있는 출처와 참고 자료는 다음과 같습니다.
MDN 웹 문서: WebSockets API
MDN 웹 문서: 서버 전송 이벤트 사용
Next.js: API 경로
연결을 원하실 경우를 대비해 관련 소셜 미디어를 공유해 드리겠습니다.
깃허브
링크드인
포트폴리오
위 내용은 실시간 웹 통신: 긴/짧은 폴링, WebSocket 및 SSE 설명 + Next.js 코드의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!