REST API vs Websocket API
채팅은 REST API로도 구현할 수 있다. 그러나 Rest API를 이용하여 반복하여 요청해서 데이터를 요청하고, 데이터를 받아올 수 있게 되면 자원 낭비가 일어나게 된다. 그렇다면 한 번만 요청하고 그 뒤로 정보가 변할 때마다 그냥 불러오도록 할 수 없을까?
여기서 새로운 방식의 API가 필요하게 된다. 바로 Websocket API이다. 구독형 API라고도 할 수 있다.
Websocket API
: Statefull protocol, 요청을 매번 보내는 게 아니라 connection을 유지해서 양방향 통신 또는 데이터 전송이 가능하도록 하는 기술
- Websocket API 방식에서는 데이터 요청자와 데이터 제공자 간에 흐르는 채널이 열리게 된다.
- REST API는 필요시마다 문을 두드려서 문을 여는 것이라면, Websocket API의 경우에는 계속해서 물이 흐르는 것과 같음
Sock.js
; Websocket과 비슷한 기능을 제공하는 브라우저 JS 라이브러리
- 브라우저와 웹 서버 사이에서 짧은 지연시간, 그리고 크로스 브라우징을 지원하는 API
- 크롬 뿐 아니라 사파리, 파이어 폭스 그리고 websocket프로토콜을 지원하지 않는 최신 브라우저에서도 정상 작동
- handShake과정을 통해 Client와 Server 접속을 유지
Stomp
; Single Text Oriented Messaging Protocol의 약자
- 클라이언트와 서버가 서로 통신하는 데 있어 메시지의 형식, 유형, 내용 등을 정의해 주는 프로토콜
- 단순한 Binary, Text가 아닌 규격을 갖춘 메시지를 보낼 수 있음
- spring에 종속적이며 구독 방식을 사용
Stomp 연결 순서
1. 서버와 연결할 클라이언트 connect
- StompJs를 이용하여 클라이언트를 생성해 줍니다.
- 옵션은 링크에서 추가적으로 확인할 수 있으며, 페이지 이동 시 reconnect를 방지하기 위해 reconnectDelay:0을 넣어주었습니다.
- 인터넷 익스플로러 구버전 등 Stomp를 지원하지 않는 사용자를 위해 SockJS로도 연결해 줍니다.
client.current = await createClient('/ws-connection');
...
const createClient = (endpoint) => {
const client = new StompJs.Client({
brokerURL: `wss://atties.shop${endpoint}`,
connectHeaders: { Authorization: access },
debug: (res) => {
console.log(res);
},
reconnectDelay: 0,
});
client.webSocketFactory = () => {
const socketIn = new SockJS(
`${process.env.NEXT_PUBLIC_API_BASE_URL}${endpoint}`,
);
return socketIn;
};
return client;
};
2. 메시지 전송 전 subscriber와 publisher 지정
2.1 subscribe : 해당 url로 나에게 메시지를 보낼 수 있는 경로가 생김
- subscribe, 즉 채팅 실시간 받기입니다.
- 채팅방 목록과 채팅방 내부가 있습니다.
- 채팅방 목록에서는 서버에 한 번 connect 후, 각 채팅방을 모두 subscribe를 하고
- 채팅방 내부에서는 서버에 한 번 connect 후, 해당 채팅방에 subscribe를 하여 진행합니다.
채팅방 목록에서의 onConnected 함수입니다.
client.current.onConnect = await onConnected;
...
const onConnected = () => {
if (chatRoomList?.length > 0) {
chatRoomList.forEach((chatRoom) => {
subscribe(client.current, chatRoom?.chatRoomId, subscribeCallback);
});
}
};
채팅방 내부에서의 onConnected 함수입니다.
client.current.onConnect = await onConnected;
...
const onConnected = () => {
subscribe(client.current, id, subscribeCallback, true);
};
subsribe 함수입니다. 채팅방 내부에 접속하면 ‘읽음’처리가‘읽음’ 처리가 되어야 하고, 채팅방 목록에서는 ‘읽음’ 처리가 되지 않아야 합니다. 그래서 이 둘을 서버 측에서 구분할 수 있도록 채팅방 내부에서 subscribe 할 때는 action:’enter’을 넣어 전송하였습니다.
const subscribe = (client, roomId, subscribeCallback, isRoom = false) => {
if (isRoom) {
client.subscribe(`/queue/chat-rooms/${roomId}`, subscribeCallback, {
Authorization: access,
action: 'enter',
});
} else {
client.subscribe(`/queue/chat-rooms/${roomId}`, subscribeCallback, {
Authorization: access,
});
}
};
subscribeCallback 함수입니다. 이 함수는 client.subsribe의 세 번째 인수로 넣어주었으며, subscribe 즉 구독한 채팅방에서 채팅을 전송하거나 채팅이 왔을 때 실행되는 함수입니다. 채팅이 왔을 때 채팅방 query를 refetch하여 화면을 서버와 최신화를 해줍니다.
- 이 부분은 추후에 서버로부터 refetch 받기 전에 클라이언트 내에서 state를 이용해 먼저 최신화할 수 있도록 추가 구현 예정입니다.
const subscribeCallback = () => {
refetchChatRoomList();
};
2.2 publisher ; publish한 url로 메시지가 이동
- publish, 즉 채팅 전송입니다.
- destination과 body를 함께 전송합니다.
- body에는 채팅방 id, 전송자 id, 채팅 내용을 담았습니다.
const publish = (client, roomId, senderId, chat) => {
client.publish({
destination: '/app/send',
body: JSON.stringify({
chatRoomId: roomId,
senderId: senderId,
content: chat,
}),
});
};
3. disconnect ; 연결 끊기
- 채팅방 페이지를 벗어나면, disconnect를 해줍니다.
useEffect(() => {
connect();
return () => {
disconnect();
};
}, []);
...
const disconnect = () => {
if (client != null && client.current.connected) {
client.current.deactivate();
}
};
채팅 시현 화면
Console에 찍힌 Debug
위에서 구현한 웹소켓의 debug를 console에 찍어본 화면입니다. 제가 채팅방에 접속하여, 채팅을 전송한 debug입니다. >>>은 제가 서버 측에 보낸 정보, <<<는 서버 측에서 저희에 온 정보입니다.
1. CONNECT : 서버와 Connect를 합니다.
- 이때, Authorization, 즉 토큰은 담아서 전송합니다.
2. CONNECTED : 연결이 완료되었습니다.
3. SUBSRIBE : subscribe 즉, 구독을 합니다.
4. SEND : 메시지를 전송하였습니다.
5. MESSAE : 메시지가 왔습니다.
- 이때, 아래 코드에 보면 subscription:sub-0이라 적혀있습니다. 구독한 채널에서 날아온 메시지라는 뜻입니다.
- destination:/queue/chat-rooms/7. 제가 subscribe 한 채널입니다.
- 위 메시지의 내용은 subsribe함수의 세 번째 인수인 subscribeCallback에서 받아서 사용할 수 있습니다.
Websocket API는 처음이라, 채팅 구현은 처음이라 시행착오를 많이 겪었습니다. 공식 문서도 참고하고, 다른 사람들 블로그를 여럿 읽어보며 참고하여 구현하였습니다. Stomp는 다른 자료들보다 문서가 적어 에를 많이 썼었는데, 해결하고 났을 때의 그 쾌감은 말로 달리 표현할 수 없더라구요. 제 블로그를 참고해서 채팅 API구현을 성공적으로 마쳤기를 바라며, 댓글로 질문 남겨주시면 아는 선에서 알려 드릴게요 😀
마지막으로 채팅 구현한 github 링크 공유하고 마칠게요 !
채팅방 목록 ; https://github.com/Att-ies/frontend/blob/dev/src/pages/chat/index.tsx
채팅방 내부 ; https://github.com/Att-ies/frontend/blob/dev/src/pages/chat/room.tsx
채팅 api ; https://github.com/Att-ies/frontend/blob/dev/src/apis/chat/socketConnect.js
'개발 일지 > 개발 일지' 카테고리의 다른 글
[개발 일지] React성능 최적화 (2) : React Query의 캐싱 기능 (1) | 2023.05.05 |
---|---|
[개발 일지] React성능 최적화 (1) : React.memo로 사용자 경험 개선하기 (0) | 2023.05.03 |
[개발 일지] Next.js에서 접근성 향상하기 (접근성 점수 84점 -> 97점 향상시킨 썰) (0) | 2023.04.27 |
[개발 일지] React Query - Custom Hook 만들기 (+Typescript) (0) | 2023.04.13 |
[개발 일지] JWT + 소셜 로그인을 정복해보자 (React + Spring Boot, Kakao Login, Naver Login) (0) | 2023.04.11 |
[개발 일지] Intersection Observer API & react-query를 이용한 무한스크롤 구현 (0) | 2023.02.28 |
[개발 일지] Suspense를 이용하여 모든 api에 Loading 화면을 설정하자 (NextJS, Suspense) (0) | 2023.02.18 |
[개발 일지] Axios instance 설정 끝장내기 (Axios, instance, interceptors, JWT) (0) | 2023.02.11 |