우리 서비스의 치명적인 빈조각이 있다.

유저끼리 활동을 약속잡고 관리하는 기능은 구현이 되어있지만 유저간 소통할 수 있는 방법이 없다. 물론 이 생각을 전에 안한 것은 아니다. 하지만 우선 순위에서 밀려있었던것 뿐이다. 전에 막연하게 생각했던 방법이 몇가지 있다.
- 수락하면 메일보내주기
- 서로의 메일 보여주기
- 채팅 구현하기
위의 두가지 방법은 너무 무책임한 방법이긴하다. 그래서 이제서야 채팅을 구현해보기로 했다.
채팅을 어떻게 구현해야할지 고민을 했다. 기존의 http통신으로는 실시간 통신도 안될뿐더러 억지로 만든다고 해도 일정 시간마다 채팅을 불러오는 귀찮은 방법을 구현해야한다. 그래서 선택한 방법이 Web socket이다.
간단하게 web socket은 http의 단점을 극복하기 위한 통신 방법이라고 생각하면 된다.

http 통신 방법의 이미지이다. request를 보내면 response를 보내주는 형식이다. 그래서 새로운 데이터가 필요할때마다 요청을 보내고 응답을 받아야한다. 만약 이 방식을 채택해서 채팅을 구현한다면 누군가 채팅을 보낼때마다 그 메세지를 받기위해 요청을 보내야 한다. 생각만해도 번거롭다.

web socket 통신 방법의 이미지이다. http통신과 다르게 매번 새로운 요청을 보내지 않는다. 양방향 통신으로 한번 연결되면 지속적으로 요청과 응답을 주고 받을 수 있다.
그럼 당연히 web socket이 http보다 더 좋은 방법인가? 그건 아니다. 각 방법마다 장단점이 존재하기 때문에 각 용도에 맞게 사용하는 것이 맞다. 우리는 채팅 기능을 구현함에 있어서 빠른 응답속도와 지속적인 연결이 필요했기 때문에 web socket을 사용해서 구현을 할 것이다.
web socket과 http에 대해서는 나중에 더 깊게 포스트를 작성해보겠다.
여러개의 web socket을 사용한 라이브러리가 존재한다. 가장 대표적인 것은 socket.io, firebase등이 있다. 하지만 우리는 ably.io를 사용해보겠다. 채택하게 된 이유는 우리는 서버리스 서비스로 Next.js와 Vercel을 사용해서 배포하고 있다. 그래서 추천하는 방식을 우선적으로 도입을 해보기로 했다.
Vercel template
Vercel에서는 Next.js를 활용한 여러가지 템플릿을 제공한다. 그 중에 서버리스 웹소켓에 대한 템플릿도 제공하고 있다. 심지어 코드까지 제공을 하기 때문에 우리가 참고해서 작업하기에 매우 용이하다. 그래서 ably.io를 사용해보기로 했다.
유명한 쇼핑몰 이름과 같아서 익숙한 이름이다. 물론 그곳과는 아무런 연관은 없다. 시작하는 방법은 간단하다.
$ npm install ably
설치를 해주고 폴더 구조를 설정하면 된다. 우선 채팅 기능은 chat이라는 route로 묶어주고 폴더는 [channel]로 다시 만들어주겠다. 여기에서 channel은 activity Id로 넣어줘서 activity Id가 channel이 될것이다.
import { Realtime } from 'ably';
import { AblyProvider, ChannelProvider } from 'ably/react';
...
export default function ChatPage({ channel, user, activityTitle }: Props) {
const client = new Realtime({
key: process.env.NEXT_PUBLIC_ABLY_KEY,
clientId: user.id,
});
const channelName = `chat:${channel}`;
return (
<AblyProvider client={client}>
<ChannelProvider channelName={channelName}>
<Chat channelName={channelName} user={user} activityTitle={activityTitle} />
</ChannelProvider>
</AblyProvider>
);
}
먼저 해줘야 하는 것은 ably를 클라이언트에서 불러오는 것이다. ably.io에서 발급받은 key값과 접속한 유저의 id를 넣어서 ably를 실행하는 것이다. 그리고 다이나믹 라우팅으로 얻은 activity Id는 channel이름이 되어서 chat:${channel}의 형식으로 이름이 들어가게 된다.
이제 AblyProvider와 ChannelProvider를 사용해서 채팅 컴포넌트를 감싸주면 된다. 이제 Chat컴포넌트에서 ably를 사용할수 있게 된다.
ably.io에서 메세지를 보내는것은 pulish라고 한다.
import { useChannel } from 'ably/react';
...
const reducer = (prev: any, event: any) => {
switch (event.name) {
case ADD:
return [...prev, event];
default:
return prev;
}
};
export default function Chat({ channelName, user, activityTitle }: Props) {
const [messages, dispatch] = useReducer(reducer, []);
const { channel, publish } = useChannel(channelName, dispatch);
const publishMessage = (text: string) => {
publish({
name: ADD,
data: {
text,
avatarUrl: user.image,
nickname: user.nickname,
},
});
};
우선 ably.io에서 제공하는 useChannel을 사용해서 현재 채널에 해당하는 이벤트를 생성해준다. 그리고 useReducer를 사용해서 상태관리를 해주는데 현재는 메세지를 추가하는 기능만 되어있다. 삭제하는 기능도 가능하지만 아직 우리 서비스에서 삭제까지 필요한지에 대한 의문이 들어서 우선 ADD만 추가해놨다.
이제 메세지를 보내는 부분에서는 채널에 pulish동작을 해주면 되는데 data에 내가 원하는 데이터를 넣어줄수가 있다. 그래서 기본적으로 보내게되는 text, 유저 이미지, 닉네임을 넣어줬다.
이렇게 추가된 메세지는 useReducer에 의해 messages에 저장된다.
export default function MessageList({ messages, user }: Props) {
return (
<ul>
{messages.map((item: any) => (
<li key={item.id} className={`flex p-3 ${user.id === item.clientId && 'flex-row-reverse'}`}>
<MessageCard message={item} user={user} />
</li>
))}
</ul>
);
}
이렇게 저장된 메세지는 간단하게 map을 통해 렌더링시켜주면 된다. ably를 처음 실행할때 넣어준 client Id와 메세지 데이터에 포함된 client Id가 일치하는지 확인해서 방향을 바꿔주면 된다.
메세지를 넣어주는 인풋은 간단하다.
export default function MessageInput({ onSubmit }: Props) {
const [inputValue, setInputValue] = useState('');
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(inputValue);
setInputValue('');
};
return (
<form onSubmit={handleSubmit} className="flex gap-2 bg-white">
<Input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="메세지를 입력하세요"
/>
<Button className="text-white font-semibold" disabled={inputValue.length === 0}>
전송
</Button>
</form>
);
}
해당 컴포넌트에서 받은 onSubmit은 위에서 만들었던 publishMessage를 넣어줬다.

우선 간단하게 테스트해본 결과 잘 동작하는 것을 확인할 수 있다. 그냥 추가만 되는 것이 아니고 다른 유저와 실시간으로 되지만 현재 내 로컬에서 두개를 동시에 하기엔 힘들기때문에 그건 나중에 테스트 해보겠다.
채팅기능까지 얼추 되어가고 있다. 하지만 미흡한 부분이 몇 존재한다. 우선 이전 대화내역은 확인이 안된다는 것이다. ably.io의 무료버전은 24시간까지 저장이 된다. 그래서 그 부분을 살려서 하면 좋을 것 같다. 그리고 추가적으로 현재 페이지에 접속중인 유저를 표시할 것이다. 그래야 실시간의 의미가 좀더 있지 않나 싶다.
솔직히 ably.io를 완전히 이해하고 사용하는 것은 아니다. 우선 타입스크립트 기반이 아니라서 현재 우리 프로젝트에서 타입 오류가 많이 발생한다. 그래서 우선적으로 any타입을 적용시켜놨는데 참 찝찝하다. 그리고 next.js 14버전을 사용하면서 서버액션을 사용하는데 ably.io는 전체가 클라이언트 액션이다보니 어쩔수 없는 기능의 부재가 존재한다. 최대한 각 이점을 끌어내서 완벽하게 채팅 구현해보겠다.