실시간 채팅을 구현하기위해 웹 소켓을 이용해 볼 것이다!
웹소켓이란?
웹 브라우저와 서버 간의 양방향 통신을 가능하게 하는 프로토콜이다. HTTP 프로토콜과 달리 웹소켓은 단일 TCP연결을 통해 지속적인 양방향 통신을 할 수 있다.
HTTP 프로토콜과 더 비교을 해보자면...
HTTP프로토콜은 기본적으로 클라이언트 - 서버 모델을 따르며, 요청 - 응답 방식으로 동작한다. 클라이언트가 서버에 요청을 보내면 서버가 그에 대한 응답을 반환하는 식이고, 클라이언트가 서버의 데이터 변경 사항을 실시간으로 확인하려면 주기적으로 요청을 보내야 한다.
반면에, 웹소켓은 클라이언트와 서버 간에 단일 TCP 연결을 유지하면서 양방향(전이중 full-duplex)으로 데이터를 실시간으로 주고 받을 수 있다. 이 연결은 열린상태로 유지되기 때문에 서버에서 클라이언트로 데이터가 전달될 때마다 새로운 요청이나 연결 설정이 필요하지 않다.
웹소켓의 연결 과정
1. Handshake: 클라이언트가 서버에 웹소켓 연결을 요청한다.
2. Bidirectional messages: 서버가 클라이언트의 연결 요청을 수락하면, 양방향 메시지 교환을 통해 웹소켓 연결을 확립한다. (이 때 서버와 클라이언트와 서버 간 프로토콜 버전, 확장 기능, 보안 등에 대한 정보를 주고받는다.)
3. One stable channel: 위의 핸드셰이크 과정이 완료되면 서버 간에 단일 연결이 유지된다.
이렇게 서버와 클라이언트의 연결이 완료되면 바이너리 데이터나 텍스트 형식의 메시지를 송수신 할 수 있다.
STOMP
하지만 웹소켓 프로토콜 자체만으로는 메시징 시스템을 구축하기에는 부족하기 때문에 STOMP(Simple Text Messaging Protocol)같은 메시징 프로토콜이 사용된다. Stomp는 웹소켓 위에서 동작하는 텍스트 기반 프로토콜로서 클라이언트와 서버가 전송할 메세지의 유형, 형식, 내용들을 정의하는 매커니즘으로, 브로커(메시징 서버)와 통신하고, topic를 구독(subscribe)해서 메시지를 받거나 발행(publish)할 수 있다. 연결시에 헤더를 추가하여 인증 처리 구현도 가능하다.
Stomp.js 의 client 사용하기
공식문서 한 번 스윽 읽어주고
https://stomp-js.github.io/api-docs/latest/classes/Client.html
stompjs@7.0.0, rx-stomp@2.0.0
Classes Client Info Source File Description STOMP Client Class. Part of @stomp/stompjs. Index Properties Public appendMissingNULLonIncoming Public beforeConnect Public brokerURL Public connectHeaders Public connectionTimeout Public debug Public discardWebs
stomp-js.github.io
타입스크립트를 사용하기 때문에 타입스크립트용 stompjs를 설치해주자
npm i @types/stompjs
우선 백엔드에서 먼저 코드를 서버 쪽 코드를 구현해 주었고, 그에 맞춰 클라이언트 코드를 구현하는 방식으로 진행했다.
그래서 뭘 해야하냐면....
1. 대기(채팅)방 "waiting-room/{roomId}" 페이지 접속
2. 접속하면 "/ws" 엔드 포인트에 핸드 쉐이크로 웹소켓 연결
3. 웹소켓에 잘 연결이 되면 "/sub/rooms/{roomId}"를 구독
4. 메시지 보내기는 "/pub/rooms/{roomId}"로 보내기
를 진행하면 된다.
먼저 기본으로 사용할 client 인스턴스를 생성한다
stompClient.tsx
import * as StompJs from '@stomp/stompjs';
const stompClient = new StompJs.Client({
brokerURL: 'ws://localhost:8080/api/ws',// '/ws' 엔드포인트에 연결
debug: function (str) {
console.log(str); //디버깅 메시지 출력하기
},
reconnectDelay: 5000, //연결이 끊어지면 5초 후에 재연결 시도
heartbeatIncoming: 4000, //4초마다 heartbeat메시지를 주고받아 연결상태를 유지
heartbeatOutgoing: 4000,
});
export default stompClient;
brokerURL에 핸드쉐이트 엔드포인트를 넣어주고, 재연결을 시도할 시간 주기와 heartbeat 전송 주기를 설정해준다.
waiting-room/[id]/page.tsx
const WaitingRoom = ({ params }: { params: { id: number } }) => {
const { id: userId } = useUserInfoStore();
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const subscribeToRoom = (roomId: number) => {
stompClient.subscribe(`/sub/rooms/${roomId}`, (message) => {
const chatMessage = JSON.parse(message.body);
console.log('Received message:', chatMessage);
});
};
stompClient.onConnect = () => {
console.log('Connected to WebSocket');
setIsConnected(true);
subscribeToRoom(params.id);
};
stompClient.onDisconnect = () => {
setIsConnected(false);
console.log('Disconnected from WebSocket');
};
stompClient.activate();
return () => {
stompClient.deactivate();
};
}, []);
return (
<div className={styles.roomContainer}>
<div className={styles.wideArea}>
<div className={styles.userList}>
<UserList />
</div>
<div className={styles.chattingArea}>
<ChatInput
roomId={params.id}
userId={userId}
disabled={!isConnected}
/>
</div>
</div>
</div>
);
};
export default WaitingRoom;
위에서 만든 client instance를 가져와서 최초 페이지 마운트 시에 onConnect와 onDisconnect 핸들러를 설정한다.
onConnect 시에는 ChatInput에 전달하는 연결 상태를 true로 바꿔주고, subsribeToRoom함수를 실행시켜 roomId를 구독하도록 했다.
연결상태를 false로 업데이트 하는 onDisconnect 핸들러도 설정해주고, activate() 메서드를 호출해 웹소켓 연결을 시작.
언마운트 시에는 client를 비활성화하는 것도 잊지 말자.
publish를 하는 입력 창 컴포넌트 ChatInput.tsx
const ChatInput = ({
roomId,
userId,
disabled,
}: {
roomId: number;
userId: number | undefined;
disabled: boolean;
}) => {
const [message, setMessage] = useState('');
// 채팅 메시지 보내기
const sendChatMessage = ({
chatType,
roomId,
userId,
message,
}: ChatMessage) => {
const chatMessage = {
chatType,
roomId,
userId,
message,
};
stompClient.publish({
destination: `/pub/rooms/${roomId}/chat`,
body: JSON.stringify(chatMessage),
});
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!message) return;
sendChatMessage({
chatType: ChatType.TEXT,
roomId,
userId,
message,
});
setMessage('');
};
return (
<form onSubmit={handleSubmit}>
<TextInputField
placeholder={disabled ? '연결 중 입니다...' : '메시지를 입력하세요'}
isChat
value={message}
onChange={(e) => setMessage(e.target.value)}
endAdornment={<button type="submit" className={submitButton}></button>}
disabled={disabled}
/>
</form>
);
};
export default ChatInput;
publish() 메소드를 통해 메시지를 전송할 때는 destination에 message를 발행할 목적지를 넣어 메시지 타입에 맞게 body를 작성하면 된다.
page의 isConnected는 ChatInput의 disabled라는 prop에 반대 값으로 가져와서 소켓 미연결시 publish를 할 수 없도록 처리했다.
테스트 결과
서로 보낸 메시지가 실시간으로 잘 들어온다!
다음에는 실시간으로 소켓에 접속한 인원을 띄우고, 각각의 유저가 보낸 메시지를 표시해보는 테스트를 해 볼 것이다.
'Front-End' 카테고리의 다른 글
[Next.js] Next.js 14에서 페이지 이동 감지하기 (0) | 2024.04.12 |
---|---|
[React / Next.js + Zustand] StompJS로 채팅 구현하기 (2) - 실시간 채팅 방 상태 관리하기 (2) | 2024.04.05 |
Next.js에서 Axios Interceptor 작성하기 (feat. Zustand, localStorage) (1) | 2024.03.23 |
[Next.js + Vanilla Extract css + Storybook] 공용 컴포넌트 만들기 (feat. 절대경로) (0) | 2024.03.15 |
[Peer 스터디] 웹 접근성에 대해 알아보자... (0) | 2024.03.15 |