소켓관련문제 - dltmdrbtjd/HANG GitHub Wiki
- 한 이벤트가 on 될 때마다 이전에 발생했던 이벤트의 결괏값이 같이 넘어오는 현상
- 상태 값의 변화가 빈번하게 일어나 페이지 리렌더링이 너무 자주 수행되는 현상
const [chatLog, setChatLog] = React.useState([]);
React.useEffect(() => {
socket.on('updateMessage', (data) => {
setChatLog([...chatLog, data]);
});
scrollToBottom();
}, [chatLog]);
const sendMessage = () => {
if (message) {
socket.emit('sendMessage', {
roomName,
targetPk: targetUserPk,
message,
userPk,
});
setMessage('');
}
};
위 코드는 sendMessage
이후 chatLog
의 상태가 변경될 때마다 updateMessage
가 새롭게 on되는 코드입니다. 위 코드를 실행하면 새로운 메시지를 보낼 때마다 채팅방 렌더링 속도가 느려지고 심하면 홈페이지 자체가 먹통이 되는 문제가 생겼습니다.
문제 파악
-
chatLog
가 변경될 때만 리렌더링 되어야 하는 컴포넌트들이 모든state
가 업데이트 될 때마다 리렌더링 되어서 속도가 느려지는게 아닐까?
해결 방안
-
updateMessage
가 on 될 때마다chatLog
배열 자체를 업데이트 하지말고 다른state
를 업데이트 한 후chatLog
배열에 적용시키자 -
SpeechBubble
컴포넌트를chatLog
배열이 업데이트 될 때만 리렌더링되도록 코드를 개선하자
const ShowChatLog = React.memo<ShowChatLogType>(({ userPk, chatLogs }) => {
return (
<>
{chatLogs.map((chat, idx) => (
<SpeechBubble
person={userPk === chat.userPk}
next={idx < chatLogs.length - 1 ? chat.userPk === chatLogs[idx + 1].userPk : false}
key={(Date.now() + Math.random() + idx).toString(36)}
>
{chat.message}
</SpeechBubble>
))}
</>
);
});
const [chatLogs, setChatLogs] = React.useState<ChatLogType[]>([]);
const [chatLog, setChatLog] = React.useState<ChatLogType>({
curTime: 0,
message: '',
userPk: 0,
});
React.useEffect(() => {
socket.on('updateMessage', (data) => {
setChatLog(data);
});
setChatLogs(chatLogs.concat(chatLog));
}, [chatLog]);
const sendMessage = () => {
if (message) {
socket.emit('sendMessage', {
roomName,
targetPk: targetUserPk,
message,
userPk,
});
setMessage('');
}
};
위 코드 적용 결과 채팅 작성 시마다 말풍선이 리렌더링되고 채팅이 버벅이다가 튕겨버리는 문제는 해결되었습니다.
하지만 여전히 updateMessage
가 수행되면 이전에 보냈던 메시지도 같이 넘어오는 현상은 남아있었고, 간헐적으로 채팅이 먹통이 되는 문제도 발생했습니다.
문제 파악
- 문제는
socket
에 대한 이해 부족으로 이벤트를 on 시키는 로직 자체가 잘못 짜여진 것 이었습니다. -
socket
에서 이벤트를 on 시키면socket
은 일정 시간마다 서버에 동기적으로 요청을 보내고 사용 가능한 정보가 있으면 정보를 받아와서 이벤트에 연결되어 있는 로직을 수행하는 방식으로 동작합니다. 따라서 이벤트를 on 시키는 코드는 최초 한 번만 실행되면 되는 것이고 위처럼 코드를 작성하면chatLog
가 변경될 때마다 새로운 이벤트를 on 시키며 서버에서 사용가능한 정보를 모두 받아오는 것이 문제였습니다.
해결 방안
-
updateMessage
이벤트는 무조건 한 번만 on 되도록 수정하자
React.useEffect(() => {
dispatch(CheckChatAlarm(targetPk));
socket.emit('join', {
joiningUserPk: userPk,
targetUserPk: targetPk,
nickname,
});
socket.on('chatLogs', (logs) => {
const addedChatLog = logs.chatLogs.map((log: string) => JSON.parse(log));
chatLogState.setChatLogs(addedChatLog);
});
socket.on('updateMessage', (data) => {
chatLogState.setChatLogs((chatLog) => chatLog.concat(data));
});
return () => {
socket.emit('leave', { roomName, userPk });
socket.off('chatLogs');
socket.off('updateMessage');
};
}, []);
-
updateMessage
는 채팅방에 들어갈 때 한 번만 on 되도록 수정하였고, 채팅방을 벗어난다면updateMessage
를 off 시키는 방법으로 코드를 개선하였습니다. 적용 결과 채팅이 간헐적으로 먹통이 되는 문제, 채팅을 여러개 입력하면 홈페이지가 버벅이는 문제가 해결되었습니다.
import React from 'react';
// redux
import { useDispatch, useSelector } from 'react-redux';
import { DeleteChatRoom, ChatAlarmCheck, getUnchecked } from 'src/redux/modules/ChatModule/chat';
// socket
import socket from 'src/util/socket';
// apis
import apis from 'src/shared/api';
// moment
import moment from 'moment';
// history
import { history, useTypedSelector } from '../../../redux/configureStore';
// user info
import { delUserInfo, getUserInfo } from '../../../shared/userInfo';
// elements
import { Grid, Text, Input, Button, Container } from '../../../elements';
// components
import SpeechBubble from './SpeechBubble';
import RoomHeader from './RoomHeader';
import Modal from '../../../components/Modal';
// style
import { WarningText, ChatInputAreaSize } from './style';
import { setMediaLimitBoxSize } from '../../../styles/Media';
const ChatRoom = () => {
const targetUserInfo = getUserInfo('targetUserInfo');
const targetUserPk = targetUserInfo.targetPk;
const dispatch = useDispatch();
const alarmCount = useTypedSelector((state) => state.chat.alarmCount);
const unchecked: number = useSelector(getUnchecked(targetUserPk));
const { userPk, nickname } = getUserInfo('userInfo');
const [chatLog, setChatLog] = React.useState([]);
const [message, setMessage] = React.useState('');
const [open, setOpen] = React.useState(false);
const messageRef = React.useRef(null);
const roomName =
(userPk < targetUserPk && `${userPk}:${targetUserPk}`) || `${targetUserPk}:${userPk}`;
const QuitRoom = () => {
socket.emit('ByeBye', { roomName, userPk });
dispatch(DeleteChatRoom(targetUserPk));
history.replace('/chat');
};
const BlockUser = () => {
apis
.AddBlockList({ targetPk: targetUserPk })
.then(() => QuitRoom())
.catch((err) => console.log(err));
};
const scrollToBottom = () => {
if (messageRef.current) {
messageRef.current.scrollIntoView({ block: 'end' });
}
};
React.useEffect(() => {
if (alarmCount > 0) dispatch(ChatAlarmCheck(alarmCount - unchecked));
socket.emit('join', { joiningUserPk: userPk, targetUserPk, nickname });
socket.on('chatLogs', (logs) => {
const addedChatLog = logs.chatLogs.map((log) => JSON.parse(log));
setChatLog(addedChatLog);
});
return () => {
socket.emit('leave', { roomName, userPk });
delUserInfo('targetUserInfo');
};
}, []);
React.useEffect(() => {
socket.on('updateMessage', (data) => {
setChatLog([...chatLog, data]);
});
scrollToBottom();
}, [chatLog]);
const sendMessage = () => {
if (message) {
socket.emit('sendMessage', {
roomName,
targetPk: targetUserPk,
message,
userPk,
});
setMessage('');
}
};
const DATEFORMAT = 'YYYY년 M월 D일';
const weekdays = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'];
const date = chatLog[0] ? moment(chatLog[0].curTime) : moment();
return (
<div ref={messageRef}>
<Container>
<Grid margin="0 0 95px">
<RoomHeader methods={[QuitRoom, () => setOpen(true)]} targetUserInfo={targetUserInfo} />
<Text
fs="xs"
wb="keep-all"
color="darkG"
padding="10px 12px"
ls="-0.5px"
addstyle={WarningText}
>
매너있는 채팅 부탁드립니다.
<br />
약속을 일방적으로 파기하거나 지키지 않을 경우 제재 대상이 될 수 있습니다.
</Text>
<Text fs="xs" textAlign="center" margin="0 0 20px">
{`${date.format(DATEFORMAT)} ${weekdays[date.days()]}`}
</Text>
{chatLog.map((chat, idx) => (
<SpeechBubble
person={userPk === chat.userPk}
next={idx < chatLog.length - 1 ? chat.userPk === chatLog[idx + 1].userPk : false}
key={(Date.now() + Math.random() + idx).toString(36)}
>
{chat.message}
</SpeechBubble>
))}
<Grid
position="fixed"
bottom="110px"
left="50%"
translate="-50%, 0"
width="90%"
radius="12px"
bgColor="white"
border="1px solid #E7E7E7"
isFlex
hoz="space-between"
ver="center"
addstyle={setMediaLimitBoxSize('768px')}
>
<Input
placeholder="채팅 내용 입력"
border="none"
value={message}
addstyle={ChatInputAreaSize}
_onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMessage(e.target.value)}
_onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) =>
e.key === 'Enter' ? sendMessage() : null
}
/>
<Button padding="6px 15px" margin="0 9px 0 0" _onClick={() => sendMessage()}>
전송
</Button>
</Grid>
</Grid>
<Modal
open={open}
close={() => setOpen(false)}
mainText="차단하기"
subText2={`${targetUserInfo.nickname} 님을 정말 차단시겠습니까?`}
agreeText="확인"
agree={BlockUser}
/>
</Container>
</div>
);
};
export default React.memo(ChatRoom);
위와 같은 구조는 하나의 state가 변경될 때마다 모든 컴포넌트가 리렌더링되는 단점이 있습니다. input 창에서 메시지를 입력할 때마다 채팅방 전체가 리렌더링 되는데 실시간으로 새로운 컴포넌트가 추가되는 특성상 내부 컨텐츠가 많아질 수 밖에 없어 메시지 입력 시마다 모든 컴포넌트가 리렌더링된다면 페이지의 성능이 저하될 수 밖에 없는 구조입니다.
해결 방안
- 컴포넌트 리렌더링에 영향을 미치는
state
별로 나누자 - 파일이 여러개로 나뉘면
state
를 관리하기 어려워지니Context API
를 사용해state
를 관리하자
import React from 'react';
// user info
import { getUserInfo } from 'src/shared/userInfo';
// socket
import { socket } from 'src/util/socket';
// redux
import { useDispatch } from 'react-redux';
import { CheckChatAlarm } from 'src/redux/modules/ChatModule/chat';
export const chatStatus = React.createContext(null);
export interface ChatLogType {
curTime: number;
message: string;
userPk: number;
}
const useProviderChatLogs = () => {
const { targetPk } = getUserInfo('targetUserInfo');
const { userPk } = getUserInfo('userInfo');
const roomName = (userPk < targetPk && `${userPk}:${targetPk}`) || `${targetPk}:${userPk}`;
const [chatLogs, setChatLogs] = React.useState<ChatLogType[]>([]);
const [inputBoxHeight, setInputBoxHeight] = React.useState<number>(90);
const chatLogState = {
chatLogs,
setChatLogs,
};
const inputBoxHeightState = React.useMemo(
() => ({
inputBoxHeight,
setInputBoxHeight,
}),
[inputBoxHeight]
);
return {
roomName,
chatLogState,
inputBoxHeightState,
};
};
const ChatContext = ({ children }) => {
const chat = useProviderChatLogs();
const { roomName, chatLogState } = chat;
const { targetPk } = getUserInfo('targetUserInfo');
const { userPk, nickname } = getUserInfo('userInfo');
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(CheckChatAlarm(targetPk));
socket.emit('join', {
joiningUserPk: userPk,
targetUserPk: targetPk,
nickname,
});
socket.on('chatLogs', (logs) => {
const addedChatLog = logs.chatLogs.map((log: string) => JSON.parse(log));
chatLogState.setChatLogs(addedChatLog);
});
socket.on('updateMessage', (data) => {
chatLogState.setChatLogs((chatLog) => chatLog.concat(data));
});
return () => {
socket.emit('leave', { roomName, userPk });
socket.off('chatLogs');
socket.off('updateMessage');
};
}, []);
return <chatStatus.Provider value={chat}>{children}</chatStatus.Provider>;
};
export default ChatContext;
위처럼 파일별로 공유되어야 하는 state
의 경우 Context API
에 담아 하위 컴포넌트에서 사용할 수 있도록 변경하였습니다.
import React from 'react';
// context
import { chatStatus, ChatLogType } from '../ChatContext';
// user info
import { getUserInfo } from '../../../../shared/userInfo';
// elements
import { Text, Grid } from '../../../../elements';
// style
import { SpeechBubbleStyle } from './style';
const SpeechBubble = () => {
const { userPk } = getUserInfo('userInfo');
const { chatLogState } = React.useContext(chatStatus);
return (
<>
{chatLogState.chatLogs.map((chat: ChatLogType, idx: number) => {
const person = userPk === chat.userPk;
const next =
idx < chatLogState.chatLogs.length - 1
? chat.userPk === chatLogState.chatLogs[idx + 1].userPk
: false;
return (
<Grid
isFlex
hoz={person && 'flex-end'}
margin={next ? '0 0 8px' : '0 0 16px'}
key={(Date.now() + Math.random() + idx).toString(36)}
>
<Text
padding="16px"
color={person ? 'white' : 'black'}
addstyle={SpeechBubbleStyle(person)}
>
{chat.message}
</Text>
</Grid>
);
})}
</>
);
};
export default React.memo(SpeechBubble);
위와 같이 모든 컴포넌트를 사용하는 state
별로 나누어 관리하고, 해당 state
값이 변경될 때만 리렌더링되도록 코드를 개선했습니다.
적용 결과 메시지 입력 시 input 창만 리렌더링되는 것을 알 수 있습니다.