[Svelte 시리즈 17편] 실시간 업데이트와 폴링/웹소켓

English version

대시보드가 늦게 갱신되면 신뢰가 떨어집니다. 이번 글은 페이지 새로고침 없이 상태를 동기화하는 세 가지 전략을 살펴봅니다.

시작 전에 정리하기

  • 무엇을 만든다: /activity 화면에서 초기에 데이터를 로드하고, SSE 또는 폴링으로 실시간 동기화를 유지하며, 선택 시 웹소켓으로 확장하는 데모.
  • 왜 중요한가: 시리즈 후반부 앱은 여러 사용자가 동시에 바라보므로 신뢰할 수 있는 실시간 전략을 선택하고 전환할 수 있어야 합니다.
  • 주의할 점: SSE나 웹소켓이 끊기면 반드시 재연결 로직을 넣고, 폴링 간격을 너무 짧게 잡으면 서버 비용이 급증합니다.

이번 글에서 새로 나오는 용어

  1. 폴링: 일정 간격마다 API를 다시 호출해 최신 데이터를 가져오는 가장 단순한 실시간 전략입니다.
  2. SSE(Server-Sent Events): 서버가 텍스트 스트림으로 데이터를 밀어 주는 단방향 연결 방식입니다.
  3. 웹소켓(WebSocket): 서버와 클라이언트가 서로 동시에 메시지를 주고받을 수 있는 양방향 통신 채널입니다.
  4. 재연결 로직: 네트워크가 끊겼을 때 타이머나 오류 핸들러에서 다시 연결을 시도하는 코드입니다.

개념

폴링(polling)은 일정 간격마다 API를 다시 호출하는 방식입니다. 구현이 단순하지만 불필요한 요청이 생길 수 있습니다. 서버 전송 이벤트(SSE)는 서버가 텍스트 스트림으로 데이터를 밀어 주는 단방향 실시간 통신입니다. 웹소켓(WebSocket)은 양방향 통신으로 사용자가 보낸 이벤트를 다른 사용자에게 즉시 전달할 수 있습니다. 세 방식 모두 연결 해제가 생길 때 재시도 로직이 필요합니다.

D2로 보는 실시간 플로우

+page.server load+page.sveltesetInterval fetch /api/tasksEventSource /api/tasks/streamsocket.io client/api/tasks/api/tasks/stream/realtime serverDB tasks table initial GETqueryJSONtasksdata.tasksoptional intervalGET /api/tasksEventSource connectsubscribeperiodic fetchpayloadpush dataemit task:updatesend updateupdate rowresultbroadcast task:sync

👉 미니 요약: 초기 load가 한 번 데이터를 가져오고, 이후에는 SSE가 밀어주거나 폴링이 당겨 옵니다. 더 고급 상황이면 웹소켓으로 양방향 이벤트를 열 수 있지만, 기본은 “load → SSE → 폴링 백업” 순서입니다.

코드

폴링과 SSE를 기본으로 소개하고, 웹소켓은 확장 예제로 정리합니다.

// src/routes/(app)/activity/+page.svelte
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  export let data;

  let tasks = structuredClone(data.tasks);
  let interval: ReturnType<typeof setInterval> | null = null;

  function apply(payload) {
    tasks = payload;
  }

  onMount(() => {
    const source = new EventSource('/api/tasks/stream');
    source.onmessage = (event) => apply(JSON.parse(event.data));
    source.onerror = () => {
      source.close();
      if (!interval) {
        interval = setInterval(async () => {
          const res = await fetch('/api/tasks');
          apply(await res.json());
        }, 5000);
      }
    };
    return () => {
      source.close();
      if (interval) clearInterval(interval);
    };
  });
</script>

<ActivityTimeline {tasks} />
<!-- src/routes/(app)/activity/+page.server.ts -->

export const load: PageServerLoad = async ({ fetch, locals }) => {
  if (!locals.user) {
    throw error(401, '로그인이 필요합니다.');
  }
  const res = await fetch('/api/tasks');
  if (!res.ok) {
    throw error(res.status, '초기 데이터를 불러오지 못했습니다.');
  }
  return { tasks: await res.json() };
};
// src/routes/api/tasks/stream/+server.ts
export const GET = () => {
  const stream = new ReadableStream({
    start(controller) {
      const timer = setInterval(async () => {
        const payload = await getTasks();
        controller.enqueue(`data: ${JSON.stringify(payload)}\n\n`);
      }, 3000);
      return () => clearInterval(timer);
    }
  });
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive'
    }
  });
};

(선택) 양방향 웹소켓 확장

팀 프로젝트에 꼭 필요하지 않으면 건너뛰어도 됩니다. SSE나 폴링만으로도 대부분의 과제 알림은 충분합니다. 양방향 채널이 필요하다고 판단될 때 아래 코드를 참고하세요.

const socket = io({ path: '/realtime' });
socket.on('task:sync', (task) => {
  tasks = tasks.map((t) => (t.id === task.id ? task : t));
});

function toggle(task) {
  socket.emit('task:update', { ...task, done: !task.done });
}
http://localhost:5173/activity
DEV

UI Preview

실시간 타임라인

SSE 연결이 살아 있으면 카드가 즉시 갱신되고, 끊기면 폴링으로 자연스럽게 전환합니다. 웹소켓 확장 시 토글한 과제가 바로 모든 탭에 반영됩니다.

EventSourcepolling

SSE

/api/tasks/stream

3초마다 payload push.

Fallback

setInterval

스트림 오류 시 자동 시작.

WebSocket

task:update / task:sync

양방향 이벤트.

👉 미니 요약: 첫 두 탭은 기본 필수 흐름(SSE + 폴백)이고, 마지막 웹소켓 탭은 선택 확장입니다. 고급 탭을 건너뛰어도 나머지 기능이 완성되므로 부담 갖지 마세요.

왜 중요한가

교내 프로젝트 관리 앱이나 캠페인 통계 화면은 여러 사용자가 동시에 바라봅니다. 실시간 전략을 적절히 골라야 데이터가 어긋나지 않습니다. 폴링은 인프라 설정이 쉬워 빠르게 도입할 수 있고, SSE는 서버 푸시가 필요한데 양방향 채팅이 아니라면 충분합니다. 웹소켓은 복잡하지만 가장 즉각적인 피드백을 제공합니다.

실습

  • 따라 하기: 폴링 또는 SSE 중 하나를 선택해 /api/tasks 데이터를 일정 주기로 갱신한다.
  • 확장하기: 시간이 남으면 웹소켓 예제를 적용해 양방향 이벤트를 데모한다.
  • 디버깅: 연결이 끊기면 clearInterval, EventSource.close(), 소켓 재연결 로직을 점검한다.
  • 완료 기준: 실시간 목록 데모를 5분 안에 설명하고 선택한 전략이 명확히 기록된다.

마무리

실시간 업데이트가 준비되면 사용자에게 신뢰 있는 정보를 제공할 수 있습니다. 다음 편에서는 성능 최적화와 코드 분할 전략으로 속도를 다듬습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요