이미지 로딩 중...

메시지 기능 확장 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 22. · 4 Views

메시지 기능 확장 완벽 가이드

채팅 앱의 메시지 기능을 한 단계 업그레이드하는 방법을 알려드립니다. 메시지 수정, 삭제부터 이모지 반응, 답장, 전달 기능까지 실무에서 꼭 필요한 고급 기능들을 단계별로 구현해봅니다. 초급 개발자도 쉽게 따라할 수 있도록 친절하게 설명합니다.


목차

  1. 메시지 수정 기능
  2. 메시지 삭제 기능
  3. 이모지 반응 추가
  4. 메시지 고정 기능
  5. 답장 (Reply) 기능
  6. 전달 (Forward) 기능

1. 메시지 수정 기능

시작하며

여러분이 채팅 앱에서 메시지를 보낸 후 오타를 발견한 적 있나요? 또는 내용을 조금 수정하고 싶었던 경험이 있으신가요?

사용자들은 메시지를 삭제하고 다시 쓰는 대신, 간단히 수정할 수 있기를 원합니다. 메시지 수정 기능이 없으면 사용자는 불편함을 겪게 됩니다.

특히 긴 메시지를 작성한 후 작은 실수를 발견했을 때, 전체를 다시 작성해야 한다면 사용자 경험이 크게 떨어집니다. 또한 대화의 맥락이 끊기고, 채팅방이 지워진 메시지와 새 메시지로 어지러워질 수 있습니다.

바로 이럴 때 필요한 것이 메시지 수정 기능입니다. 사용자가 이미 보낸 메시지를 클릭하여 내용을 수정하고, 수정된 사실을 다른 사람들에게 투명하게 알릴 수 있습니다.

개요

간단히 말해서, 메시지 수정 기능은 이미 전송된 메시지의 내용을 변경할 수 있게 해주는 기능입니다. 사용자가 메시지를 길게 눌러 편집 모드로 진입하고, 내용을 수정한 후 저장하면 됩니다.

실무에서 이 기능이 필요한 이유는 명확합니다. 카카오톡, 슬랙, 디스코드 같은 주요 메시징 앱들이 모두 이 기능을 제공하고 있으며, 사용자들은 이를 당연한 기능으로 여깁니다.

특히 업무용 채팅에서는 정확한 정보 전달이 중요하기 때문에, 오타나 잘못된 내용을 빠르게 수정할 수 있어야 합니다. 기존에는 메시지를 삭제하고 다시 보내야 했다면, 이제는 클릭 몇 번으로 내용을 수정할 수 있습니다.

이는 대화의 흐름을 유지하면서도 정확한 정보를 전달할 수 있게 해줍니다. 이 기능의 핵심 특징은 세 가지입니다.

첫째, 수정 이력 추적으로 누가 언제 무엇을 수정했는지 기록합니다. 둘째, 수정된 메시지에는 '(수정됨)' 표시가 나타나 투명성을 보장합니다.

셋째, 일정 시간이 지나면 수정을 제한하여 악용을 방지합니다. 이러한 특징들이 신뢰할 수 있는 채팅 환경을 만드는 데 중요한 역할을 합니다.

코드 예제

// 메시지 수정 기능 구현
interface Message {
  id: string;
  content: string;
  isEdited: boolean;
  editedAt?: Date;
}

// 메시지 수정 함수
async function editMessage(messageId: string, newContent: string): Promise<Message> {
  // 수정 가능 시간 체크 (5분 이내)
  const message = await getMessageById(messageId);
  const timeLimit = 5 * 60 * 1000; // 5분

  if (Date.now() - message.createdAt.getTime() > timeLimit) {
    throw new Error('수정 가능한 시간이 지났습니다');
  }

  // 메시지 업데이트
  const updatedMessage = {
    ...message,
    content: newContent,
    isEdited: true,
    editedAt: new Date()
  };

  await saveMessage(updatedMessage);
  return updatedMessage;
}

설명

이것이 하는 일: 메시지 수정 기능은 사용자가 이미 전송한 메시지를 다시 편집할 수 있게 해주며, 수정 이력을 추적하고 투명하게 표시합니다. 첫 번째로, Message 인터페이스는 메시지의 기본 정보를 정의합니다.

id와 content는 메시지의 식별자와 내용이고, isEdited는 수정 여부를, editedAt은 수정 시각을 저장합니다. 이렇게 구조화하면 메시지가 수정되었는지, 언제 수정되었는지를 명확하게 추적할 수 있습니다.

그 다음으로, editMessage 함수가 실행되면서 먼저 수정 가능한 시간이 지났는지 확인합니다. 메시지를 보낸 지 5분이 지나면 수정할 수 없도록 제한하여, 나중에 대화 내용을 임의로 바꾸는 악용을 방지합니다.

이는 채팅의 신뢰성을 유지하는 중요한 안전장치입니다. 마지막으로, 시간 체크를 통과하면 메시지 객체를 업데이트합니다.

새로운 내용으로 content를 변경하고, isEdited를 true로 설정하며, editedAt에 현재 시각을 기록합니다. 이 정보들은 UI에서 '(수정됨)' 표시를 보여주는 데 사용됩니다.

여러분이 이 코드를 사용하면 사용자 친화적인 메시지 수정 기능을 구현할 수 있습니다. 사용자는 실수를 쉽게 바로잡을 수 있고, 다른 사용자들은 메시지가 수정되었다는 사실을 알 수 있어 투명한 대화가 가능합니다.

또한 시간 제한으로 악용을 방지하면서도 충분한 수정 시간을 제공합니다.

실전 팁

💡 수정 이력을 데이터베이스에 별도로 저장하여 나중에 수정 전 내용을 확인할 수 있게 하세요. 관리자 기능이나 분쟁 해결에 유용합니다.

💡 수정 중인 상태를 다른 사용자에게 실시간으로 표시하세요. "홍길동이 메시지를 수정하고 있습니다..." 같은 표시로 충돌을 방지할 수 있습니다.

💡 수정 횟수를 제한하는 것도 고려하세요. 무한 수정을 허용하면 시스템 리소스가 낭비될 수 있습니다.

💡 모바일에서는 길게 누르기, 웹에서는 마우스 오버 시 나타나는 메뉴를 제공하여 직관적인 UX를 만드세요.

💡 수정된 메시지를 클릭하면 수정 전후 비교를 보여주는 기능을 추가하면 사용자 신뢰도가 높아집니다.


2. 메시지 삭제 기능

시작하며

여러분이 잘못된 사람에게 메시지를 보냈거나, 민감한 정보를 실수로 공유했던 적이 있나요? 이런 상황에서는 메시지를 빠르게 삭제해야 합니다.

특히 단체 채팅방에서는 더욱 시급합니다. 메시지 삭제 기능이 없거나 불완전하면 사용자는 큰 곤란을 겪을 수 있습니다.

개인정보나 기밀 정보가 노출되거나, 오해를 불러일으키는 메시지가 남아있으면 심각한 문제가 발생할 수 있습니다. 또한 단순히 채팅방을 정리하고 싶을 때도 삭제 기능은 필수적입니다.

바로 이럴 때 필요한 것이 메시지 삭제 기능입니다. '나만 삭제'와 '모두에게서 삭제' 옵션을 제공하여, 상황에 맞게 유연하게 대처할 수 있습니다.

개요

간단히 말해서, 메시지 삭제 기능은 원하지 않는 메시지를 채팅방에서 제거하는 기능입니다. 사용자가 자신이 보낸 메시지를 선택하고 삭제 옵션을 선택하면 됩니다.

실무에서 이 기능은 두 가지 모드로 구현됩니다. '나만 삭제'는 내 화면에서만 메시지가 사라지고 다른 사람들은 여전히 볼 수 있습니다.

'모두에게서 삭제'는 모든 참여자의 화면에서 메시지가 사라집니다. WhatsApp 같은 앱에서는 "이 메시지가 삭제되었습니다"라는 표시가 남아 투명성을 유지합니다.

기존에는 관리자만 메시지를 삭제할 수 있었다면, 이제는 사용자 스스로 자신의 메시지를 관리할 수 있습니다. 이는 프라이버시 보호와 사용자 권한 강화라는 현대 앱 디자인의 핵심 원칙을 반영합니다.

이 기능의 핵심 특징은 다음과 같습니다. 첫째, 소프트 삭제(soft delete) 방식으로 데이터베이스에서는 유지하되 UI에서만 숨깁니다.

둘째, 삭제 시간 제한을 두어 오래된 메시지는 삭제하지 못하게 합니다. 셋째, 삭제 알림을 다른 사용자에게 전송하여 실시간 동기화를 보장합니다.

이러한 특징들이 안전하고 신뢰할 수 있는 삭제 기능을 만듭니다.

코드 예제

// 메시지 삭제 기능 구현
enum DeleteType {
  FOR_ME = 'FOR_ME',
  FOR_EVERYONE = 'FOR_EVERYONE'
}

interface DeletedMessage {
  id: string;
  content: string;
  isDeleted: boolean;
  deletedAt?: Date;
  deletedBy?: string;
}

// 메시지 삭제 함수
async function deleteMessage(
  messageId: string,
  userId: string,
  deleteType: DeleteType
): Promise<void> {
  const message = await getMessageById(messageId);

  // 삭제 권한 체크
  if (message.senderId !== userId) {
    throw new Error('자신이 보낸 메시지만 삭제할 수 있습니다');
  }

  if (deleteType === DeleteType.FOR_EVERYONE) {
    // 모두에게서 삭제 (소프트 삭제)
    await updateMessage(messageId, {
      isDeleted: true,
      deletedAt: new Date(),
      deletedBy: userId
    });
    // 실시간 알림 전송
    await broadcastMessageDeleted(messageId);
  } else {
    // 나만 삭제 (사용자별 숨김 목록에 추가)
    await addToHiddenMessages(userId, messageId);
  }
}

설명

이것이 하는 일: 메시지 삭제 기능은 사용자가 자신이 보낸 메시지를 제거할 수 있게 해주며, 삭제 범위를 선택할 수 있습니다. 첫 번째로, DeleteType 열거형과 DeletedMessage 인터페이스가 삭제 동작을 정의합니다.

DeleteType은 삭제 범위를 구분하고, DeletedMessage는 삭제된 메시지의 정보를 저장합니다. isDeleted 플래그로 메시지가 삭제되었는지 표시하고, deletedAt과 deletedBy로 언제 누가 삭제했는지 기록합니다.

이렇게 하면 감사(audit) 목적으로 삭제 이력을 추적할 수 있습니다. 그 다음으로, deleteMessage 함수가 실행되면서 먼저 권한을 확인합니다.

메시지를 보낸 사람만 삭제할 수 있도록 검증하여 보안을 유지합니다. 다른 사람의 메시지를 임의로 삭제하는 것을 방지하는 중요한 단계입니다.

세 번째 단계에서는 삭제 유형에 따라 다르게 처리됩니다. FOR_EVERYONE인 경우 메시지를 소프트 삭제합니다.

실제로 데이터베이스에서 삭제하지 않고 isDeleted 플래그만 true로 설정합니다. 이렇게 하면 나중에 복구하거나 법적 문제가 생겼을 때 원본 데이터를 확인할 수 있습니다.

그리고 broadcastMessageDeleted로 모든 참여자에게 실시간으로 삭제 사실을 알립니다. FOR_ME인 경우에는 사용자별 숨김 목록에 메시지 ID를 추가합니다.

이렇게 하면 해당 사용자의 화면에서만 메시지가 보이지 않고, 다른 사람들은 여전히 볼 수 있습니다. 각 사용자가 자신의 채팅 화면을 독립적으로 관리할 수 있게 해주는 방식입니다.

여러분이 이 코드를 사용하면 유연하고 안전한 메시지 삭제 기능을 만들 수 있습니다. 사용자는 상황에 맞게 삭제 범위를 선택할 수 있고, 소프트 삭제 방식으로 데이터 무결성을 유지하며, 권한 검증으로 보안을 보장합니다.

실전 팁

💡 삭제된 메시지 자리에 "이 메시지가 삭제되었습니다" 같은 플레이스홀더를 표시하여 대화 맥락을 유지하세요. 완전히 제거하면 대화 흐름이 이상해집니다.

💡 중요한 메시지는 삭제 전 확인 다이얼로그를 표시하세요. "정말 모두에게서 삭제하시겠습니까?" 같은 메시지로 실수를 방지합니다.

💡 삭제 시간 제한을 설정하세요. 예를 들어 보낸 지 1시간이 지나면 모두에게서 삭제할 수 없게 하여, 대화 내용을 나중에 임의로 바꾸는 것을 방지합니다.

💡 데이터베이스에서 완전 삭제(hard delete)는 주기적인 정리 작업에서만 수행하세요. 예를 들어 삭제된 지 90일이 지난 메시지만 실제로 삭제합니다.

💡 관리자 권한으로 다른 사용자의 메시지를 삭제할 수 있는 기능을 별도로 구현하되, 이 경우 삭제 로그를 더 상세히 기록하세요.


3. 이모지 반응 추가

시작하며

여러분이 친구의 메시지에 빠르게 반응하고 싶을 때, 긴 답장을 쓰는 대신 간단한 이모지로 감정을 표현하고 싶었던 적이 있나요? "좋아요 👍", "웃겨요 😂", "슬퍼요 😢" 같은 반응은 대화를 더 생동감 있게 만듭니다.

텍스트로만 반응하면 채팅방이 금방 복잡해집니다. 10명이 "좋아요", "저도요", "동의합니다"라고 답장하면 정작 중요한 내용이 묻혀버립니다.

또한 간단한 공감을 표현하기 위해 매번 메시지를 작성하는 것은 비효율적입니다. 바로 이럴 때 필요한 것이 이모지 반응 기능입니다.

메시지 옆에 작은 이모지를 추가하여, 여러 사람의 반응을 깔끔하게 집계하고 표시할 수 있습니다.

개요

간단히 말해서, 이모지 반응은 메시지에 대한 감정이나 의견을 빠르게 표현하는 기능입니다. 사용자가 메시지를 길게 누르면 나타나는 이모지 팔레트에서 원하는 반응을 선택하면 됩니다.

실무에서 이 기능은 사용자 참여도를 크게 높입니다. Facebook, Instagram, Slack 등 모든 주요 소셜 플랫폼이 이 기능을 제공하고 있습니다.

특히 업무용 채팅에서는 "확인했습니다"는 의미의 체크 이모지 ✅나, 동의를 나타내는 엄지 👍가 매우 유용합니다. 투표나 의견 수렴이 필요할 때도 간단히 이모지로 집계할 수 있습니다.

기존에는 각자 "좋아요", "동의합니다" 같은 짧은 답장을 보내야 했다면, 이제는 클릭 한 번으로 반응할 수 있습니다. 채팅방이 깔끔하게 유지되면서도 모든 사람의 의견을 쉽게 파악할 수 있습니다.

이 기능의 핵심 특징은 다음과 같습니다. 첫째, 반응 집계 시스템으로 같은 이모지를 선택한 사람들을 그룹화합니다.

둘째, 실시간 업데이트로 누군가 반응을 추가하면 즉시 모든 사람에게 표시됩니다. 셋째, 반응 취소 기능으로 실수로 잘못 누른 이모지를 다시 클릭하여 제거할 수 있습니다.

이러한 특징들이 직관적이고 즐거운 대화 경험을 만듭니다.

코드 예제

// 이모지 반응 기능 구현
interface Reaction {
  emoji: string;
  userId: string;
  createdAt: Date;
}

interface MessageWithReactions {
  id: string;
  content: string;
  reactions: Reaction[];
}

// 이모지 반응 추가/제거 함수
async function toggleReaction(
  messageId: string,
  userId: string,
  emoji: string
): Promise<MessageWithReactions> {
  const message = await getMessageById(messageId);

  // 이미 같은 반응을 했는지 확인
  const existingReactionIndex = message.reactions.findIndex(
    r => r.userId === userId && r.emoji === emoji
  );

  if (existingReactionIndex >= 0) {
    // 반응 제거 (토글)
    message.reactions.splice(existingReactionIndex, 1);
  } else {
    // 반응 추가
    message.reactions.push({
      emoji,
      userId,
      createdAt: new Date()
    });
  }

  await saveMessage(message);
  // 실시간 업데이트 브로드캐스트
  await broadcastReactionUpdate(messageId, message.reactions);

  return message;
}

// 반응 집계 함수
function aggregateReactions(reactions: Reaction[]): Map<string, string[]> {
  const aggregated = new Map<string, string[]>();

  reactions.forEach(reaction => {
    if (!aggregated.has(reaction.emoji)) {
      aggregated.set(reaction.emoji, []);
    }
    aggregated.get(reaction.emoji)!.push(reaction.userId);
  });

  return aggregated;
}

설명

이것이 하는 일: 이모지 반응 기능은 사용자가 메시지에 대한 감정이나 의견을 이모지로 표현할 수 있게 해주며, 여러 사람의 반응을 깔끔하게 집계합니다. 첫 번째로, Reaction과 MessageWithReactions 인터페이스가 반응 데이터 구조를 정의합니다.

각 반응은 어떤 이모지인지(emoji), 누가 추가했는지(userId), 언제 추가했는지(createdAt) 정보를 담고 있습니다. 메시지는 reactions 배열로 모든 반응을 저장합니다.

이렇게 구조화하면 한 메시지에 여러 종류의 이모지 반응과 여러 사용자의 반응을 효율적으로 관리할 수 있습니다. 그 다음으로, toggleReaction 함수가 실행되면서 토글(toggle) 방식으로 동작합니다.

사용자가 이미 같은 이모지로 반응했으면 제거하고, 아직 반응하지 않았으면 추가합니다. 이는 UI에서 매우 직관적인 동작입니다.

사용자가 👍를 한 번 누르면 추가되고, 다시 누르면 취소되는 방식이죠. findIndex로 기존 반응을 찾아서, 있으면 splice로 제거하고, 없으면 push로 추가합니다.

세 번째로, 반응이 추가되거나 제거된 후에는 broadcastReactionUpdate로 모든 채팅 참여자에게 실시간으로 알립니다. 이렇게 하면 누군가 반응을 추가하는 순간 다른 사람들의 화면에서도 즉시 업데이트됩니다.

채팅방의 실시간성을 유지하는 중요한 부분입니다. 마지막으로, aggregateReactions 함수는 UI 표시를 위해 반응을 집계합니다.

같은 이모지를 선택한 사람들을 그룹화하여 Map 형태로 반환합니다. 예를 들어 👍를 3명이, 😂를 2명이 선택했다면, Map에 {👍: [user1, user2, user3], 😂: [user4, user5]} 형태로 저장됩니다.

UI에서는 "👍 3 😂 2" 같은 형태로 간결하게 표시할 수 있습니다. 여러분이 이 코드를 사용하면 직관적이고 효율적인 반응 시스템을 만들 수 있습니다.

사용자는 클릭 한 번으로 의견을 표현하고, 같은 생각을 가진 사람들이 얼마나 되는지 한눈에 파악할 수 있습니다. 또한 토글 방식으로 실수를 쉽게 되돌릴 수 있고, 실시간 업데이트로 생동감 있는 대화가 가능합니다.

실전 팁

💡 자주 사용하는 이모지 5-6개를 빠른 선택 버튼으로 제공하세요. 전체 이모지 팔레트는 '더보기' 버튼으로 접근할 수 있게 하면 UX가 좋아집니다.

💡 반응 아이콘에 마우스를 올리면 누가 반응했는지 툴팁으로 보여주세요. "홍길동, 김철수, 이영희가 좋아합니다" 같은 정보가 유용합니다.

💡 한 사용자가 하나의 메시지에 여러 종류의 이모지로 반응할 수 있게 하세요. 👍와 😂를 동시에 누를 수 있어야 합니다.

💡 애니메이션 효과를 추가하여 반응이 추가될 때 이모지가 톡 튀어나오는 느낌을 주세요. 작은 디테일이 사용자 경험을 크게 향상시킵니다.

💡 반응 개수가 많으면 "👍 99+"처럼 표시하여 UI가 지저분해지는 것을 방지하세요. 클릭하면 전체 목록을 모달로 보여줄 수 있습니다.


4. 메시지 고정 기능

시작하며

여러분이 단체 채팅방에서 중요한 공지사항이나 안내를 공유했는데, 새로운 메시지들에 묻혀서 다시 찾기 어려웠던 경험이 있나요? 활발한 채팅방에서는 중요한 메시지가 금방 위로 올라가버립니다.

메시지 고정 기능이 없으면 같은 공지를 반복해서 올려야 합니다. "회의 시간 오후 3시입니다", "저녁 식사 장소는 강남역 2번 출구입니다" 같은 중요한 정보를 사람들이 계속 물어보면, 매번 스크롤을 올려 찾거나 다시 입력해야 합니다.

이는 비효율적이고 짜증나는 일입니다. 바로 이럴 때 필요한 것이 메시지 고정 기능입니다.

중요한 메시지를 채팅방 상단에 고정하여, 누구나 쉽게 확인할 수 있게 만듭니다.

개요

간단히 말해서, 메시지 고정 기능은 특정 메시지를 채팅방 상단에 항상 표시되도록 고정하는 기능입니다. 관리자나 권한이 있는 사용자가 중요한 메시지를 선택하여 고정할 수 있습니다.

실무에서 이 기능은 그룹 채팅에서 필수적입니다. Telegram에서는 여러 개의 메시지를 고정할 수 있고, WhatsApp에서는 하나만 고정할 수 있습니다.

프로젝트 채팅방에서 작업 마감일을 고정하거나, 커뮤니티 채팅방에서 규칙을 고정하거나, 여행 계획 채팅방에서 일정표를 고정하는 등 다양하게 활용됩니다. 기존에는 중요한 정보를 찾기 위해 스크롤을 올리거나 검색해야 했다면, 이제는 채팅방을 열자마자 바로 확인할 수 있습니다.

새로 들어온 사람도 고정된 메시지를 먼저 보게 되어, 같은 질문을 반복하는 것을 줄일 수 있습니다. 이 기능의 핵심 특징은 다음과 같습니다.

첫째, 권한 관리로 관리자나 특정 역할을 가진 사용자만 고정할 수 있습니다. 둘째, 고정 개수 제한으로 너무 많은 메시지가 고정되어 화면이 복잡해지는 것을 방지합니다.

셋째, 고정 알림으로 메시지가 고정되거나 해제될 때 참여자들에게 알립니다. 이러한 특징들이 효과적인 정보 공유를 가능하게 합니다.

코드 예제

// 메시지 고정 기능 구현
interface PinnedMessage {
  messageId: string;
  pinnedBy: string;
  pinnedAt: Date;
  order: number; // 여러 메시지 고정 시 순서
}

interface ChatRoom {
  id: string;
  pinnedMessages: PinnedMessage[];
  maxPinnedCount: number; // 최대 고정 개수
}

// 메시지 고정 함수
async function pinMessage(
  roomId: string,
  messageId: string,
  userId: string
): Promise<PinnedMessage[]> {
  const room = await getChatRoom(roomId);

  // 권한 확인 (관리자만 가능)
  const hasPermission = await checkAdminPermission(roomId, userId);
  if (!hasPermission) {
    throw new Error('메시지를 고정할 권한이 없습니다');
  }

  // 최대 고정 개수 확인
  if (room.pinnedMessages.length >= room.maxPinnedCount) {
    throw new Error(`최대 ${room.maxPinnedCount}개까지만 고정할 수 있습니다`);
  }

  // 이미 고정되어 있는지 확인
  const alreadyPinned = room.pinnedMessages.some(
    p => p.messageId === messageId
  );
  if (alreadyPinned) {
    throw new Error('이미 고정된 메시지입니다');
  }

  // 메시지 고정
  const pinnedMessage: PinnedMessage = {
    messageId,
    pinnedBy: userId,
    pinnedAt: new Date(),
    order: room.pinnedMessages.length
  };

  room.pinnedMessages.push(pinnedMessage);
  await saveChatRoom(room);

  // 고정 알림 전송
  await broadcastMessagePinned(roomId, messageId);

  return room.pinnedMessages;
}

// 메시지 고정 해제 함수
async function unpinMessage(
  roomId: string,
  messageId: string,
  userId: string
): Promise<PinnedMessage[]> {
  const room = await getChatRoom(roomId);

  // 권한 확인
  const hasPermission = await checkAdminPermission(roomId, userId);
  if (!hasPermission) {
    throw new Error('메시지를 고정 해제할 권한이 없습니다');
  }

  // 고정 해제
  room.pinnedMessages = room.pinnedMessages.filter(
    p => p.messageId !== messageId
  );

  // 순서 재정렬
  room.pinnedMessages.forEach((p, index) => {
    p.order = index;
  });

  await saveChatRoom(room);
  await broadcastMessageUnpinned(roomId, messageId);

  return room.pinnedMessages;
}

설명

이것이 하는 일: 메시지 고정 기능은 중요한 메시지를 채팅방 상단에 항상 표시하여, 참여자들이 핵심 정보를 놓치지 않도록 합니다. 첫 번째로, PinnedMessage와 ChatRoom 인터페이스가 고정 메시지의 구조를 정의합니다.

PinnedMessage는 어떤 메시지가(messageId) 누구에 의해(pinnedBy) 언제(pinnedAt) 고정되었는지 기록합니다. order 필드는 여러 메시지가 고정되었을 때 표시 순서를 결정합니다.

ChatRoom의 maxPinnedCount는 고정 가능한 최대 개수를 제한하여, 너무 많은 메시지가 고정되어 화면을 차지하는 것을 방지합니다. 그 다음으로, pinMessage 함수가 실행되면서 여러 검증을 수행합니다.

먼저 checkAdminPermission으로 권한을 확인하여, 아무나 메시지를 고정할 수 없게 합니다. 단체 채팅방에서 모든 사람이 고정할 수 있다면 혼란스러워질 것입니다.

그 다음 최대 개수를 확인하여, 예를 들어 3개까지만 고정 가능하도록 제한합니다. 마지막으로 이미 고정된 메시지인지 중복 체크합니다.

세 번째 단계에서는 검증을 통과하면 PinnedMessage 객체를 생성하고 room.pinnedMessages 배열에 추가합니다. order는 현재 배열의 길이로 설정하여, 나중에 고정된 메시지일수록 아래에 표시됩니다.

이렇게 하면 가장 최근에 고정된 중요 정보가 위에 오게 됩니다. 마지막으로, broadcastMessagePinned로 모든 참여자에게 메시지가 고정되었음을 알립니다.

사용자들의 화면에서 즉시 고정 메시지 영역이 업데이트되어, "관리자가 메시지를 고정했습니다" 같은 알림과 함께 표시됩니다. unpinMessage 함수는 반대로 고정을 해제합니다.

메시지를 배열에서 제거한 후, forEach로 남은 메시지들의 order를 재정렬합니다. 중간에 하나가 제거되면 순서가 0, 1, 3, 4 같이 빈 번호가 생기는데, 이를 0, 1, 2, 3으로 다시 정리하는 것입니다.

여러분이 이 코드를 사용하면 체계적인 메시지 고정 시스템을 만들 수 있습니다. 권한 관리로 무분별한 고정을 방지하고, 개수 제한으로 UI를 깔끔하게 유지하며, 순서 관리로 여러 고정 메시지를 효과적으로 표시할 수 있습니다.

실전 팁

💡 고정된 메시지 영역을 접을 수 있게 만드세요. 사용자가 X 버튼을 눌러 임시로 숨길 수 있으면, 화면 공간을 효율적으로 사용할 수 있습니다.

💡 고정 메시지를 클릭하면 원본 메시지 위치로 스크롤되게 하세요. 전체 맥락을 이해하는 데 도움이 됩니다.

💡 고정 메시지 개수를 채팅방 크기나 용도에 따라 다르게 설정하세요. 소규모 팀 채팅은 1개, 대규모 커뮤니티는 5개 같은 식으로 차등화합니다.

💡 고정된 메시지가 수정되거나 삭제되면 자동으로 고정 해제되도록 하세요. 더 이상 유효하지 않은 정보가 고정되어 있는 것을 방지합니다.

💡 고정 메시지 영역에 아이콘(📌)을 표시하여 시각적으로 구분하세요. 사용자가 일반 메시지와 쉽게 구분할 수 있습니다.


5. 답장 (Reply) 기능

시작하며

여러분이 활발한 단체 채팅방에서 특정 메시지에 대답하고 싶은데, 다른 사람들이 계속 메시지를 보내서 맥락이 끊긴 경험이 있나요? 30개의 메시지가 지나간 후 "네, 동의합니다"라고 쓰면 무엇에 동의하는 건지 아무도 모릅니다.

답장 기능이 없으면 대화가 혼란스러워집니다. 여러 사람이 동시에 다른 주제로 이야기하는 단체 채팅에서는 특히 더 심각합니다.

"@홍길동 아까 그 말은..." 같이 컨텍스트를 설명하느라 메시지가 길어지고, 그래도 정확히 어떤 메시지를 말하는 건지 불명확합니다. 바로 이럴 때 필요한 것이 답장(Reply) 기능입니다.

특정 메시지를 인용하여 답장하면, 대화의 맥락이 명확하게 유지됩니다.

개요

간단히 말해서, 답장 기능은 특정 메시지에 대한 응답임을 명확히 표시하는 기능입니다. 사용자가 메시지를 길게 눌러 '답장' 옵션을 선택하면, 원본 메시지의 일부가 인용되어 표시됩니다.

실무에서 이 기능은 대화의 흐름을 유지하는 핵심입니다. Telegram, WhatsApp, Slack 모두 이 기능을 제공하며, 사용자들은 이를 자연스럽게 활용합니다.

특히 비동기 커뮤니케이션에서 중요한데, 몇 시간 또는 며칠 후에 답장을 해도 어떤 메시지에 대한 답인지 명확하게 전달됩니다. 기존에는 "@사용자명" 같은 멘션으로 대상을 지정했다면, 이제는 메시지 자체를 직접 연결할 수 있습니다.

멘션은 사람을 지정하지만, 답장은 특정 메시지를 지정합니다. "네, 동의합니다" 대신 "홍길동: 내일 회의 어때요?

네, 동의합니다" 같은 형태로 맥락이 보존됩니다. 이 기능의 핵심 특징은 다음과 같습니다.

첫째, 메시지 스레딩으로 대화의 맥락을 시각적으로 연결합니다. 둘째, 원본 메시지 바로가기로 답장을 클릭하면 원본 위치로 이동합니다.

셋째, 중첩 답장 지원으로 답장에 대한 답장도 가능합니다. 이러한 특징들이 복잡한 대화에서도 명확한 커뮤니케이션을 가능하게 합니다.

코드 예제

// 답장(Reply) 기능 구현
interface ReplyInfo {
  replyToMessageId: string;
  replyToUserId: string;
  replyToContent: string; // 원본 메시지 미리보기 (처음 50자)
}

interface MessageWithReply {
  id: string;
  content: string;
  senderId: string;
  replyTo?: ReplyInfo;
  createdAt: Date;
}

// 답장 메시지 생성 함수
async function sendReplyMessage(
  roomId: string,
  senderId: string,
  content: string,
  replyToMessageId: string
): Promise<MessageWithReply> {
  // 답장 대상 메시지 가져오기
  const originalMessage = await getMessageById(replyToMessageId);

  if (!originalMessage) {
    throw new Error('답장하려는 메시지를 찾을 수 없습니다');
  }

  // 원본 메시지 미리보기 생성 (처음 50자)
  const previewContent = originalMessage.content.length > 50
    ? originalMessage.content.substring(0, 50) + '...'
    : originalMessage.content;

  // 답장 정보 생성
  const replyInfo: ReplyInfo = {
    replyToMessageId: originalMessage.id,
    replyToUserId: originalMessage.senderId,
    replyToContent: previewContent
  };

  // 새 메시지 생성
  const newMessage: MessageWithReply = {
    id: generateMessageId(),
    content,
    senderId,
    replyTo: replyInfo,
    createdAt: new Date()
  };

  // 메시지 저장 및 브로드캐스트
  await saveMessage(roomId, newMessage);
  await broadcastMessage(roomId, newMessage);

  // 답장받은 사용자에게 알림
  if (originalMessage.senderId !== senderId) {
    await sendNotification(originalMessage.senderId, {
      type: 'reply',
      message: `${senderId}님이 답장했습니다: ${content}`
    });
  }

  return newMessage;
}

// 답장 체인 조회 함수
async function getReplyChain(messageId: string): Promise<MessageWithReply[]> {
  const chain: MessageWithReply[] = [];
  let currentMessage = await getMessageById(messageId);

  // 원본 메시지까지 역추적
  while (currentMessage) {
    chain.unshift(currentMessage);

    if (currentMessage.replyTo) {
      currentMessage = await getMessageById(currentMessage.replyTo.replyToMessageId);
    } else {
      break;
    }
  }

  return chain;
}

설명

이것이 하는 일: 답장 기능은 특정 메시지에 대한 응답을 명확하게 연결하여, 대화의 맥락을 보존하고 사용자 간 소통을 원활하게 합니다. 첫 번째로, ReplyInfo와 MessageWithReply 인터페이스가 답장 구조를 정의합니다.

ReplyInfo는 어떤 메시지에(replyToMessageId) 대한 답장인지, 원작성자가 누구인지(replyToUserId), 원본 내용의 미리보기는 무엇인지(replyToContent) 저장합니다. MessageWithReply는 일반 메시지 정보에 replyTo 필드를 추가하여, 이것이 답장인지 일반 메시지인지 구분합니다.

replyTo가 undefined면 일반 메시지이고, 값이 있으면 답장입니다. 그 다음으로, sendReplyMessage 함수가 실행되면서 먼저 답장 대상 메시지를 조회합니다.

메시지가 이미 삭제되었거나 존재하지 않으면 에러를 발생시켜, 유효하지 않은 답장을 방지합니다. 그 다음 원본 메시지의 미리보기를 생성하는데, 처음 50자만 잘라서 저장합니다.

전체 내용을 저장하면 데이터베이스 용량이 낭비되고, UI에서도 너무 길게 표시됩니다. "안녕하세요.

내일 회의는 오후 3시에 진행하면 어떨까요?" 같은 메시지는 "안녕하세요. 내일 회의는 오후 3시에 진행하면 어떨까..."로 표시됩니다.

세 번째 단계에서는 ReplyInfo 객체를 생성하고 새 메시지에 포함시킵니다. 이렇게 하면 메시지가 저장될 때 답장 관계가 함께 저장되어, 나중에 조회할 때 원본 메시지와 연결할 수 있습니다.

데이터베이스에서는 외래 키 관계처럼 replyToMessageId로 연결되어 있습니다. 네 번째로, 메시지를 저장하고 브로드캐스트한 후 알림을 보냅니다.

중요한 점은 자기 자신에게는 알림을 보내지 않는다는 것입니다(originalMessage.senderId !== senderId 조건). 다른 사람이 내 메시지에 답장하면 "홍길동님이 답장했습니다: 좋은 생각이네요!" 같은 푸시 알림을 받게 됩니다.

마지막으로, getReplyChain 함수는 답장의 전체 맥락을 추적합니다. A 메시지에 B가 답장하고, B 메시지에 C가 답장했다면, C 메시지에서 이 함수를 호출하면 [A, B, C] 순서로 전체 대화 체인을 반환합니다.

while 루프로 replyTo를 따라 거슬러 올라가며, unshift로 배열 앞에 추가하여 시간 순서대로 정렬합니다. 여러분이 이 코드를 사용하면 명확한 대화 맥락을 유지할 수 있습니다.

사용자는 어떤 메시지에 대한 답인지 한눈에 알 수 있고, 원본 메시지로 바로 이동할 수 있으며, 복잡한 대화에서도 스레드를 추적할 수 있습니다.

실전 팁

💡 답장 메시지를 클릭하면 원본 메시지 위치로 부드럽게 스크롤되게 하세요. 스크롤 애니메이션과 하이라이트 효과로 원본을 쉽게 찾을 수 있습니다.

💡 답장 미리보기에 이미지나 파일 아이콘을 표시하세요. "사진에 답장", "문서에 답장" 같은 시각적 힌트가 이해를 돕습니다.

💡 답장 깊이를 제한하세요. A→B→C→D→E처럼 너무 깊어지면 UI가 복잡해지므로, 3단계 정도로 제한하고 그 이상은 스레드 기능을 권장하세요.

💡 답장 작성 중에 원본 메시지 미리보기를 하단에 표시하여, 사용자가 무엇에 답장하는지 확인하면서 작성할 수 있게 하세요.

💡 답장을 취소하는 X 버튼을 명확히 제공하세요. 실수로 잘못된 메시지를 선택한 경우 쉽게 취소할 수 있어야 합니다.


6. 전달 (Forward) 기능

시작하며

여러분이 한 채팅방에서 받은 유용한 정보나 재미있는 콘텐츠를 다른 채팅방에 공유하고 싶었던 적이 있나요? 메시지를 복사해서 붙여넣기 하는 것은 번거롭고, 이미지나 파일은 다시 다운로드하고 업로드해야 합니다.

전달 기능이 없으면 콘텐츠 공유가 비효율적입니다. 긴 메시지를 일일이 복사하거나, 여러 개의 메시지를 하나씩 옮기는 것은 시간이 오래 걸립니다.

또한 원본 출처 정보가 사라져서, 나중에 누가 처음 말한 건지 알 수 없게 됩니다. 바로 이럴 때 필요한 것이 전달(Forward) 기능입니다.

클릭 몇 번으로 메시지를 다른 채팅방에 그대로 전달하여, 빠르고 정확한 정보 공유가 가능합니다.

개요

간단히 말해서, 전달 기능은 한 채팅방의 메시지를 다른 채팅방으로 복사하여 보내는 기능입니다. 사용자가 메시지를 선택하고 '전달' 옵션을 누른 후, 대상 채팅방을 선택하면 됩니다.

실무에서 이 기능은 정보 확산에 핵심적입니다. WhatsApp에서는 최대 5개 채팅방까지 동시 전달이 가능하고, Telegram에서는 무제한으로 전달할 수 있습니다.

뉴스 기사, 이벤트 정보, 업무 자료 등을 여러 팀에 빠르게 공유할 때 유용합니다. 다만 가짜 뉴스 확산 문제로 일부 플랫폼은 전달 횟수를 제한하기도 합니다.

기존에는 메시지를 복사-붙여넣기 하거나, 스크린샷을 찍어 보내야 했다면, 이제는 원본 형식을 유지하면서 즉시 전달할 수 있습니다. 텍스트뿐만 아니라 이미지, 동영상, 문서 파일도 모두 원본 품질 그대로 전달됩니다.

이 기능의 핵심 특징은 다음과 같습니다. 첫째, 일괄 전달로 여러 메시지를 한 번에 선택하여 전달할 수 있습니다.

둘째, 다중 대상 전달로 하나의 메시지를 여러 채팅방에 동시에 보낼 수 있습니다. 셋째, 전달 표시로 "전달된 메시지"라는 라벨이 붙어 출처를 구분할 수 있습니다.

이러한 특징들이 효율적인 정보 공유를 가능하게 하면서도, 메시지의 출처를 명확히 합니다.

코드 예제

// 전달(Forward) 기능 구현
interface ForwardInfo {
  originalMessageId: string;
  originalSenderId: string;
  originalRoomId: string;
  forwardedBy: string;
  forwardedAt: Date;
  forwardCount: number; // 전달 횟수 (체인)
}

interface ForwardedMessage extends MessageWithReply {
  isForwarded: boolean;
  forwardInfo?: ForwardInfo;
}

// 메시지 전달 함수
async function forwardMessage(
  originalMessageId: string,
  fromRoomId: string,
  toRoomIds: string[], // 여러 채팅방에 전달 가능
  forwardedBy: string
): Promise<ForwardedMessage[]> {
  // 원본 메시지 가져오기
  const originalMessage = await getMessageById(originalMessageId);

  if (!originalMessage) {
    throw new Error('전달하려는 메시지를 찾을 수 없습니다');
  }

  // 전달 횟수 제한 (가짜 뉴스 방지)
  const maxForwardCount = 5;
  const currentForwardCount = originalMessage.forwardInfo?.forwardCount || 0;

  if (currentForwardCount >= maxForwardCount) {
    throw new Error(`이 메시지는 ${maxForwardCount}번 전달되어 더 이상 전달할 수 없습니다`);
  }

  // 전달 대상 개수 제한
  if (toRoomIds.length > 5) {
    throw new Error('한 번에 최대 5개 채팅방까지 전달할 수 있습니다');
  }

  const forwardedMessages: ForwardedMessage[] = [];

  // 각 채팅방에 전달
  for (const toRoomId of toRoomIds) {
    // 권한 확인 (전달하려는 채팅방의 멤버인지)
    const isMember = await checkRoomMembership(toRoomId, forwardedBy);
    if (!isMember) {
      console.warn(`${forwardedBy}${toRoomId}의 멤버가 아닙니다`);
      continue;
    }

    // 전달 정보 생성
    const forwardInfo: ForwardInfo = {
      originalMessageId: originalMessage.id,
      originalSenderId: originalMessage.senderId,
      originalRoomId: fromRoomId,
      forwardedBy,
      forwardedAt: new Date(),
      forwardCount: currentForwardCount + 1
    };

    // 전달된 메시지 생성 (내용은 복사, 메타데이터는 새로 생성)
    const forwardedMessage: ForwardedMessage = {
      id: generateMessageId(),
      content: originalMessage.content,
      senderId: forwardedBy, // 전달한 사람이 발신자
      isForwarded: true,
      forwardInfo,
      createdAt: new Date()
    };

    // 첨부 파일도 함께 복사
    if (originalMessage.attachments) {
      forwardedMessage.attachments = originalMessage.attachments;
    }

    // 메시지 저장 및 브로드캐스트
    await saveMessage(toRoomId, forwardedMessage);
    await broadcastMessage(toRoomId, forwardedMessage);

    forwardedMessages.push(forwardedMessage);
  }

  return forwardedMessages;
}

// 전달 체인 추적 함수
async function getForwardChain(messageId: string): Promise<ForwardedMessage[]> {
  const chain: ForwardedMessage[] = [];
  let currentMessage = await getMessageById(messageId);

  // 원본 메시지까지 역추적
  while (currentMessage) {
    chain.unshift(currentMessage);

    if (currentMessage.forwardInfo) {
      currentMessage = await getMessageById(
        currentMessage.forwardInfo.originalMessageId
      );
    } else {
      break; // 원본 메시지 도달
    }
  }

  return chain;
}

설명

이것이 하는 일: 전달 기능은 메시지를 다른 채팅방으로 빠르게 공유할 수 있게 해주며, 전달 이력을 추적하고 악용을 방지합니다. 첫 번째로, ForwardInfo와 ForwardedMessage 인터페이스가 전달 메시지의 구조를 정의합니다.

ForwardInfo는 원본 메시지의 모든 정보(어디서, 누가, 언제 보낸 메시지인지)와 전달 정보(누가, 언제 전달했는지)를 저장합니다. 특히 forwardCount는 메시지가 몇 번이나 전달되었는지 추적하여, 가짜 뉴스나 스팸이 무한히 퍼지는 것을 방지하는 중요한 필드입니다.

ForwardedMessage는 일반 메시지에 isForwarded 플래그와 forwardInfo를 추가하여, UI에서 전달된 메시지임을 표시할 수 있게 합니다. 그 다음으로, forwardMessage 함수가 실행되면서 여러 검증을 수행합니다.

먼저 원본 메시지가 존재하는지 확인하고, 전달 횟수가 제한(예: 5번)을 초과하지 않았는지 체크합니다. WhatsApp은 코로나19 팬데믹 때 가짜 뉴스 확산을 막기 위해 이런 제한을 도입했습니다.

또한 한 번에 전달할 수 있는 채팅방 개수도 제한하여(예: 5개) 스팸을 방지합니다. 세 번째 단계에서는 toRoomIds 배열을 순회하며 각 채팅방에 메시지를 전달합니다.

중요한 점은 각 채팅방마다 권한을 확인한다는 것입니다. 사용자가 멤버가 아닌 채팅방에는 전달할 수 없게 하여 보안을 유지합니다.

만약 5개 중 2개 채팅방에 권한이 없다면, 나머지 3개에만 전달되고 에러 없이 계속 진행됩니다(console.warn으로 로그만 남김). 네 번째로, 각 채팅방마다 새로운 ForwardedMessage를 생성합니다.

중요한 점은 content는 원본을 복사하지만, id와 createdAt은 새로 생성한다는 것입니다. 이렇게 하면 각 채팅방에서 독립적인 메시지로 취급되어, 한 채팅방에서 삭제해도 다른 채팅방에는 영향을 주지 않습니다.

senderId는 forwardedBy로 설정되어, 전달한 사람이 발신자로 표시됩니다. attachments(첨부 파일)도 함께 복사하여, 이미지나 문서가 포함된 메시지도 온전히 전달됩니다.

마지막으로, getForwardChain 함수는 전달 이력을 추적합니다. A→B→C처럼 메시지가 여러 번 전달되었다면, 최종 메시지에서 이 함수를 호출하면 [A(원본), B(1차 전달), C(2차 전달)] 전체 체인을 반환합니다.

이는 메시지의 출처를 투명하게 확인하고, 정보의 신뢰성을 판단하는 데 도움이 됩니다. 여러분이 이 코드를 사용하면 안전하고 효율적인 메시지 전달 시스템을 만들 수 있습니다.

사용자는 클릭 몇 번으로 여러 채팅방에 정보를 공유하고, 시스템은 전달 횟수와 권한을 체크하여 악용을 방지하며, 전달 표시로 원본과 전달된 메시지를 구분할 수 있습니다.

실전 팁

💡 전달 시 코멘트를 추가할 수 있는 옵션을 제공하세요. "이거 유용할 것 같아서 전달합니다"처럼 짧은 메모를 붙일 수 있으면 맥락 전달에 도움이 됩니다.

💡 전달된 메시지에는 시각적 표시를 명확히 하세요. 회색 배경이나 "↪ 전달됨" 아이콘으로 일반 메시지와 구분하면 사용자가 출처를 인식할 수 있습니다.

💡 전달 전에 미리보기 화면을 제공하세요. "다음 채팅방에 전달됩니다: [채팅방1], [채팅방2]..." 확인 단계로 실수를 방지합니다.

💡 민감한 정보가 포함된 메시지는 전달을 차단하거나 경고를 표시하세요. 비밀번호, 신용카드 번호 같은 패턴을 감지하여 사용자를 보호합니다.

💡 전달 통계를 제공하세요. "이 메시지는 15번 전달되었습니다" 같은 정보로, 인기 있는 콘텐츠인지 스팸인지 판단할 수 있게 합니다.


#TypeScript#MessageFeatures#ChatSystem#UIComponents#EventHandling#Message,Features,Advanced

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

그룹 채팅 고급 기능 완벽 가이드

그룹 채팅 애플리케이션에서 사용자 경험을 극대화하는 필수 고급 기능들을 배워봅니다. 공지사항 관리부터 멤버 멘션, 고정 메시지까지 실무에서 바로 적용 가능한 구현 방법을 초급자도 이해할 수 있게 설명합니다.

그룹 채팅 메시지 및 권한 관리 완벽 가이드

실시간 그룹 채팅에서 메시지 브로드캐스트부터 읽음 상태 관리, 관리자와 일반 멤버의 권한 설정까지 완벽하게 구현하는 방법을 배워봅니다. 실무에서 바로 사용할 수 있는 권한 검증 미들웨어까지 포함된 완벽한 가이드입니다.

그룹 멤버 관리 시스템 완벽 가이드

초대부터 강퇴까지, 실시간 그룹 채팅 앱의 멤버 관리 시스템을 완벽하게 구현하는 방법을 배워봅니다. 멤버 초대, 권한 관리, 온라인 상태 표시까지 실무에 바로 적용할 수 있는 핵심 기능들을 다룹니다.

그룹 채팅방 생성 및 관리 완벽 가이드

실시간 그룹 채팅 애플리케이션을 만들 때 필요한 핵심 기능들을 단계별로 배워봅니다. API 설계부터 Socket.io를 활용한 실시간 통신, 그룹 관리 기능까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.

실시간 채팅의 핵심 읽음 표시와 타이핑 인디케이터 완벽 가이드

카카오톡이나 슬랙처럼 메시지를 읽었는지 표시하고, 상대방이 타이핑 중인지 보여주는 기능을 직접 구현해보세요. 실시간 소켓 통신부터 상태 관리, 성능 최적화까지 실무에서 바로 사용할 수 있는 모든 것을 담았습니다.