이번 섹션에서는 Slack에서 빼놓을 수 없는 기능 중 하나인 실시간 채팅을 구현하기 위해 웹 소켓을 사용하는 방법을 배운다. 웹 소켓은 사용해본 적이 없는데 안그래도 지금 진행하는 프로젝트에서 웹 소켓을 사용해야하는 상황이 생겨서 배울 수 있음에 반가웠다..ㅎㅎ 추가로 커스텀 스크롤바, 멘션 기능, 리버스 인피니트 스크롤링 등 각종 프론트 기술에 대해서도 다루어볼 예정이다.
1. Socket.io를 통한 웹 소켓 통신
Socket.io 전용 훅스 만들기
const useSocket = () => {
const socket = io.connect(`${backUrl}`); // 백과 연결
socket.emit('hello', 'world'); // 서버에 hello라는 이벤트 이름으로 'world'라는 데이터를 보냄
socket.on('message', (data) => { // 서버에서 보내는 데이터를 받아 이벤트 리스너처럼 사용
console.log(data);
});
- socket을 통해 백과 소통
- connect로 백과 연결하여 소켓을 사용할 수 있다.
- 쉽게 그냥 데이터를 emit으로 보내고 on으로 받는다고 보면 된다. (단 이벤트의 이름이 일치할 때!)
- 연결을 끊을 땐 disconnect를 사용한다.
- 웹 소켓도 API 명세서가 있으니 그걸 보고 작업하면 된다.
socket.io 사용에 있어 주의할 점
서버에 연결된 모든 사람과 소통할 수 있기 때문에 범위를 잘 정해주어야 한다.
-> Slack에 워크스페이스와 채널이 존재하듯 Socket에도 계층(namespace와 room)이 존재하는데, 워크스페이스 - namespace / 채널 - room 이런 식으로 대응시켜 사용한다.
2. react-custom-scrollbars
스크롤바를 커스텀할 수 있는 라이브러리
const ChatList: VFC<Props> = ({ chatData }) => {
const scrollbarRef = useRef(null);
const onScroll = useCallback(() => {
}, [])
return (
<ChatZone>
<Scrollbars autoHide ref={scrollbarRef} onScrollFrame={onScroll}> // 이 부분
{chatData?.map((chat) => (
<Chat key={chat.id} data={chat} />
))}
</Scrollbars>
</ChatZone>
);
};
- autoHide : 스크롤을 멈추면 자동으로 사라지게 하는 속성
- onScrollFrame : 스크롤 내릴 시 호출될 함수를 넣어줄 수 있다
3. day.js
날짜 변환 라이브러리
day.js vs moment.js
<span>{dayjs(data.createdAt).format('h:mm A')}</span>
<span>{moment(data.createdAt).format('h:mm A')}</span>
사용하는 방법에 있어서 moment와 차이점은 없으며,
불변성 유지, 가벼움, moment와 동일한 api 사용 측면에서 추천할 만한 라이브러리이다.
date-fns
Lodash 스타일
Luxon
불변성 유지가 안된다는 moment의 단점을 보완하기 위해 만들어진 라이브러리
=> 결론 : day.js, date-fns, Luxon 세 가지 중에 취향껏 골라서 사용하면 된다.
4. 멘션 기능 구현하기
react-mentions
<MentionsTextarea
id="editor-chat"
value={chat}
onChange={onChangeChat}
onKeyPress={onKeydownChat}
placeholder={placeholder}
inputRef={textareaRef}
allowSuggestionsAboveCursor
>
<Mention
appendSpaceOnAdd
trigger="@"
data={memberData?.map((v) => ({ id: v.id, display: v.nickname })) || []}
renderSuggestion={renderSuggestion}
/>
</MentionsTextarea>
- appendSpaceOnAdd : 멘션 시 커서를 한 칸 띄워주는 속성
- renderSuggestion : @ 입력시 멘션할 이름을 리스트로 추천해준다.
- Mention의 부모는 무조건 MentionsInput이어야 한다. 참고로 위의 코드에서는 MentionsInput이 안보이는데, Mention을 감싸고 있는 MentionsTextarea가 바로 MentionsInput에 해당하는 태그이다.
export const MentionsTextarea = styled(MentionsInput)`
font-family: Slack-Lato, appleLogo, sans-serif;
font-size: 15px;
padding: 8px 9px;
width: 100%;
`;
이건 styled component의 장점인데, 이렇게 괄호를 사용하여 기존에 존재하는 컴포넌트에도 css를 또 입힐 수 있다. 경험상 어떤 라이브러리에서 가져온 컴포넌트들은 이게 적용이 안되는 경우도 있는 것 같던데 이건 제대로 한 번 알아봐야 할 것 같다.
5. 정규표현식으로 문자열 변환하기
멘션을 나타내는 부분을 알아보기 쉽도록 하이라이트 효과 넣기
=> 정규 표현식 사용
문자열에서 특정한 패턴을 찾아내기 위함
regexify-string
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} />;
},
})
- pattern에서 정규표현식을 통해 문자열의 패턴을 찾고, decorator에서 찾은 부분을 바꿔줌
정규표현식
@[이름](아이디) 를 찾아보자
정규 표현식은 // 사이에 적어준다.
- g : flag, global의 g로, 모두 찾겠다는 의미 (g 안붙이고 그냥 // 이면 하나만 찾겠다는 의미)
- \ : escape, 특수기호들을 무력화해주며, 써주는 것이 안전함
- . : 모든 글자
- + : 한 개 이상 / 최대한 많이 찾음
- ? : 0개 이상
- \d : 숫자
- +? : 최대한 조금 찾음
- \n : 줄바꿈
- | : 또는
최대한 많이 / 조금 이 부분에 대해 추가 설명하자면 예를 들어
@[핑재]12](7) 이 경우에 +를 사용하면 '핑재]12'를 찾아내는 것이고, +?를 사용하면 '핑재'만 찾아내는 것을 의미한다.
6. ref 전달하기
React.forwardRef를 사용하여 ref를 다른 컴포넌트에 전달해줄 수 있다.
const FancyButton = React.forwardRef((props, ref) => { // ref 전달
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 전달된 ref를 받음
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
7. SWR을 통해 구현한 인피니트 스크롤링 (무한 스크롤)
Slack 특성상 이전 내용을 보려면 위로 올려야하기 때문에 우리가 구현해야 할 건 정확히는 '리버스 인피니트 스크롤링'이다.
const { data: chatData, mutate: mutateChat, revalidate, setSize } = useSWRInfinite<IDM[]>(
(index) => `/api/workspaces/${workspace}/dms/${id}/chats?perPage=20&page=${index + 1}`,
fetcher,
);
이와 같이 useSWR의 useSWRInfinite를 사용하여 구현할 수 있다.
const isEmpty = chatData?.[0]?.length === 0;
const isReachingEnd = isEmpty || (chatData && chatData[chatData.length - 1]?.length < 20) || false;
인피니트 스크롤 구현 시 isEmpty와 isReachingEnd를 선언해주면 좋은데, isEmpty를 통해 데이터가 비어있는지(더 이상 가져올 데이터가 없는 상태), isReachingEnd를 통해 끝에 다다랐는지 그 상태를 나타낼 수 있다.
const ChatList = forwardRef<Scrollbars, Props>(({ chatSections, setSize, isReachingEnd }, scrollRef) => {
const onScroll = useCallback(
(values) => {
if (values.scrollTop === 0 && !isReachingEnd) {
console.log('가장 위');
setSize((prevSize) => prevSize + 1).then(() => {
const current = (scrollRef as MutableRefObject<Scrollbars>)?.current;
if (current) {
current.scrollTop(current.getScrollHeight() - values.scrollHeight);
}
});
}
},
[scrollRef, isReachingEnd, setSize],
);
스크롤이 가장 위로 가면 페이지를 하나 추가해주고, 스크롤의 위치를 유지시켜준다.
const chatSections = makeSection(chatData ? chatData.flat().reverse() : []);
useSWRInfinite 사용 시 chatData는 2차원 배열이 되기 때문에 flat()을 이용해 1차원 배열로 만들어주면서 reverse 시켜준다.
이렇게 웹 소켓을 이용한 양방향 통신 방법부터 각종 기능들을 비교적 쉽고 빠르게 구현할 수 있는 라이브러리들을 다루는 방법을 알아보았는데, 라이브러리에 너무 의존적인 것 같으면서도 제로초님의 말씀처럼 많이들 사용하는 이런 라이브러리들을 공식 문서를 보면서 잘 다룰 줄 아는 것도 중요할 것 같다는 생각이 들었다. 그리고 무한 스크롤은 구현이 어렵다고 전에도 들었었는데 사실 지금도 이해가 잘 되진 않은 상태라, 다음에 한 번 직접 구현해보면서 머릿 속에 넣어보고 싶다! 점점 갈수록 배우는 것도 많은데 조금 휙휙 빠르게 지나가는 감이 있어서... 웹 소켓도 이번에 플젝에서 사용해보려면 조만간 복습이 필요할 것 같다.ㅠ
'프론트엔드 > React' 카테고리의 다른 글
[React/TS] Slack 클론코딩 7주차 - 보너스 (1) | 2023.12.03 |
---|---|
[React/TS] Slack 클론코딩 6주차 - 마무리하기 (1) | 2023.11.26 |
Redux(리덕스)란? 상태 관리 도구 리덕스에 대해 알아보자 (0) | 2023.11.07 |
[React/TS] Slack 클론코딩 3주차, 4주차 (1) | 2023.11.05 |
[React/TS] Slack 클론코딩 2주차 - 로그인, 회원가입 만들기 (1) | 2023.10.08 |