-
[ Next ] Ably Realtime 무료 소켓 활용하여 채팅 만들기Front/Next 2025. 9. 27. 23:18반응형
채팅 기능을 구현하기 위해 무료로 사용할 수 있는 소켓을 찾아서 이걸 활용하고자 했다.
Login to your Ably dashboard
Ably provides a suite of APIs to build, extend, and deliver powerful digital experiences in realtime. Organizations like Toyota, Bloomberg, HubSpot, and Hopin depend on Ably’s platform to offload the growing complexity of business-critical realtime data
ably.com
우선 ably에서 회원 가입을 한 후 새로운 프로젝트를 생성
ably에서 발급된 키를
루트 프로젝트 .env.local에 환경 변수로 설정
Installment
우선 ably사용을 위해 ably를 install
npm install ably
Initalized
ably를 활용할때 어떠한 사용자가 연결했는지 알기 위해 token발급하여 채널 연결 방식으로 진행
우선 ably에서 토큰 발급을 위한 route를 만든다.
사용자 정보가 필요하기때문에 사용자 id를 clientId에 넣어서 토큰을 발급
export async function GET(request: Request) { const { searchParams } = new URL(request.url); const clientId = searchParams.get('clientId'); const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY! }); const tokenRequest = await ably.auth.createTokenRequest({ clientId: clientId || undefined }); return NextResponse.json(tokenRequest); }
이후 소켓 연결을 zustand 스토어를 활용하여 사용자가 로그인을 했을때 ably소켓과 연결
연결한 ably는 ablyInstance에 저장
let ablyInstance: Realtime | null = null; const getAblyInstance = (): Ably.Realtime | null => { const userStore = localStorage.getItem('user') if(!userStore) return null const storedUser = JSON.parse(userStore); const {setLogin} = useUserStore.getState?.(); setLogin(storedUser) if(!ablyInstance) { ablyInstance = new Ably.Realtime({ authUrl: `/api/ably-token?clientId=${storedUser.id}` }); } return ablyInstance } export const useAblyStore = create<AblyState>((set,get) => ({ ably: null, initAbly: () => { const instance = getAblyInstance() set({ably: instance}) }, }))
이후 로그인 버튼을 눌렀을때 해당 로그인 성공시에는 initAbly 함수를 실행해서 ablyInstance에 넣어준다.
const onClickLogin = async () => { const res = await fetch("/api/auth/login", { method: "POST", body: JSON.stringify({username, password}), headers: { "Content-Type": "application/json" }, }) const data = await res.json() if(data.success) { initAbly() } else { alert(data.error) } }
Channel
ably에서 channel은 채팅방이라고 생각하면 되며, 각 채팅방별로 channel Id를 다르게 설정한다.
현재 진행중인 프로젝트에서는 소켓 연결만 하고 채팅방 미리보기 같은 개념은 없고, 채팅방 접속만 있는 프로젝트를 만들고있어서
channel접속은 채팅방 접속할때만 처리되도록 했다.
useEffect를 활용하여 처음 접속할때 그리고, 접속한 상태에서 새로고침을 하면 소켓연결이 끊어지기때문에 다시 재연결을 시키기 위해 연결이 안되어있으면 다시 재연결
useEffect(() => { if(!ably) { initAbly() return } attachChannel() }, [ably]);
채널 연결은 접속할 방 id를 활용하여 채널 접속을 할 수 있다.
channel은 ably.channels.get(channelId) 를 통해 해당 id의 채널에 접속할 수 있으며,
여기 채널에 들어온 모든 사람들은 서로 메시지를 주고 받을 수 있다.
getChannel: (channelName: string) => { const ably = get().ably if(!ably) { throw new Error("Ably instance not initialized."); } return ably.channels.get(channelName) }
하지만 채널에 접속해서 메시지를 주고 받을 수는 있지만 이 채널에는 해당 사용자가 연결되었다는 정보는 없다.
정리하면 카톡에서 채팅에 들어가면 상대방이 보낸 메시지는 계속 해서 받을 수 있지만 그 채팅방에는 들어가지 않았다고 생각하면 된다. 그래서 여기 채널에 입장 했다는 것을 처리해야한다.
채널의 입장은 presence.enter를 활용하여 처리할 수 있으며, 처음 ably토큰을 발급했을때는 id만 넣어주었기 때문에 사용자 닉네임은 존재하지 않기 때문에 enter시 어떤 닉네임의 사용자가 들어왔는지 보여주기 위해서 사용자 닉네임을 metadata로 전달
const channel = getChannel(Id); channelRef.current = channel channel.on('attached', async () => { await channel.presence.enter({username}) })
이후 새로운 사용자가 입장했을 경우 현재 방 안에 있는 사용자들에게 어떤 사람이 들어왔다고 메시지 전송을 위해 enter 와 leave일때 메시지를 전달.
subscribe(['enter','leave']) 를 하면 enter와 leave시에 해당 함수가 callback된다.
이후 현재 해당 채널에 접속되어있는 모든 사용자들의 정보를 받을 수 있으며,
현재 채널에 접속된 사용자는 store에 저장하여 관리를 했다.
이후 마지막 enter까지 완료과 되면 flag로 attached 값을 주어 해당 사용자에게 ui를 보여줄지 말지를 처리
나머지는 서로 전달되는 메시지는 subscribe()를 통해 모든 메시지가 전달된다.
전달 받은 메시지는 리스트에 계속해서 추가해주면 된다.
const attachChannel = async () => { if(isChannelAttached) return const channel = getChannel(Id); channelRef.current = channel channel.on('attached', async () => { await channel.presence.enter({username}) }) channel.presence.subscribe(['enter', 'leave'], async (presenceMsg: Ably.PresenceMessage) => { console.log('presenceMsg', presenceMsg) const presenceSet = await channel.presence.get(); const members: CampMember[] = presenceSet.map((p: Ably.PresenceMessage) => ({ clientId: p.clientId, username: p.data?.username || '익명' })); console.log('members', members); setRoomMemberList(members); setRoomMember(members.length); setIsChannelAttached(true); if(presenceMsg.clientId != getUser().id) return if(presenceMsg.action == 'enter') { const msg = `${username}${getJosa(username)} 입장했습니다.`; await ablyMessage("enter", msg); } }); await channel.subscribe(async (msg: Ably.Message) => { console.log('msg', msg) setMessages((prev) => [...prev, getMsg]) }) await channel.attach() }
Message
이제 채널 연결이 완료 됨에 따라서 서로 메시지를 주고 받도록 처리
메시지는 publish를 통해서 보면 되고, action은 이 메시지가 채팅 메시지인지 시스템 메시지인 구분하기 위한 key값으로 처리
ably 소켓 무료 버전을 사용하다보니 아무거나 입력하고 도배 방지를 위해서 메시지를 전송 완료 전까지는 메시지가 전송이 안되도록 flag를 설정했다.
이후 다양한 메시지를 보낼 생각이기에 메시지 보내는 함수를 따로 만들어서 사용했다.
그래서 active 변수를 따로 받아서 추가로 메시지에 특정 이벤트가 발생할 경우 어떤 event가 발생하는지 처리
이후 event가 발생함에 따라서 방 상태에도 영향을 주기에 특정 event가 발생했을 경우 현재 방상태의 정보도 같이 보내어 이 보낸 정보는 각각 클라이언트 쪽에서의 store에서 관리하도록 했다.
const sendMessage = async () => { if(!channelRef.current) return if(!message) return if(messageFlag) return const msg = `${username}: ${message}` await ablyMessage('chat',msg) setMessage('') setMessageFlag(false) } const ablyMessage = async (action: keyof typeof msg, message: string, active?: Active) => { const channel = channelRef.current if(!channel) return const ablyMessage: AblyMessage = { from: getUser(), msg: message, active: active, room: null, } if(ablyMessage.active) { ablyMessage.room = getRoom() } await channel.publish(action, ablyMessage) }
Deconnected
채널에서 나갈때는 해당 컴포넌트가 unmounted 되기때문에
useEffect로 처음 mount될때 한번 실행시켜두고 unmount 될때 cleanup함수를 실행시켜 채널과 채널 연결을 끊기도록 했다.
useEffect(() => { return () => { cleanup(); }; }, []);
이러면 새로고침할때 뿐만 아니라 뒤로가기 혹은 채팅방 나가기 버튼을 눌렀을때 cleanup함수가 알아서 실행되며 연결이 끊기게 된다. 이후 방 정보는 사용자가 나갈때 DB에 업데이트가 되도록 처리하여 DB업데이트는 최대한 자주 안하는 방식으로 처리
채널연결을 끊을때에는
leave로 해당 채널을 나가고
unsubscribe로 해당 채널의 전송을 끊고,
detach로 연결자체를 끊고,
off로 채널 자체를 닫는다.
마지막으로는 channelInstance를 null로 초기화 시킨다.
const cleanup = async () => { if (!channelRef.current) return; try { await channelRef.current.presence.leave(); channelRef.current.presence.unsubscribe(); channelRef.current.unsubscribe(); await channelRef.current.detach(); channelRef.current.off(); } catch (e) { console.error('socket detached error', e); } channelRef.current = null; /* 나갈시 나간 사용자가 DB 업데이트 처리 */ try { await fetch(`/api/camp/${campId}`, { method: 'POST', body: JSON.stringify({item, member}), headers: { "Content-Type": "application/json" }, keepalive: true, }) } catch (e) { console.error('camp update error', e); } setIsChannelAttached(false) };
이러면 서로 메시지가 정상적으로 주고 받을 수 있다.
728x90반응형'Front > Next' 카테고리의 다른 글
[ Next ] KaKao 지도 API 사용하기 (0) 2025.09.28 [ Next ] Redis 사용하여 캐시 관리 (0) 2025.09.28 [ Next ] MongoDB 연결 (0) 2025.09.27 [ Next ] webpack 활용 svg icon (0) 2025.09.10