이번 프로젝트에서 웹소켓을 통해 게시물의 좋아요 수 및 댓글의 실시간 업데이트 기능을 구현하였다.
프론트엔드에서는 웹소켓을 통해 전송하는 데이터의 형태를 정확히 설정하고,
웹소켓을 통해 전송받은 데이터를 필요에 맞게 처리만 해주면 어려운 것은 딱히 없다.
조건
useWebSocket은 react 16.8 이상에서 사용 가능하고,
리액트 훅을 사용할 수 있는 함수형 컴포넌트에서 사용해야 한다.
이번 프로젝트 개발환경 세팅은 아래와 같았당.
next: 15.1.0
react: 19.0.0
useWebSocket 설치하기
pnpm add react-use-websocket
npm install react-use-websocket
yarn add react-use-websocket
프론트엔드에서 필요한 것은 useWebSocket 설치와 웹소켓 서버 주소만 있으면 되고,
웹소켓을 사용하고자 하는 컴포넌트에서 useWebSocket() 사용하면 된당.
useWebSocket()의 타입은 다음과 같다.

위의 적힌 대로, useWebSocket의 첫 번째 인자에는 웹소켓 서버 주소를 넣는당.
두 번째 인자에는 여러 옵션을 설정할 수 있당.
나는 열림, 닫힘, 오류에 대한 정보를 콘솔로 확인하고자 onOpen, onClose, onError 세 가지를 사용했다.
그리고 기본적으로 useWebSocket은 연결이 끊긴 경우 재연결 시도를 하지 않기 때문에 별도로 설정해야 한다.
그래서 shouldReconnect도 옵션 추가!
세 번째 인자에는 true/false 값을 전달하여 웹소켓 연결을 열지 닫을지 정할 수 있다.
const socketUrl = reviewId ? `${SOCKET_URL}?review_id=${reviewId}` : null;
const { sendJsonMessage, lastMessage, readyState } = useWebSocket(socketUrl, {
onOpen: () => console.log('✨ WebSocket 연결 열림'),
onError: error => console.log('🚨 WebSocket 에러', error),
onClose: () => console.log('💀 WebSocket 연결 닫힘'),
shouldReconnect: () => true,
});
나는 소켓 주소 뒤에 현재 페이지의 게시물의 id를 넣어줘야 해서 저렇게 설정했당.
저거 하면 다 한겨.
필요한 것만 구조 분해 할당을 통해 가즈와서 쓰면 된당!
나는 객체 형태의 데이터를 JSON 형태로 웹소켓을 통해 전송하려고 sendJsonMessage과
응답 메세지를 받을 lastMessage,
그리고 현재 웹소켓 연결상태를 알 수 있는 readyState를 구조 분해 할당으로 뽑아왔당.

위의 readyState의 enum을 참고하여 isSocketOpen 변수를 만들었당.
const isSocketOpen = readyState === 1;
const handleClickLike = () => {
if (!user) return setShowLoginPopup(true);
if (user.id === user_id) return toast(LIKE_MY_OWN_REVIEW_ERROR_MESSAGE);
if (!isSocketOpen) return toast(SOCKET_ERROR_MESSAGE);
if (isLiked)
sendJsonMessage(
JSON.stringify({ type: 'like', user_id: user.id, review_id, is_liked: false }),
);
else if (!isLiked)
sendJsonMessage(
JSON.stringify({ type: 'like', user_id: user.id, review_id, is_liked: true }),
);
};
좋아요 클릭 시,
- 로그인 하지 않은 사용자의 경우, 로그인 경고 팝업창
- 로그인 사용자와 게시물 작성자가 같은 경우, 자신의 게시물 좋아요 못한다는 경고 토스트
- 웹소켓이 닫혀있는 경우, 오류 발생 토스트
- 로그인이 된 상태 + 로그인 사용자 ≠ 게시물 작성자 + 웹소켓 열린 경우,
- 좋아요 한 경우, 좋아요 취소 요청을 웹소켓을 통해 전송
- 좋아요 하지 않은 경우, 좋아요 요청을 웹소켓을 통해 전송
const onSubmit: SubmitHandler<CommentFormType> = data => {
const comment = data.content;
if (!user) return;
if (!isSocketOpen) return toast(SOCKET_ERROR_MESSAGE);
if (isSocketOpen) {
if (!comment_id) {
sendJsonMessage(
JSON.stringify({
type: 'comment',
method: 'POST',
review_id,
content: comment,
user_id: user.id,
}),
);
reset({ content: '' });
} else if (setShowForm && comment_id) {
sendJsonMessage(
JSON.stringify({
type: 'comment',
method: 'PATCH',
comment_id,
review_id,
content: comment,
user_id: user.id,
}),
);
setShowForm(false);
}
}
};
댓글 작성 및 수정 후 제출 시,
- 로그인 한 사용자가 아닌 경우, 낫띵 해쁜스. 근데 로그인 한 사용자가 아니면 아예 폼 자체를 숨겼기 때문에 submit할 일은 없을 것.
- 웹소켓이 닫혀있는 경우, 오류 발생 토스트
- 로그인 한 사용자 + 웹소켓이 열린 경우,
- comment_id가 없는, 즉 새 댓글 작성의 경우, method에 'POST'임을 명시하여 데이터 전달
- comment_id가 있는, 즉 이미 있는 댓글의 수정의 경우, method에 'PATCH'임을 명시하여 데이터 전달
이렇게 메세지를 전달하면 백엔드에서 데이터를 처리한 후 웹소켓을 통해 응답 메세지를 보내주게 되는데,
좋아요 같은 경우에는,
해당 응답 메세지를 토대로 리렌더링하였고,
댓글 같은 경우에는,
응답이 오면, 댓글 목록을 refetch하여 리렌더링하였다.
이 때! 그 응답 메세지는 나한테만 오는 게 아니라 해당 웹소켓과 연결된 사용자 모두에게 가는 것이므로,
그에 맞게 잘 처리해주어야 한당.
useEffect(() => {
if (lastMessage) {
const receivedData: SocketResponseType = JSON.parse(lastMessage.data);
if (receivedData.type === 'comment' && receivedData.review_id === review_id) {
commentRefetch();
if (user && receivedData.user_id === user.id) {
switch (receivedData.method) {
case 'POST':
toast(POST_COMMENT_SUCCESS_MESSAGE);
break;
case 'PATCH':
toast(EDIT_COMMENT_SUCCESS_MESSAGE);
break;
case 'DELETE':
toast(DELETE_COMMENT_SUCCESS_MESSAGE);
break;
default:
toast(SOCKET_ERROR_MESSAGE);
}
}
}
if (receivedData.type === 'like') {
if (receivedData.review_id === review_id) {
setLikeCount(receivedData.like_count);
if (user && receivedData.user_id === user.id) setIsLiked(receivedData.is_liked);
}
}
console.log('💬 websocket res:', receivedData);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessage]);
웹소켓에서 lastMessage가 올 때마다 실행 될 useEffect 훅
나는 하나의 웹소켓으로 좋아요와 댓글 두 가지의 정보를 받기 때문에 메세지에는 어떤 정보인지를 type에 명시하여 메세지를 주고받았다.
일단 첫 렌더링 시에는 좋아요와 댓글 모두 api 요청을 통해 렌더링을 한다.
좋아요의 경우, likeCount와 isLiked 상태를 만들어 첫 렌더링 되자마자 상태를 업데이트하고 해당 상태로 렌더링을 한다.
lastMessage가 왔을 때,
- type이 댓글인 경우,
- 댓글 목록 GET 요청 API를 refetch
- 응답 데이터로 리렌더링
- lastMessage에 있는 댓글 작성, 수정, 삭제한 사용자의 id가 현 사용자의 id와 같은 경우, 요청 성공 토스트를 띄웠당. 뿅
- type이 좋아요인 경우,
- 현재 후기 페이지의 id와 lastMessage의 review_id가 같은 경우, likeCount 상태 업데이트
- 현재 사용자의 id와 lastMessage의 user_id가 같은 경우, isLiked 상태 업데이트
- 현재 후기 페이지의 id와 lastMessage의 review_id가 같은 경우, likeCount 상태 업데이트
이로써 웹소켓 연결 완료
정리하자면,
- useWebSocket에 첫 번째 인자에 웹소켓 서버 주소 전달하고, 두 번째 인자에 필요한 옵션 설정하고, 세 번째 인자는 boolean 값으로 연결 여부를 전달!
- useWebSocket의 반환 값을 구조 분해 할당으로 가져온당.
- sendMessage 또는 sendJsonMessage: 웹소켓에 데이터를 보내줄 메서드
- lastMessage 또는 lastJsonMessage: 웹소켓을 통해 전달받는 메세지
- readyState: 웹소켓의 연결 상태를 알 수 있는 number 형태 데이터
- 입력이 있을 때 sendMessage 또는 sendJsonMessage를 통해 백엔드에서 필요한 데이터 전송
- lastMessage 또는 lastJsonMessage가 오면 메세지에 담긴 데이터 처리
끝...!
웹소켓 한대서 겁먹었는데 사실 프론트에선 데이터 형태만 정확하게 주고 받고, 데이터 처리만 제대로 해주면 되는 것 같았다.
백엔드에서 복잡한 로직 처리가 다 이루어지기 때문에,
프론트엔드에선 미리 코드 짤 수 있는 부분 최대한 짜두어 바로 연결해볼 수 있게 만들어 두면 덜 눈치가 보인당.
또 서로가 데이터 처리를 위해 각자 필요한 것을 잘 주고 받고,
프론트에서 연결 상태 잘 확인해주고,
오류 고치는 동안 잘 기다려주고,
하면 된당 ^_^
이게 연결되서 실시간으로 막 업데이트 되는 거 보면 오류나도 재밌당 ㅋ.ㅋ
그래서 백엔드 분과 재밌어하면서 작업해서 뿌듯 ^_^
'Utility' 카테고리의 다른 글
| 텍스트 에디터 Tiptap 커스터마이징 (0) | 2025.01.19 |
|---|---|
| 돌아가는 로딩 스피너 코드 (0) | 2024.12.23 |
| cva (Class Variance Authority) (1) | 2024.12.14 |
| clsx (0) | 2024.12.13 |
| Tailwind Merge (0) | 2024.12.13 |