Skip to content

[FE] 채팅 개발 시리즈

Jungmin edited this page Dec 14, 2023 · 1 revision

채팅-1 채팅을 어떻게 저장할까?

사용자가 입장할 때마다 모든 채팅을 다시 처음부터 불러오면 사용자 입장에서 불편함을 느낄 수 있다. 따라서 채팅을 전역상태로 관리하기로 결정했다. 우선 채팅데이터를 저장할 chatLogAtom이라는 atom을 하나 생성하자. 구조는 아래와 같다. 즉, key는 각 방의 고유 id며 value는 채팅 데이터다.

고려할 점

  • 채팅로그는 고유한 값을 가지며, 혹시나 과거 데이터를 불러오거나 현재 데이터를 불러와 저장할 떄 중복해서 채팅로그가 저장되더라도 데이터 무결성이 위반돼서는 안된다.
  • 채팅로그가 저장된 순서를 보장할 수 있어야한다.
  • 중복 제거와 정렬을 하기위한 시간복잡도가 커서는 안된다. (다른, 기능을 위한 로직을 처리하기위해)

따라서 key는 logId로해서 중복 제거를 자동을 해주고, 순서 보장을 해주는 Map 자료구조를 사용하기로 했다.

export const chatLogAtom = atom<Record<string, Map<string, ChatLog>>>({
  key: 'chatLogAtom',
  default: {},
});

export interface ChatLog {
  message: string;
  userId: string;
  type: 'message' | 'in' | 'out' | 'kick';
  time: Date;
}

즉, chatLogAtom에 저장되는 예시는 아래와 같다.

{
	190305 : new Map([
    [
        "65728511bb2a8dabf82b6768",
        {
            "message": "fdfs님이 입장하셨습니다.",
            "type": "in",
            "time": "2023-12-08T02:53:05.585Z"
        }
    ],
    [
        "65728516bb2a8dabf82b6796",
        {
            "message": "재밌나요",
            "userId": "65728511bb2a8dabf82b676a",
            "type": "message",
            "time": "2023-12-08T02:53:10.950Z"
        }
    ],
		])
		,

	10205: new Map([
    [
        "65728511bb2a8dabf82b6768",
        {
            "message": "fdfs님이 입장하셨습니다.",
            "type": "in",
            "time": "2023-12-08T02:53:05.585Z"
        }
    ],
    [
        "65728516bb2a8dabf82b6796",
        {
            "message": "재밌나요",
            "userId": "65728511bb2a8dabf82b676a",
            "type": "message",
            "time": "2023-12-08T02:53:10.950Z"
        }
    ],
		])

}

type은 4개의 경우가 있다. 아래와 같은 4가지의 타입으로 나눈 이유는 message와 다르게 나머지 3개의 이벤트가 발생했을 때는 다른 UI를 보여주도록 하기 위해서다.

message | 사용자가 입력한 채팅 -- | -- in | 사용자 입장 out | 사용자 퇴장 kick | 사용자 강퇴

예시는 다음과 같다.

vv PNG

채팅-2 읽지 않은 사람 수를 어떻게 계산할까?

처음 생각한 해결 방안

각 채팅마다 읽지 않은 인원리스트를 DB에 저장하면 되지 않을까?

문제점

특정 유저가 10,000개의 채팅을 읽지 않은 상태로 방에 접속하면 DB는 10,000개의 채팅에 대해 데이터를 갱신해야 한다. 이후, 프론트엔드는 갱신된 데이터를 다시 받아 렌더링해야한다.

다른 방안을 생각해보자..

이러한 고민을 멘토링을 진행하면서, 결국 채팅 데이터를 저장할 때 순서가 보장되면서 고유한 값을 채팅의 key로 저장해야한다는 생각이 들었다. 따라서, 다음과 같이 채팅에 logId를 부여하기로 백엔드에서 결정을 했다. Untitled (6)

백엔드에서 안읽은 사람수를 계산해서 프론트에 넘겨주는 과정은 아래의 링크에서 확인해볼 수 있다. 안읽은 사람수 계산하기

백엔드에서 전달해준 데이터로 안읽은 사람 수 계산하기

자신을 포함한 다른 사용자가 입장하거나 나갈 때마다 unread라는 socket 이벤트를 받는다. unread이벤트의 응답은 다음과 같다.

key | value(채팅의 logId) -- | -- 1 | 6578402a51a926a577b8cfaf 2 | 6578403651a926a577b8cfd9 위의 데이터를 가지고 어떻게 모든 채팅의 안 읽은 사람 수를 계산할 수 있을까?? 위의 데이터를 해석하자면 1명이 안 읽은 채팅 logId는 6578402a51a926a577b8cfaf 이후의 채팅이다. 2명이 안 읽은 채팅 logId는 6578403651a926a577b8cfd9 이후의 채팅이다. 이를 범위로 나타내면 다음과 같다. ![Untitled (7)](https://github.com/boostcampwm2023/web03-LockFestival/assets/101504594/14eb4393-341e-4bd1-a7fb-e1cf12e5338a)

⇒ logId는 대소비교가 가능하다. 즉, 과거 채팅의 logId보다 현재 채팅의 logId가 반드시 크다.

따라서, 로그아이디를 unread 데이터의 value와 비교하면서 다음과 같이 안읽은 사람수를 계산할 수 있다. Untitled (8) 계산하는 로직은 다음과 같다.

반복문을 통해 자신의 logId보다 크거나 같은 logId를 unread 배열에서 찾으면 된다.

만약 unread 배열의 모든 logId보다 자신이 크다면 자신을 제외한 모든 인원이 읽지 않은 것이므로 unread 배열의 가장 마지막 unreadCount를 보여주면 된다.

const unreadCount = useCallback(
  (logId: string) => {
    if (chatUnreadSortArray.length === 0) {
      return 0;
    }
  // 자신보다 크거나 같은 logId를 찾을 때 까지 반복
    for (let i = 0; i < chatUnreadSortArray.length; i++) {
      if (logId <= chatUnreadSortArray[i][1]) {
        return i === 0 ? 0 : Number(chatUnreadSortArray[i - 1][0]);
      }
    }

    return Number(chatUnreadSortArray[chatUnreadSortArray.length - 1][0]);
  },
  [chatUnread]
);

트러블 슈팅

처음의 코드는 아래와 같았다. 하지만 해당 로직에는 엄청난 문제가 있었다. 자신보다 크거나 같은 logId를 찾을 때 까지 반복하는데 찾으면 해당 logId의 unreadCount에서 단순히 1을 뺀 값을 보여줬다.

const unreadCount = useMemo(() => {
    if (!chatUnread) {
      return 0;
    }
    const mapArr = new Array(...chatUnread);

    for (const [key, value] of chatUnread) {
      if (logId <= value) {
        return Number(key) - 1;
      }
    }
    return Number([mapArr.length - 1][0] + 1);
  }, [chatUnread]);

왜 문제가 되는가? 4명이 있는 채팅방이라고 가정을 하자.

읽은 사람 | 메세지 -- | -- 모두 읽음 | hi 모두 읽음 | good 2명만 읽음 | great 2명만 읽음 | gigi 2명만 읽음 | bbbb

채팅방 상황이 위와 같다면 unread 배열은 다음과 같아질 것이다.

즉, 1명만 안 읽은 상황은 없다. 하지만 단순히 2 - 1을 해서 1을 보여주는 문제가 있었다.

{
    "2": "a9"
}

따라서 단순히 해당 logId의 unreadCount - 1이 아니라 앞의 요소를 보여주도록 변경했다.

채팅-1 채팅을 어떻게 저장할까?

사용자가 입장할 때마다 모든 채팅을 다시 처음부터 불러오면 사용자 입장에서 불편함을 느낄 수 있다.

따라서 채팅을 전역상태로 관리하기로 결정했다.

우선 채팅데이터를 저장할 chatLogAtom이라는 atom을 하나 생성하자. 구조는 아래와 같다.

즉, key는 각 방의 고유 id며 value는 채팅 데이터다.

고려할 점

  • 채팅로그는 고유한 값을 가지며, 혹시나 과거 데이터를 불러오거나 현재 데이터를 불러와 저장할 떄 중복해서 채팅로그가 저장되더라도 데이터 무결성이 위반돼서는 안된다.
  • 채팅로그가 저장된 순서를 보장할 수 있어야한다.
  • 중복 제거와 정렬을 하기위한 시간복잡도가 커서는 안된다. (다른, 기능을 위한 로직을 처리하기위해)

따라서 key는 logId로해서 중복 제거를 자동을 해주고, 순서 보장을 해주는 Map 자료구조를 사용하기로 했다.

export const chatLogAtom = atom<Record<string, Map<string, ChatLog>>>({
  key: 'chatLogAtom',
  default: {},
});

export interface ChatLog { message: string; userId: string; type: 'message' | 'in' | 'out' | 'kick'; time: Date; }

즉, chatLogAtom에 저장되는 예시는 아래와 같다.

{
	190305 : new Map([
    [
        "65728511bb2a8dabf82b6768",
        {
            "message": "fdfs님이 입장하셨습니다.",
            "type": "in",
            "time": "2023-12-08T02:53:05.585Z"
        }
    ],
    [
        "65728516bb2a8dabf82b6796",
        {
            "message": "재밌나요",
            "userId": "65728511bb2a8dabf82b676a",
            "type": "message",
            "time": "2023-12-08T02:53:10.950Z"
        }
    ],
		])
		,
10205: new Map([
[
    &quot;65728511bb2a8dabf82b6768&quot;,
    {
        &quot;message&quot;: &quot;fdfs님이 입장하셨습니다.&quot;,
        &quot;type&quot;: &quot;in&quot;,
        &quot;time&quot;: &quot;2023-12-08T02:53:05.585Z&quot;
    }
],
[
    &quot;65728516bb2a8dabf82b6796&quot;,
    {
        &quot;message&quot;: &quot;재밌나요&quot;,
        &quot;userId&quot;: &quot;65728511bb2a8dabf82b676a&quot;,
        &quot;type&quot;: &quot;message&quot;,
        &quot;time&quot;: &quot;2023-12-08T02:53:10.950Z&quot;
    }
],
	])

}

type은 4개의 경우가 있다. 아래와 같은 4가지의 타입으로 나눈 이유는 message와 다르게 나머지 3개의 이벤트가 발생했을 때는 다른 UI를 보여주도록 하기 위해서다.

message 사용자가 입력한 채팅
in 사용자 입장
out 사용자 퇴장
kick 사용자 강퇴

예시는 다음과 같다.

Lock Festival 🔒

Rules

개발일지

Description

학습 노트

회의록

사전 회의
1주차 회의록
2주차 회의록
3주차 회의록
4주차 회의록
5주차 회의록
6주차 회의록

데일리 스크럼

1주차
2주차
3주차
4주차
5주차
6주차

회고록

1주차 회고록
2주차 회고록
3주차 회고록
4주차 회고록
5주차 회고록
6주차 회고록

스프린트

멘토링 일지

Clone this wiki locally