PJH's live chat - Menu

박정호·2023년 2월 5일
0

Live Chat Project

목록 보기
3/7
post-thumbnail

🚀 Start

이제 채팅 앱의 메인페이지의 레이아웃을 그려보자.


페이지는 Header, Left Side Bar, Chat Screen 총 3개로 나뉜다.

모달창은 다음과 같이 나뉜다.

대략적 workspace 구조

// Layouts/Workspace/index/tsx

      <Header> // 상위 메뉴
        <RightMenu> // 오른쪽 메뉴
           <ProfileImg/> // 프로필 이미지
          	  <Menu> // ⭐️ Menu 컴포넌트에 사용자 정보 전달 (children)
                <ProfileModal>
                  <img />
                  <div>
    			  </div>
                </ProfileModal>
                <LogOutButton/>
              </Menu>
        </RightMenu>
      </Header>
 -----------------------------------------------------------------   
      <WorkspaceWrapper> 
        <Workspaces> // 첫번째 왼쪽 메뉴바
          <WorkspaceButton /> // 워크스페이스 메뉴
          <AddButton /> // 워크스페이스 추가 
        </Workspaces>
    	
        <Channels> // 두번째 왼쪽 메뉴바
          <WorkspaceName /> // 워크스페이스 이름
          <MenuScroll>
            <Menu> // ⭐️ Menu 컴포넌트에 워크스페이스 기능 전달 (children)
              <WorkspaceModal>
                <button /> // 워크스페이스에 사용자 초대
                <button /> // 채널 만들기
                <button /> // 로그아웃
              </WorkspaceModal>
            </Menu>
            <ChannelList /> // ⭐️ 채널 목록을 나타내는 컴포넌트
            <DMList /> // ⭐️ DM 목록을 나타내는 컴포넌트
          </MenuScroll>
        </Channels>
    
        <Chats> // ⭐️ 해당 채널,DM 채팅 컴포넌트 출력
          <Suspense fallback={<div>로딩중</div>}>
            <Routes>
              <Route path="/channel/:channel" element={<Channel />} />
              <Route path="/dm/:id" element={<DirectMessage />} />
            </Routes>
          </Suspense>
        </Chats>
      </WorkspaceWrapper>
--------------------------------------------------------------------
      <CreateWorkSpaceModal/> // 워크스페이스생성 모달창
      <CreateChannelModal/> // 채널생성 모달창
      <InviteWorkspaceModal/> // 워크스페이스 초대 모달창
                
      <ToastContainer position="bottom-center" /> // 알림창


✔️ User Info

사용자 정보로 사용자의 랜덤 아바타 이미지를 생성해보자. 그리고 이미지 클릭시 사용자의 이름과 로그아웃 버튼이 출력된다.

아바타 이미지는 사용자 정보뿐만 아니라 채팅창에서 프로필, 메뉴바의 프로필 등 여러곳에서 사용된다.


👉 gravatar

설치

npm i gravatar @types/gravatar

사용

<ProfileImg 
	src={gravatar.url(userData.email, { s: '28px', d: 'retro' })}
    alt={userData.nickname}
 />
 
 //gravatar.url(email, options);

👉 Node.js Gravatar library



✔️ workspace list

workspace는 가장 큰 단위로 나눠지는 그룹이며, workspace 리스트에는 생성된 workspac들과 생성버튼이 존재한다.


구현

1️⃣ 로그인한 사용자가 속한 workspace들을 차례로 출력

2️⃣ App.tsx에서의 중첩라우팅 설정으로 워크스페이스 url에 따른 경로 변경

<Route path="/workspace/:workspace/*" element={<Workspace />} />

3️⃣ 워크스페이스 첫글자 대문자로 워크스페이스 프로필 생성

4️⃣ 생성 추가 버튼 클릭시 모달창 출력

const [showCreateWorkspaceModal, setShowCreateWorkspaceModal] =
    useState(false);

const onClickCreateWorkspace = useCallback(() => {
    setShowCreateWorkspaceModal(true);
  }, []);

 <Workspaces>
          {userData?.Workspaces.map(ws => { // 1️⃣ 번
            return (
              <Link key={ws.id} to={`/workspace/${ws.url}/channel/일반`}> // 2️⃣ 번
                <WorkspaceButton>
                  {ws.name.slice(0, 1).toUpperCase()} // 3️⃣ 번
                </WorkspaceButton>
              </Link>
            );
          })}
          <AddButton onClick={onClickCreateWorkspace}>+</AddButton> // 4️⃣ 번
</Workspaces>


✔️ channel list

토글 기능이 있는 채널리스트

Channel List 컴포넌트

1️⃣ 토클 아이콘 및 제목

2️⃣ 채널 목록 데이터들을 요청

3️⃣ 채널 각각에 대한 컴포넌트

const ChannelList: FC<Props> = () => {
  const { workspace } = useParams<{ workspace?: string }>();
  const [channelCollapse, setChannelCollapse] = useState(false);
  const { data: userData } = useSWR<IUser>('/api/users', fetcher, { dedupingInterval: 2000});
  const { data: channelData } = useSWR<IChannel[]>(
    userData ? `/api/workspaces/${workspace}/channels` : null, fetcher); // 2️⃣ 번

  const toggleChannelCollapse = useCallback(() => {
    setChannelCollapse(prev => !prev);
  }, []);

  return (
    <>
      <h2> 
        <CollapseButton // 1️⃣ 번
          collapse={channelCollapse}
          onClick={toggleChannelCollapse}
        >
          <i
            className="c-icon p-channel_sidebar__section_heading_expand p-channel_sidebar__section_heading_expand--show_more_feature c-icon--caret-right c-icon--inherit c-icon--inline"
            data-qa="channel-section-collapse"
            aria-hidden="true"
          />
        </CollapseButton>
        <span>Channels</span>
      </h2>

      <div>
        {!channelCollapse && channelData?.map(channel => { // 2️⃣ 번
            return <EachChannel key={channel.id} channel={channel} />; // 3️⃣ 번
          })}
      </div>
    </>
  );
};

export default ChannelList;


✔️ DM list

토글 기능이 있는 DM 리스트

DM List 컴포넌트

1️⃣ 토클 아이콘 및 제목

2️⃣ 채널 목록 데이터들을 요청

3️⃣ 채널 각각에 대한 컴포넌트

const DMList = () => {
  const { workspace } = useParams<{ workspace?: string }>();
  const { data: userData } = useSWR<IUser>('/api/users', fetcher, {
    dedupingInterval: 2000, // 2초
  });
  const { data: memberData } = useSWR<IUserWithOnline[]>(
    userData ? `/api/workspaces/${workspace}/members` : null,
    fetcher
  );
 
  const [channelCollapse, setChannelCollapse] = useState(false);
  const [onlineList, setOnlineList] = useState<number[]>([]);

  const toggleChannelCollapse = useCallback(() => {
    setChannelCollapse(prev => !prev);
  }, []);

  useEffect(() => {
    console.log('DMList: workspace 바꼈다', workspace);
    setOnlineList([]);
  }, [workspace]);

  useEffect(() => {
    socket?.on('onlineList', (data: number[]) => {
      setOnlineList(data);
    });
    console.log('socket on dm', socket?.hasListeners('dm'), socket);
    return () => {
      console.log('socket off dm', socket?.hasListeners('dm'));
      socket?.off('onlineList');
    };
  }, [socket]);

  return (
    <>
      <h2>
        <CollapseButton
          collapse={channelCollapse}
          onClick={toggleChannelCollapse}
        >
          <i
            className="c-icon p-channel_sidebar__section_heading_expand p-channel_sidebar__section_heading_expand--show_more_feature c-icon--caret-right c-icon--inherit c-icon--inline"
            data-qa="channel-section-collapse"
            aria-hidden="true"
          />
        </CollapseButton>
        <span>Direct Messages</span>
      </h2>
      <div>
        {!channelCollapse &&
          memberData?.map(member => {
            const isOnline = onlineList.includes(member.id);
            return (
              <EachDM key={member.id} member={member} isOnline={isOnline} />
            );
          })}
      </div>
    </>
  );
};

export default DMList;


👉 유저 상태 마크

유저의 로그인 유무에 따라 상태 마크 표시가 결정된다.

DMList, EachDM 컴포넌트

1️⃣ 메시지 송신이 이루어진다는 것은 로그인을 뜻하며, 이를 통해 onlineList에 해당하는 데이터를 담는다.

2️⃣ 담긴 데이터에 해당하는 유저를 찾고, EachDM 컴포넌트에 prop으로 전달.

3️⃣ 전달된 값에 따라 상태 마크에 디자인 부여

// DMList
const DMList = () => {
const [onlineList, setOnlineList] = useState<number[]>([]);

useEffect(() => {
    socket?.on('onlineList', (data: number[]) => { // 1️⃣ 번
      setOnlineList(data);
    });
    ...
 }
   return (
   	 memberData?.map(member => {
         const isOnline = onlineList.includes(member.id); // 2️⃣ 번
         return <EachDM key={member.id} member={member} isOnline={isOnline} />
          );
     })
  
}

// EachDM
const EachDM: FC<Props> = ({ member, isOnline }) => {
  ...
  return (
  	<i
        className={`c-icon p-channel_sidebar__presence_icon p-channel_sidebar__presence_icon--dim_enabled c-presence ${
          isOnline // 3️⃣ 번
            ? 'c-presence--active c-icon--presence-online'
            : 'c-icon--presence-offline'
        }`}
        aria-hidden="true"
        data-qa="presence_indicator"
        data-qa-presence-self="false"
        data-qa-presence-active="false"
        data-qa-presence-dnd="false"
      />
  )

  
}





👉 메시지 수신 표시

상대방에게 온 메시지에 대한 알림을 수신된 메시지 수로 표시. 이때 중요한 것은 내가 확인하지 않은 메시지가 언제부터인지에 대한 기준이 필요하다.

따라서, 데이터 요청시 쿼리데이터로 마지막으로 메시지를 본 시점을 전달하여 그 뒤로의 데이터들에 대한 count를 확인하면 될 것이다.

DirectMessage 컴포넌트

  • DM에서 받아온 메시지 데이터에 대한 시점을 localstorage에 저장해놓는다.
 useEffect(() => {
    localStorage.setItem(`${workspace}-${id}`, new Date().getTime().toString());
  }, [workspace, id]);

EachDM 컴포넌트

  • localstorage에 저장된 데이터를 이용하여 서버에 요청을 한다.
    ex) "제가 받아온 데이터는 이 시점까지 였는데, 이 시점 이후로 들어온 데이터 개수 좀 보내주세요"
const EachDM: FC<Props> = ({ member, isOnline }) => {
  
  const { workspace } = useParams<{ workspace?: string }>();
  const location = useLocation();
  
  const { data: userData } = useSWR<IUser>('/api/users', fetcher, {
    dedupingInterval: 2000, // 2초
  });
  
  const date = localStorage.getItem(`${workspace}-${member.id}`) || 0;
  const { data: count, mutate } = useSWR<number>(
    userData
      ? `/api/workspaces/${workspace}/dms/${member.id}/unreads?after=${date}`
      : null,
    fetcher
  );


  return (
      {(count && count > 0 && <span className="count">{count}</span>) || null}
  
  );
};
profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글