채팅방에서 읽지 않은 인원 수 계산하기-2

navyjeongs·2024년 1월 27일
0

리액트

목록 보기
7/7
post-thumbnail

채팅방에서 읽지 않은 인원 수 계산하기-1와 이어집니다.

앞의 게시글에서 각 채팅에 대해 읽지 않은 인원 수를 계산하는 아이디어를 떠올렸다.

유저별 로그 ID로 전체 채팅의 읽지 않은 인원 수 계산하기

이제 백엔드에서 어떻게 프론트에게 값을 넘겨주는지 확인하겠다.(백엔드 부분은 내가 작성한 로직이 아니라 간단히 설명하겠다.)

일단 DB에서 roomID를 통해서 해당 채팅방의 유저 정보를 가져온다. 그 후, makeUnreadCountMap 메서드에 chatUsers를 넘겨준다.

각 유저 정보는 ChatUserInfoDto의 형식이다.

DTO


export class ChatUserInfoDto {
  userId: string; // 유저 고유 id
  nickname: string; // 유저 닉네임
  profileImg: string; // 유저 프로필 이미지 주소
  isLeader: boolean; // 방장 여부
  isLeave: boolean; // 방을 나갔는지 여부
  lastChatLogId: string; // 마지막으로 읽은 채팅의 logID
}

async getUnreadCount(roomId: string) {
  const chatUsers: ChatUserInfoDto[] = await this.chatRepository.findUserListByRoomId(roomId);
  return this.makeUnreadCountMap(chatUsers);
}

유저 필터링

일단, lastChatLogId가 존재하는 유저만 filter를 통해 가져오자.
lastChatLogId는 현재 접속중이지 않은 유저들만 가지고 있는 속성이다. 만약, 현재 채팅방에 접속중인 유저가 있다면 해당 유저의 lastChatLogId는 null이다.

const countMap: { [k: string]: number } = chatUsers.filter((user: ChatUserInfoDto) => {
  return !!user.lastChatLogId;
});

특정 속성만 갖기

위의 filter의 결과로 lastChatLogId가 존재하는 유저의 배열이 반환된다.

읽지 않은 인원 수 계산을 위해 우리는 유저들의 lastChatLogId만 필요하고 나머지 속성(userId, nickname 등)은 필요하지 않다.

따라서 map를 통해 lastChatLogId만 추출하자.

const countMap: { [k: string]: number } = chatUsers
  .filter((user: ChatUserInfoDto) => {
    return !!user.lastChatLogId;
  })
  .map(({ lastChatLogId }) => {
    return lastChatLogId;
  });

lastChatLogId 중복확인

그러면 filter와 map의 결과로 접속중이지 않으면서(lastChatLogId가 존재) lastChatLogId만 가지고 있는 유저의 배열이 반환된다.

우리는 특정 lastChatLogId에 대해 몇 명이 중복으로 가지고 있는지 확인을 해야한다.

전체 4명의 유저(A,B,C,D)가 채팅방에 있다고 가정하자.

유저 B와 유저 C가 같은 채팅을 읽고 함께 나갔다면 두 유저의 lastChatLogId가 같을 것이다. 이런 경우를 표시해야한다.

logID채팅 내용읽은 사람
6578402a51a926a577b8cfa4안녕하세요.A,B,C,D
6578402a51a926a577b8cfb0새로왔어요!A,D
6578402a51a926a577b8cfcf잘 부탁해요A,D
... 중략
6578402a51a926a577ccaa24100,000번째 채팅!A,D
6578402a51a926a577ccaa24나만 남았네D

reduce를 활용해 객체 만들기

reduce는 객체를 만드는데 만약 acc[cur]이 있다면 해당 값에 +1을 하고 acc[cur]이 없다면 1로 설정한다.

여기서 acc[cur]은 요소중에서 key가 lastChatLogId인 요소가 있는지를 의미한다.

즉, 위의 경우 유저 B에서 acc[6578402a51a926a577b8cfa4]가 1이 되었으므로 유저 C에서 다시 acc[6578402a51a926a577b8cfa4]을 하면 기존 값 1에서 1을 더한 2가 된다.

const countMap: { [k: string]: number } = chatUsers
  .filter((user: ChatUserInfoDto) => {
    return !!user.lastChatLogId;
  })
  .map(({ lastChatLogId }) => {
    return lastChatLogId;
  })
  .reduce((acc, cur) => {
    acc[cur] = acc[cur] ? acc[cur] + 1 : 1;
    return acc;
  }, {});

그러면 이제 key값이 logID고 value가 logID의 등장 횟수인 객체가 countMap에 담긴다.

countMap에서 정보 얻기

앞선 결과 다음과 같은 countMap이 만들어졌다고 생각하자.

{
  'b8cfa4' : 2,
  'ccaa24' : 1
}

우리는 logID가 대소 비교가 가능하다는 것을 알고있으므로 해당 객체를 통해 알 수 있는 것은 정보는 다음과 같다.

  • b8cfa4까지 읽고 나간 유저는 2명이다.
  • ccaa24까지 읽고 나간 유저는 1명이다.
  • 나머지 한명의 유저(D)는 접속중이다.
  • ccaa24의 logId가 더 크므로 ccaa24까지 읽은 유저는 반드시 b8cfa4를 읽었을 것이다.

즉, b8cfa4 채팅까지는 모두가 읽었다. b8cfa4이후 부터 ccaa24까지는 2명(A, D)이 읽었다. ccaa24이후 부터는 1명(D)이 읽었다.

읽지 않은 객체 만들기

Object.entries를 사용하여 countMap을 [key, value] 쌍의 배열로 변환한다.

Object.entries(countMap)
  .sort(([key1], [key2]) => {
    return key1.localeCompare(key2);
  })

즉, 아래와 같은 객체가 있다면

{

  'ccaa24' : 1
  'b8cfa4' : 2,
}

다음과 같이 변환한다. i번째 요소의 0번 요소는 logId고 1번 요소는 해당 logId를 lastChatLogId로 가지는 인원 수다.

[
    ["ccaa24", 1]
    ["b8cfa4", 2],
]

배열로 변환 이후 sort를 활용하여 key값을 기준으로 오름차순 정렬한다. key값으로 정렬하는 이유는 logID가 대소비교가 가능하므로 logID가 작은 채팅부터 읽지 않은 인원 수를 계산해야하기 때문이다.

가장 오래된 logID부터 reduce를 통해 읽지 않은 인원 수를 센다. 일단 읽지 않은 인원 수가 가장 적은 경우는 모두가 읽은 경우이므로 count를 0부터 시작한다.

그다음 현재까지의 count를 key로 하고 logId를 value로 하는 객체를 만든다. 이 때, count에서는 현재까지의 value를 계속 더한다.

let count: number = 0;
return Object.entries(countMap)
  .sort(([key1], [key2]) => {
    return key1.localeCompare(key2);
  })
  .reduce((acc, [key, value]) => {
    count += value;
    acc[count] = key;
    return acc;
  }, {});

즉, sort의 결과로 아래와 같은 배열이 있다면

[
    ["b8cfa4", 2],
    ["ccaa24", 1]
]

reduce를 통해 다음과 같은 객체가 최종적으로 반환된다.

{
  2 : 'b8cfa4',
  3 : 'ccaa24',
}

해당 객체를 프론트로 넘겨주자.

백엔드 전체 코드

전체 코드는 아래와 같다.

  async getUnreadCount(roomId: string) {
    const chatUsers: ChatUserInfoDto[] = await this.chatRepository.findUserListByRoomId(roomId);

    return this.makeUnreadCountMap(chatUsers);
  }

  makeUnreadCountMap(chatUsers: ChatUserInfoDto[]) {
    const countMap: { [k: string]: number } = chatUsers
      .filter((user: ChatUserInfoDto) => {
        return !!user.lastChatLogId;
      })
      .map(({ lastChatLogId }) => {
        return lastChatLogId;
      })
      .reduce((acc, cur) => {
        acc[cur] = acc[cur] ? acc[cur] + 1 : 1;
        return acc;
      }, {});

    let count: number = 0;
    return Object.entries(countMap)
      .sort(([key1], [key2]) => {
        return key1.localeCompare(key2);
      })
      .reduce((acc, [key, value]) => {
        count += value;
        acc[count] = key;
        return acc;
      }, {});
  }

프론트에서

그러면 프론트에서는 다음과 같은 객체를 넘겨받는다.

{
  2 : 'b8cfa4',
  3 : 'ccaa24',
}

위 객체를 해석하자면 logID가 b8cfa4까지는 모두가 읽은 것이다.(A,B,C,D)
logID가 b8cfa4보다 크고 ccaa24보다 작거나 같으면 2명이 읽지 않은 것이다.(A와 D가 읽음)
logID가 ccaa24보다 크면 이제 3명이 읽지 않은 것이다.(D만 읽음)

해당 정보를 이용해서 각 채팅에 대해 읽지 않은 인원 수를 계산해보자. 우선 앞서 받은 객체를 [key,value]의 배열로 만들자. chatUnread에는 위의 객체가 저장된다.

const chatUnreadSortArray = useMemo(() => {
  return new Array(...chatUnread).sort(([key1], [key2]) => {
    return key1.localeCompare(key2);
  });
}, [chatUnread]);

위의 결과로 다음의 배열이 반환된다.

[
	[2, 'b8cfa4'],
  	[3, 'ccaa24']
]

그 이후, 각각의 채팅이 unreadCount 함수를 호출해 읽지 않은 인원 수를 얻는다.

  1. chatUnreadSortArray가 빈 배열(모두가 접속하고 있는 상태)이라면 0을 반환한다.(모든 채팅에 대해 모두가 읽음)

  2. 1이 만족하지 않는다면 반복문을 통해 현재 채팅의 logId가 chatUnreadSortArray[i]의 logId보다 작다면 앞의 요소(chatUnreadSortArray[i-1])의 key를 반환한다.

const unreadCount = useCallback(
  (logId: string) => {
    if (chatUnreadSortArray.length === 0) {
      return 0;
    }

    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]
);

이 때, chatUnreadSortArray[i]의 첫 번째 요소 - 1이 아니라 chatUnreadSortArray[i-1]의 첫 번째 요소를 반환하는 이유는 다음과 같다.

아래와 같이 각 요소의 첫 번째 값이 연속적이라면 현재 요소의 key -1을 해도 정상적으로 읽지 않은 인원수를 보여준다.

즉, b8cfa4이후부터 ccaa24까지는 1명이 읽지 않았으므로 ccaa24의 2에서 1을 뺀 1을 반환해도 된다.

[
	[1, 'b8cfa4'],
  	[2, 'ccaa24']
]

하지만 만약 key값이 아래와 같다고 가정하자. 아래 같은 경우에는 b8cfa4까지는 모두가 읽었으며 b8cfa4이후부터 ccaa24까지는 2명이 읽은 것이다. 즉, 1명이 읽지 않은 채팅과 3명이 읽지 않은 채팅은 존재하지 않는다.

{
  2 : 'b8cfa4',
  4 : 'ccaa24',
}

특정 채팅의 logId가 c00123라고 하자. chatUnreadSortArray[i]의 첫 번째 요소 - 1을 반환하게 되면 4 - 1을 통해 3을 반환한다.

하지만 해당 채팅은 2명이 읽지 않은 채팅이다.

따라서 chatUnreadSortArray[i]의 첫 번째 요소 - 1이 아니라 chatUnreadSortArray[i-1]의 첫 번째 요소를 반환해야한다.

결과

카카오톡처럼 각각의 채팅에 대해 몇 명이 읽지 않았는지 실시간으로 확인할 수 있다.

profile
Front-End Developer

0개의 댓글