우리 프로젝트에서는 사용자마다 한 소켓을 사용한다.
소켓으로 주고받는 데이터는 크게 두 카테고리로 분류할 수 있다.
캔버스 데이터는 항상 주고받아야하기 때문에,
캔버스가 렌더링 되는 순간 소켓을 연결한다.
하지만 채팅은 그렇게되면 쓸데없는 데이터양이 늘어나기때문에
채팅 버튼을 클릭했을 때 채팅 소켓을 연결하도록 설계했다.
[ 좌측 하단 파란색 버튼이 채팅버튼]
export const useChatSocket = (
onMessageReceived: (message: ChatMessage) => void,
onChatError: (error: ChatError) => void,
group_id: string,
user_id: string
) => {
useEffect(() => {
// 유효하지 않은 group_id이면 소켓 연결 안 함
if (!group_id || group_id === '0' || !user_id) {
console.log('소켓 연결 스킵: 유효하지 않은 group_id 또는 user_id');
return;
}
// 채팅 이벤트 리스너 등록
socketService.onChatMessage(onMessageReceived);
socketService.onChatError(onChatError);
// 채팅방 참여
socketService.joinChat({ group_id });
console.log(`채팅방 참여: group_id=${group_id}`);
return () => {
// 클린업 시 이벤트 리스너 제거
socketService.offChatMessage(onMessageReceived);
socketService.offChatError(onChatError);
};
}, [group_id, user_id, onMessageReceived, onChatError]);
그런데 이상하게도 마이페이지, 그룹 등 다른 메뉴 버튼을 클릭할 때마다 콘솔에 이런 로그가 계속 찍혔다.
채팅 소켓을 계속해서 시도하고 있는 것이다.
Chat 컴포넌트가 App.tsx에서 항상 렌더링되고 있었기 때문이었다.
마이페이지든 뭐든 열릴 때마다 App 전체가 리렌더링 → Chat도 매번 실행 → useEffect
// App.tsx
return (
<main>
<PixelCanvas />
<Modal isOpen={isMyPageModalOpen}> {/* 마이페이지 모달 */}
<MyPageModalContent />
</Modal>
<Chat /> {/* ← 항상 렌더링! 마이페이지와 상관없이 */}
</main>
);
Chat 컴포넌트를 조건부 렌더링 하거나, 메모이제이션 으로 불필요한 리렌더링을 방지해야 한다.
소켓이 계속 재연결되어 불필요한 네트워크 트래픽이 발생할 수 있다.
React.memo는 컴포넌트를 메모이제이션(memoization) 해서, props가 바뀌지 않으면 리렌더링을 생략해주는 기능이다.
// 일반 컴포넌트
function Chat() { ... }
// 메모이제이션 적용
const Chat = React.memo(function Chat() { ... });
부모 컴포넌트가 리렌더링돼도,
자식 컴포넌트(Chat)의 props가 이전과 동일하다면,
React는 불필요한 리렌더링을 하지 않음
즉, 마이페이지를 열어도 Chat은 리렌더링되지 않게 된다!
const Chat = React.memo(function Chat() {
...
});
React.memo는 props가 바뀌지 않으면 컴포넌트를 리렌더링하지 않도록 막아준다.
덕분에 App이 리렌더링되더라도 Chat 컴포넌트는 props가 같다면 재실행되지 않는다.
{isChatOpen && <Chat />}
이번 경험을 통해 리액트 성능 최적화의 핵심은 "불필요한 리렌더링을 줄이는 것"이라는 걸 체감했다.
React.memo는 간단하지만 매우 강력한 도구인듯하다.
앞으로 적극적으로 활용하게 될 것 같다.
리액트를 왜 쓰는지 조금씩 알아가는 중 .. !
frontend 솜씨가 좋으시네요