블로그 목록
Tutorial10분 읽기

Agora RTM으로 실시간 채팅 구현하기

Agora RTM의 login, channel subscribe, publish, unsubscribe, logout 수명주기로 채팅을 구현합니다. RTC channel과 RTM channel의 식별자를 함께 사용할 수 있지만 연결과 token 권한은 별도라는 점을 구분하고, message validation·rate limit·한글 IME composition 처리도 다룹니다.

AgoraRTMChatMessaging

Agora RTM(Real-Time Messaging)은 실시간 텍스트 메시징과 시그널링을 위한 경량 SDK입니다. 화상 통화(RTC)와 함께 사용하면 채널 내 채팅, 손들기, 참가자 상태 등을 안정적으로 구현할 수 있습니다. 이 글에서는 RTM으로 실시간 채팅 시스템을 구축하는 방법과 모범 사례를 정리합니다.

RTM이란?

RTM은 Agora의 실시간 메시징 서비스로, 음성·영상이 아닌 데이터를 빠르게 주고받을 때 사용합니다.

  • 채널 메시지: 특정 채널을 구독한 모든 사용자에게 메시지 브로드캐스트
  • 시그널링: 화면 공유 시작/중지, 손들기, 호스트·시청자 역할 등 앱 단위 이벤트를 JSON으로 전달
  • 저지연: 전용 인프라로 짧은 지연 시간과 높은 도달률을 목표로 설계됨

RTC(화상·음성)와 RTM(텍스트·시그널링)은 별도 SDK와 권한 범위를 사용하며, 같은 Agora App ID 아래에서 채널명과 사용자 식별자를 맞춰 함께 사용할 수 있습니다. 이 글의 예시는 운영과 권한 분리를 위해 토큰 API도 나누지만, AccessToken2에는 RTC와 RTM 권한을 함께 담을 수도 있습니다.

같은 방(채널) 하나에 두 개의 독립 파이프라인이 나란히 붙는 구조입니다.

                 ┌──────────────────────────────────────┐
   Client A      │            Agora App ID                │      Client B
 ┌───────────┐   │                                        │   ┌───────────┐
 │ RTC SDK   │───┼──▶ RTC 채널 "room-1"  (영상/음성) ◀────┼───│ RTC SDK   │
 │ uid=1001  │   │     토큰: 채널+uid 기준               │   │ uid=1002  │
 ├───────────┤   │                                        │   ├───────────┤
 │ RTM SDK   │───┼──▶ RTM 채널 "room-1" (채팅/시그널링)◀─┼───│ RTM SDK   │
 │ user=1001 │   │     토큰: userId 기준                 │   │ user=1002 │
 └───────────┘   │                                        │   └───────────┘
                 └──────────────────────────────────────┘
   채널명·사용자 매핑 필요   토큰은 별도 발급 또는 RTC+RTM 권한 결합

준비 사항

  • Agora 콘솔: 프로젝트의 App ID, App Certificate
  • RTM 토큰 서버: App Certificate는 서버에만 두고, 클라이언트에는 userId로 요청한 RTM 토큰만 내려주는 API를 구현합니다.
  • 패키지: agora-rtm-sdk (Web)

전체 흐름 요약

  1. RTM 클라이언트 생성 → new RTM(APP_ID, userId)
  2. 토큰 발급 → 백엔드에서 userId 기준 RTM 토큰 발급
  3. 로그인 → rtm.login({ token })
  4. 채널 구독 → rtm.subscribe(channelName) → 해당 채널 메시지 수신 가능
  5. 메시지 발송 → rtm.publish(channelName, message)
  6. 메시지 수신 → rtm.addEventListener("message", handler) 에서 event.publisher, event.message 처리
  7. 퇴장 → rtm.logout()

아래 코드는 프로젝트의 app/agora-demo/rtc/page.tsx에서 사용한 RTM 채팅 흐름을 참고한 예시입니다.

1. RTM 클라이언트 생성 및 로그인

RTM은 사용자 단위로 동작합니다. userId는 문자열로, RTC의 uid와 맞춰 두면 같은 사용자를 하나의 주체로 다루기 쉽습니다.

import AgoraRTM from "agora-rtm-sdk";

const { RTM } = AgoraRTM;
const rtm = new RTM(APP_ID, rtmUserId);

// 이 예시는 백엔드에서 RTM 전용 토큰을 별도 발급
const res = await fetch(
  `${RTM_TOKEN_API}?userId=${encodeURIComponent(rtmUserId)}`
);
const { token: rtmToken } = await res.json();

await rtm.login({ token: rtmToken });
  • RTC 채널 토큰(/api/token)과 RTM 사용자 토큰(/api/rtm-token)은 서로 다른 API에서 발급하는 구성을 권장합니다.

2. 채널 구독 및 메시지 수신

채팅을 하려면 채널을 구독해야 합니다. 구독한 채널로 들어오는 모든 메시지를 message 이벤트로 받을 수 있습니다.

await rtm.subscribe(channelName);

rtm.addEventListener("message", (event) => {
  const raw =
    typeof event.message === "string"
      ? event.message
      : new TextDecoder().decode(event.message);
  const publisher = event.publisher; // 발신자 userId

  // JSON 시그널링(화면 공유 등)과 일반 채팅 구분
  try {
    const d = JSON.parse(raw);
    if (d?.type === "screen") {
      // 화면 공유 시작/중지 시그널 처리
      setScreenShareUid(d.start ? Number(d.uid) : null);
      return;
    }
  } catch {
    // JSON이 아니면 일반 채팅 메시지
  }

  // 자신이 보낸 메시지는 발송 시 이미 UI에 넣었으므로 중복 표시 방지
  if (String(publisher) === String(rtmUserId)) return;

  setMessages((prev) => [
    ...prev,
    { uid: publisher, text: raw, isMe: false },
  ]);
});
  • 채널 메시지는 해당 채널을 구독한 모든 사용자에게 전달됩니다.
  • 메시지가 문자열 또는 Uint8Array로 올 수 있으므로, 바이너리일 때는 TextDecoder로 디코딩하는 것이 안전합니다.

3. 메시지 발송

채널에 텍스트를 보낼 때는 rtm.publish(channelName, message)를 사용합니다. 메시지는 문자열로 보내며, JSON 문자열을 사용하면 타입별 시그널링을 나눌 수 있습니다.

// 일반 채팅
await rtm.publish(channelName, "안녕하세요");

// 시그널링 예: 화면 공유 알림
await rtm.publish(
  channelName,
  JSON.stringify({ type: "screen", start: true, uid: localUid })
);
  • 발송 성공 후 자신의 메시지는 곧바로 로컬 UI에 추가하고, message 이벤트에서는 publisher === 자신인 경우는 스킵하면 중복 표시를 막을 수 있습니다.

4. 한글 IME 중복 전송 방지

한글처럼 조합형 입력(IME)을 쓰는 경우, 조합 확정에 사용한 Enter를 메시지 전송으로 처리하면 의도하지 않은 전송이 생길 수 있습니다. 시간·문자열 길이 휴리스틱은 정상적인 짧은 메시지를 버릴 수 있으므로, 표준 KeyboardEvent.isComposing과 composition 이벤트를 사용합니다.

const isComposingRef = useRef(false);

async function handleKeyDown(event) {
  if (event.key !== "Enter") return;
  if (event.isComposing || isComposingRef.current) return;

  event.preventDefault();
  const text = draft.trim();
  if (!text) return;
  await rtm.publish(channelName, text);
  setDraft("");
}

<input
  value={draft}
  onChange={(event) => setDraft(event.target.value)}
  onCompositionStart={() => { isComposingRef.current = true; }}
  onCompositionEnd={() => { isComposingRef.current = false; }}
  onKeyDown={handleKeyDown}
/>
  • isComposing이 true인 keydown은 조합 확정 입력이므로 전송하지 않습니다. 전송 요청 자체의 중복 방지는 별도의 요청 ID나 버튼 비활성화로 처리합니다.

5. 시그널링 확장 (타입별 메시지)

채팅과 시그널링을 같은 채널에서 구분하려면 JSON으로 type 필드를 두고 처리하는 방식을 많이 씁니다.

type용도
(없음 또는 일반 텍스트)채팅 메시지
screen화면 공유 시작/중지 알림 (start, uid)
presence참가자 수·역할 (라이브 스트리밍에서 호스트/시청자 수 등)
signaling손들기, 질문 요청 등 (예: action: "raise_hand", fromUid)

예시:

// 수신 측
const d = JSON.parse(raw);
switch (d?.type) {
  case "screen":
    setScreenShareUid(d.start ? d.uid : null);
    break;
  case "presence":
    setPresenceMap((prev) => ({ ...prev, [d.uid]: { role: d.role } }));
    break;
  case "signaling":
    if (d.action === "raise_hand") addRaisedHand(d.fromUid);
    break;
  default:
    addChatMessage({ uid: event.publisher, text: raw });
}

6. 퇴장 시 정리

채널을 나갈 때는 RTM 로그아웃을 호출해 연결과 리소스를 정리합니다. RTC와 함께 쓰는 경우에는 RTC leave 및 트랙 close 후 rtm.logout()을 호출하면 됩니다.

await rtm.logout();

RTC와 함께 쓸 때 정리

구분RTCRTM
역할음성·영상 스트림텍스트·시그널링
토큰채널·uid 기준 RTC 토큰userId 기준 RTM 토큰
입장client.join(appId, channelName, token, uid)rtm.login({ token }) + rtm.subscribe(channelName)
퇴장client.leave(), 트랙 close()rtm.logout()
  • 같은 채널명을 사용하면, 화상 통화 방 하나에 RTC(영상/음성)와 RTM(채팅·시그널링)을 동시에 붙일 수 있습니다.
  • 실제 구현 예는 Agora Demo > RTC - Call (/agora-demo/rtc)에서 채팅 패널과 화면 공유 알림을 확인할 수 있고, Live Streaming 데모에서는 손들기·참가자 수(presence)까지 RTM으로 처리하고 있습니다.

참고

  • 이 예시는 RTM 토큰을 별도 백엔드 API에서 발급합니다. 하나의 AccessToken2를 쓸 때는 RTC와 RTM 권한을 모두 포함해야 합니다.
  • 메시지 크기·빈도 제한은 Agora Signaling 제한 사항을 참고하세요.
  • 기본 API 흐름은 Agora Signaling Web quickstart를 참고하세요.

RTC 화상 통화 구현은 Agora RTC를 활용한 실시간 화상 통화 구현하기 글을 참고하세요.

관련 글

참고 자료

실제 데모

/agora-demo/rtc

© 2026 Frank Kim. All rights reserved.