npm install socket.io-client
const socket = io.connect(backUrl); // 서버(backUrl)에 소켓 연결을 시도합니다.
socket.emit('hello', 'world'); // 'world' 데이터를 'hello' 이벤트로 서버에 보냅니다.
socket.on('message', (data) => { // 서버에서 오는 "message" 이벤트를 수신합니다.
console.log(data)
});
socket.disconnect() // 서버와의 연결을 끊습니다.
Socket.IO 는 WebSocket 기반의 라이브러리로 실시간 양방향 통신을 구현할 수 있습니다.
import { useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
const backUrl = process.env.NODE_ENV === 'production' ? 'https://sleact.nodebird.com' : 'http://localhost:3095';
const sockets: { [key: string]: Socket } = {};
const useSocket = (workspace: string): [Socket | undefined, () => void] => {
const disconnect = useCallback(() => {
if (workspace && sockets[workspace]) {
console.log('소켓 연결 끊음');
sockets[workspace].disconnect();
delete sockets[workspace];
}
}, [workspace]);
if (!workspace) {
return [undefined, disconnect];
}
if (!sockets[workspace]) {
sockets[workspace] = io(`${backUrl}/ws-${workspace}`, {
transports: ['websocket'], // 구형 브라우저를 위한 http 통신을 보내지 않고 즉시 ws 통신을 시도합니다.
}); // Namespace별로 한 번만 Socket을 생성하여 중복 연결을 방지합니다.
console.info('create socket', workspace, sockets[workspace]);
sockets[workspace].on('connect_error', (err) => {
console.error(err);
console.log(`connect_error due to ${err.message}`);
});
}
return [sockets[workspace], disconnect];
};
export default useSocket;
React 의 컴포넌트내에서 Socket 연결 시, 컴포넌트 리렌더링마다 새로운 Socket 연결이 생겼다가 끊기는 불필요한 재연결, 이벤트 중복 등록 등의 성능 문제 발생할 수 있습니다. 위의 코드를 통해 sockets 객체를 통해 Socket 들을 관리하고 이미 존재하는 Namespace 의 중복 생성을 방지하여 위의 문제를 해결합니다.
// front/layouts/Workspace
const [socket, disconnect] = useSocket(workspace);
useEffect(() => {
if (userData && channelData && socket) {
socket.emit('login', { id: userData.id, channels: channelData.map((channel) => channel.id) });
}
}, [userData, channelData, socket]);
useEffect(() => {
return () => {
disconnect();
};
}, [workspace, disconnect]);
// front/components/DMList
useEffect(() => {
socket?.on('onLineList', (data: number[]) => {
setOnlineList(data);
});
return () => {
socket?.off('onLineList');
};
}, [socket]);
위의 코드는 유저가 로그인 후 특정 워크스페이스에 접속 시 서버에게 알리고 서버는 해당 워크스페이스의 로그인 되어있는 유저들을 알려주는 코드입니다.
먼저 워크스페이스 로딩 완료되었을 때 emit 을 통해 login 이벤트로 필요한 데이터를 서버에 전달하면, 서버는 on 을 통해onLineList 이벤트로 현재 접속중인 유저들의 데이터를 전달해줍니다.
불필요한 리소스 사용 방지를 위해 Socket 은 disconnect 를 해주어야 하고, 이벤트 중복 실행 방지를 막기 위해 Socket 의 이벤트도 언제나 잘 꺼주어야 합니다.
npm install react-custom-scrollbars
react-custom-scrollbars 는 React 에서 스크롤바를 자유롭게 커스터마이징할 수 있게 해주는 라이브러리입니다.
import Scrollbars from 'react-custom-scrollbars';
const ChatList = ({ chatData }: Props) => {
const scrollbarRef = useRef(null);
const onScroll = useCallback(() => {}, []);
return (
<ChatZone>
<Scrollbars autoHide ref={scrollbarRef} onScrollFrame={onScroll}>
{chatData?.map((chat) => (
<Chat data={chat} key={chat.id} />
))}
</Scrollbars>
</ChatZone>
);
};
export default ChatList;
autoHide 옵션을 사용하여 스크롤바를 숨길 수 있습니다.
autoHideTimeout 옵션을 사용하여 스크롤바가 사라지기까지의 시간(ms)을 설정할 수 있습니다.
autoHideDuration 옵션을 사용하여 사라질 때 애니메이션 시간(ms)을 설정할 수 있습니다.
onScrollFrame 옵션은 스크롤 상태를 0~1 범위의 비율로 제공하여 그에 따른 조작을 할 수있고,
onScroll 원본 DOM 이벤트와 거의 동일하여 스크롤 컨테이너 DOM 요소를 관리할 수 있습니다.
npm install dayjs
dayjs는 JavaScript에서 날짜와 시간을 다루기 편하게 만들어주는 라이브러리입니다.
가볍고 불변성을 가지고 있습니다.Moment.js 와 거의 동일한 API 를 가지고 있어 쉽게 전환이 가능합니다.
import dayjs from 'dayjs';
const now = dayjs(); // 현재 시간
const date = dayjs('2025-09-01'); // 특정 날짜
// 포맷팅
dayjs().format('YYYY-MM-DD'); // 2025-09-18
dayjs().format('YYYY/MM/DD HH:mm:ss'); // 2025/09/18 16:30:45
// 날짜 연산
dayjs().add(1, 'day'); // 내일
dayjs().subtract(2, 'month'); // 두 달 전
// 비교
dayjs('2025-09-18').isBefore('2025-10-01'); // true
dayjs('2025-09-18').isAfter('2025-01-01'); // true
// Diff 계산
dayjs('2025-10-01').diff('2025-09-18', 'day'); // 13 (일 단위 차이)
// Locale
import 'dayjs/locale/ko';
dayjs().locale('ko').format('YYYY년 MM월 DD일'); // 2025년 09월 18일
npm install react-mentions
react-mentions는 React에서 @멘션, #태그 기능을 쉽게 구현할 수 있게 도와주는 라이브러리입니다.
import { MentionsInput, Mention, SuggestionDataItem } from 'react-mentions';
const ChatBox = () => {
const renderSuggestion = useCallback(
(
suggestion: SuggestionDataItem,
search: string,
highlightedDisplay: React.ReactNode,
index: number,
focused: boolean,
): React.ReactNode => {
if (!memberData) return;
return (
<EachMention focused={focused}>
<img
src={gravatar.url(memberData[index].email, { s: '20px', d: 'retro' })}
alt={memberData[index].nickname}
/>
<span>{highlightedDisplay}</span>
</EachMention>
);
},
[memberData],
);
return (
<MentionsInput forceSuggestionsAboveCursor >
<Mention
appendSpaceOnAdd
trigger="@"
data={memberData?.map((data) => ({ id: data.id, display: data.nickname })) || []}
renderSuggestion={renderSuggestion}
/>
</MentionsInput>
)
}
export default ChatBox;
MentionsInput 컴포넌트내 Mention 컴포넌트가 위치하는 구조로 되어있습니다.
forceSuggestionsAboveCursor 를 MentionsInput 에 사용시 멘션이 항상 위로 배치 가능합니다.
allowSuggestionsAboveCursor 를 사용하게 된다면 아래 공간이 부족하면 위에 배치가 가능하도록, 즉 워크스페이스 사람이 적으면 아래로 배치됩니다.
Mention 컴포넌트의 appendSpaceOnAdd는 멘션 선택 후 자동으로 한 칸 띄어쓰기 추가됩니다. trigger="@" 를 통해 @ 입력 시 리스트를 활성화시켜줍니다. renderSuggestion 리스트의 항목을 렌더링할 때 커스텀 UI 적용할 수 있습니다.
renderSuggestion 함수를 보면 suggestion 를 통해서는 데이터를 가져올 수 있고, search 는 현재 입력한 검색어를 알 수 있습니다. highlightedDisplay를 통해 리스트의 항목을 하이라이트 처리가 가능합니다. focused 는 현재 포커스된 항목을 표시합니다.
문자열의 특별한 패턴을 프로그래밍에서 구현할 때 사용
npm install regexify-string
regexify-string 은 문자열을 정규식 기준으로 분리하고, React 컴포넌트 등으로 쉽게 매핑할 수 있게 해주는 라이브러리입니다.
const result = useMemo(
() =>
regexifyString({
input: data.content,
pattern: /@\[(.+?)\]\((\d+?)\)|\n/g,
decorator(match, index) {
const arr: string[] | null = match.match(/@\[(.+?)\]\((\d+?)\)/);
if (arr) {
return (
<Link key={match + index} to={`/workspace/${workspace}/dm/${arr[2]}`}>
@{arr[1]}
</Link>
);
}
return <br key={index} />;
},
}),
[data.content],
);
Socket.IO는 이전에 사용해본 경험이 있어 이해하기 어렵지 않았습니다. 다만 당시에는 중복된 소켓 생성, 이벤트 중복에 대한 개념이 부족하여 해당 개념을 익히는데 도움이 되었습니다. 또한 정말 다양한 라이브러리가 또 다시 React 생태계의 장점을 잘 말해주고 있다고 느겼습니다. 그리고 정규표현식은 항상 미뤄왔지만 이제는 배워야 할 시간이 온 것 같습니다.