Today Sangmin Learned
article thumbnail
728x90
실제 슬랙 화면

나는 팀원들과 프로젝트를 하거나 단순 의사소통을 할 때 특히 슬랙으로 소통하는 일이 잦아서 슬랙을 자주 사용한다.
물론 뭐 클론코딩이지만, 내가 사용하고 있는 서비스를 직접 이렇게 구현해보고 있다는게 신기했다.
오늘 포스팅할 내용은 간단하게 채널과 DM 섹션이다.

현재 DM에 사용자로 등록되어있는 'a', 'aa', 'a', 'a', 'a' 이렇게 5개의 계정은 각각 로그인 및 회원가입 페이지를 만들 때 임시로 만들었던 계정들이다. 회원가입을 하면 자동으로 DM 섹션에 추가가 되는 것이다. 사실 실제 슬랙에서는 이 워크스페이스에 사용자를 초대해야 DM 섹션에 뜨게 되지만, 어쨌든.. 회원가입을 한 값이 여기에 추가된다는 게 중요한 게 아닐까? 싶다.

진행을 하기 전에 이전 포스팅에서는 전부 서버 주소가 포함되어 있었는데, 웹팩 프록시 설정을 통해 앞 서버 주소를 생략하였다.

devServer: {
historyApiFallback: true,
port: 3090,
publicPath: '/dist/',
proxy: {
'/api/': {
target: 'http://localhost:3095',
changeOrigin: true,
},
},
},

그래서, 기존 axios를 통해 통신하는 API 주소가 'http://localhost:3095/api/users/~' 이런 식이었다면 이제 '/api/users/~'으로 바뀌었다.

1. 1. 채널 리스트 컴포넌트

// import useSocket from '@hooks/useSocket';
import { CollapseButton } from '@components/DMList/styles';
import { IChannel, IUser } from '@typings/db';
import fetcher from '@utils/fetcher';
import React, { FC, useCallback, useState } from 'react';
import { useParams } from 'react-router';
import { NavLink } from 'react-router-dom';
import useSWR from 'swr';
const ChannelList: FC = () => {
const { workspace } = useParams<{ workspace?: string }>();
// const [socket] = useSocket(workspace);
const {
data: userData,
error,
revalidate,
mutate,
} = useSWR<IUser>('/api/users', fetcher, {
dedupingInterval: 2000, // 2초
});
const { data: channelData } = useSWR<IChannel[]>(userData ? `/api/workspaces/${workspace}/channels` : null, fetcher);
const [channelCollapse, setChannelCollapse] = useState(false);
const toggleChannelCollapse = useCallback(() => {
setChannelCollapse((prev) => !prev);
}, []);
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>Channels</span>
</h2>
<div>
{!channelCollapse &&
channelData?.map((channel) => {
return (
<NavLink
key={channel.name}
activeClassName="selected"
to={`/workspace/${workspace}/channel/${channel.name}`}
>
<span># {channel.name}</span>
</NavLink>
);
})}
</div>
</>
);
};
export default ChannelList;
view raw index.tsx hosted with ❤ by GitHub

이 부분은 이전에 포스팅했던 useSWR을 그대로 사용하였다. 이전 포스팅 에서 채널을 새로 만드는 방법을 기록했다. 여기에서 axios를 사용했는데, 중요했던 부분은 채널 생성하기를 누를 경우

`http://localhost:3095/api/workspaces/${workspace}/channels`

이쪽으로 axios.post를 보내서 서버에 newChannel의 형태로 새 채널을 저장하고, 그 이후 revalidateChannel()을 통해 채널을 다시 불러오게 하는 것이었다. 아 채널들은 channelData에 저장이 되고, 이 채널 리스트들이 바로 # 채널이름 의 형태로 저장이 되는 것이다.

1.1. 1-1. 채널 리스트 불러오기(useSWR 사용)

21행을 보면 userData가 있을 경우, 즉 로그인이 되어있을 경우 useSWR을 사용해서 위 workspace/channels를 불러오게 하였다. 이제 data에는 채널 리스트가 담긴 데이터가 저장이 되어있는 것이다. 그 아랫줄 channelCollapse는 맨 위 움짤처럼 토글했을 때 내부 채널들이 보이고, 접히고를 useState로 나타낸 것이다.

1.2. 1-2. 껍데기 (return문)

CollapseButton은 움짤에서 눌렀을 때 90도로 회전하는 삼각형 버튼이다. toggleChannelCollapse를 통해, 눌렀을 경우 채널들이 보이게 되는 것이다. 그 내부의 이미지는 슬랙에서 그대로 가져온 것.div 내부에 map문을 보면, !channelCollapse, 즉 채널이 접혀있지 않을 경우에, 눌렀을 경우

`/workspace/${workspace}/channel/${channel.name}`

로 이동하는 NavLink들을 집어넣었다. NavLink와 Link의 차이점은, NavLink의 경우 Link의 기능에 더해서, 선택됐을 경우(즉 NavLink가 가리키는 URL이 활성화된 경우) 특정 스타일 또는 특정 클래스를 지정할 수 있다는 점이다.
그러니까 여기서는, NavLink가 클릭 됐을 경우 activeClassName='selected'가 추가되어 클래스에 selected가 추가가 된다. 그리고 그 바뀐 클래스에 따라서 아래를 보고 있던 삼각형이 오른쪽을 바라보게 되는 것이다.

우리가 위에서, useSWR을 통해 불러온 채널들(데이터들)을 channelData에저장한다고 했다. 그 channelData를 map 함수를 통해 돌면서 띄워주는 것이다.

그래서 이렇게 누르면, workspace/워크스페이스이름/channel/채널이름 으로 URL이 바뀌게 된다! 채널마다 따로 URL을 갖게 되는 것이다.

2. 2. DM 리스트 컴포넌트

DM 리스트는 채널 리스트와 비슷하다.

import { IDM, IUser, IUserWithOnline } from '@typings/db';
import React, { useState, useCallback, FC, useEffect } from 'react';
import { useParams } from 'react-router';
import { CollapseButton } from '@components/DMList/styles';
import useSWR from 'swr';
import fetcher from '@utils/fetcher';
import { NavLink } from 'react-router-dom';
interface Props {
userData?: IUser;
}
const DMList: FC<Props> = ({ userData }) => {
const { workspace } = useParams<{ workspace?: string }>();
const { data: memberData } = useSWR<IUserWithOnline[]>(
userData ? `/api/workspaces/${workspace}/members` : null,
fetcher,
);
// const [socket] = useSocket(workspace);
const [channelCollapse, setChannelCollapse] = useState(false);
const [countList, setCountList] = useState<{ [key: string]: number }>({});
const [onlineList, setOnlineList] = useState<number[]>([]);
const toggleChannelCollapse = useCallback(() => {
setChannelCollapse((prev) => !prev);
}, []);
const onMessage = (data: IDM) => {
console.log('DM도착', data);
setCountList((list) => {
return {
...list,
[data.SenderId]: list[data.SenderId] ? list[data.SenderId] + 1 : 1,
};
});
};
useEffect(() => {
console.log('DMList: workspace 바뀜', workspace);
setOnlineList([]);
setCountList({});
}, [workspace]);
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);
const count = countList[member.id] || 0;
return (
<NavLink key={member.id} activeClassName="selected" to={`/workspace/${workspace}/dm/${member.id}`}>
<i
className={`c-icon p-channel_sidebar__presence_icon p-channel_sidebar__presence_icon--dim_enabled c-presence ${
isOnline ? '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"
/>
<span className={count > 0 ? 'bold' : undefined}>{member.nickname}</span>
{member.id === userData?.id && <span> (나)</span>}
{count > 0 && <span className="count">{count}</span>}
</NavLink>
);
})}
</div>
</>
);
};
export default DMList;
view raw index.tsx hosted with ❤ by GitHub

DM은 실시간 채팅 기능이 구현이 아직 되어있지 않은 상태이다. 그래서 아직 껍데기만 있고 제대로 쓰지 못하는 기능들이 있다. 예를 들면

이 이름 옆에 동그라미는, 오프라인일 경우 꺼져있고 온라인일 경우 켜지게 된다. 이 온라인 여부는 isOnline Hook으로 관리하는데, 이 isOnline은 onlineList에 들어가있는지의 여부에 따라서 결정이 된다. 그 온라인 리스트는 실제로 접속해있는지에 따라 결정되어 명수가 리스트 내에 숫자 형태로 저장이 된다. 그래서 일단 껍데기 수준에서는 사용하지 못하는 기능이다.

그 외에는 음.. 채널과 마찬가지로 눌렀을 때 해당 DM 페이지 URL로 이동하게 된다는 점? 정도가 있을 것 같다.

{member.id === userData?.id && <span> (나)</span>}

이 부분을 보면, member 리스트 내에 담겨있는 id와 userData(로그인 한 user)의 id가 같을 경우, 즉 '나'인 경우에는 옆에 (나) 표시가 뜨게 하여 나와 타인의 구분을 쉽게 하였다. 이렇게 한 이유는, 슬랙에서 중복된 이름을 가진 사용자가 있을 수 있기 때문이다.

이외에 resetCount나, onMessage 등은.. DM 전체가 완성되고나서 다시 포스팅할 예정이다.

profile

Today Sangmin Learned

@steadily-worked

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!