
나는 팀원들과 프로젝트를 하거나 단순 의사소통을 할 때 특히 슬랙으로 소통하는 일이 잦아서 슬랙을 자주 사용한다.
물론 뭐 클론코딩이지만, 내가 사용하고 있는 서비스를 직접 이렇게 구현해보고 있다는게 신기했다.
오늘 포스팅할 내용은 간단하게 채널과 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; |
이 부분은 이전에 포스팅했던 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; |
DM은 실시간 채팅 기능이 구현이 아직 되어있지 않은 상태이다. 그래서 아직 껍데기만 있고 제대로 쓰지 못하는 기능들이 있다. 예를 들면

이 이름 옆에 동그라미는, 오프라인일 경우 꺼져있고 온라인일 경우 켜지게 된다. 이 온라인 여부는 isOnline Hook으로 관리하는데, 이 isOnline은 onlineList에 들어가있는지의 여부에 따라서 결정이 된다. 그 온라인 리스트는 실제로 접속해있는지에 따라 결정되어 명수가 리스트 내에 숫자 형태로 저장이 된다. 그래서 일단 껍데기 수준에서는 사용하지 못하는 기능이다.
그 외에는 음.. 채널과 마찬가지로 눌렀을 때 해당 DM 페이지 URL로 이동하게 된다는 점? 정도가 있을 것 같다.
{member.id === userData?.id && <span> (나)</span>}
이 부분을 보면, member 리스트 내에 담겨있는 id와 userData(로그인 한 user)의 id가 같을 경우, 즉 '나'인 경우에는 옆에 (나) 표시가 뜨게 하여 나와 타인의 구분을 쉽게 하였다. 이렇게 한 이유는, 슬랙에서 중복된 이름을 가진 사용자가 있을 수 있기 때문이다.
이외에 resetCount나, onMessage 등은.. DM 전체가 완성되고나서 다시 포스팅할 예정이다.
'Web' 카테고리의 다른 글
[디버깅] Cannot use JSX unless the '--jsx' flag is provided.ts(17004) 해결 (0) | 2021.08.01 |
---|---|
[React + TypeScript] DM 페이지와 ChatBox 만들기, DM 콘솔에 띄우기 (0) | 2021.07.13 |
[React + TypeScript] API통신을 이용한 워크스페이스 만들기 (0) | 2021.07.09 |
[API통신] SWR - revalidate와 mutate의 차이점 (+optimistic, pessimistic UI) (0) | 2021.07.06 |
[API통신] withCredentials, SWR (0) | 2021.07.05 |