
WebSocket: 실시간으로 client 와 server가 데이터를 주고받는 방법', 한번만 연결해주면 된다.
WebSocket이 있기 전에는 client에서 server로 주기적으로 데이터를 요청하는 폴링방식을 사용하였다. (ex. 30초 주기로 server에 데이터 요청)
import io, { Socket } from 'socket.io-client';
import { useCallback } from 'react';
const backUrl = 'http://localhost:3095';
const sockets: { [key: string]: Socket } = {};
const useSocket = (workspace?: string): [Socket | undefined, () => void] => {
console.log('rerender', workspace);
const disconnect = useCallback(() => {
if (workspace) {
sockets[workspace].disconnect();
delete sockets[workspace];
}
}, [workspace]);
if (!workspace) {
return [undefined, disconnect];
}
if (!sockets[workspace]) {
sockets[workspace] = io(`${backUrl}/ws-${workspace}`, {
transports: ['websocket'],
});
}
return [sockets[workspace], disconnect];
};
export default useSocket;
추가로 아래의 경우처럼 서버주소
backUrl을 변수로 따로 저장해 놓을 것!
예를 들어,.env파일에backendUrl값 저장을 위해process.env.REACT_APP_BACKENDURL에 해당 값을 저장한다.
const backUrl = "http://localhost:3095";
const useSocket = () => {
axios.get(`${backUrl}/api/users`);
}
npm i react-custom-scrollbars
npm i -D @types/react-custom-scrollbars
사용하고자 하는 컴포넌트를
Scrollbars에 감싼다
<Scrollbars autoHide ref={scrollRef} onScrollFrame={onScroll}>
{Object.entries(chatSections).map(([date, chats]) => {
return (
<Section className={`section-${date}`} key={date}>
<StickyHeader>
<button>{date}</button>
</StickyHeader>
{chats.map((chat) => (
<Chat key={chat.id} data={chat} />
))}
</Section>
);
})}
</Scrollbars>
속성값의 의미
autoHide: 자동으로 스크롤 바가 사라지도록 하는 속성
onScrollFrame: 스크롤 바를 내릴때 호출되며 이때 사용할 함수를 넣어준다
npm i luxon
npm i -D luxon
import {DateTime} from 'luxon';
//ISO 8601
const now = DateTime.local();
console.log(now.toISO()); // 2023-11-11T10:20:07.936+01:00
// 사람들이 보통 읽는 방식
const now = DateTime.local();
console.log(now.toLocaleString()); // 11/11/2023 (month, day, year순)
//토큰 기반 서식
const now = DateTime.local();
console.log(now.toFormat("yyyy-MM-dd HH:mm:ss")); //2023-11-11 10:21:57
npm i react-mentions
npm i -D @types/react-mentions
공식문서 예시
<MentionsInput value={this.state.value} onChange={this.handleChange}>
<Mention
trigger="@"
data={this.props.users}
renderSuggestion={this.renderUserSuggestion}
/>
<Mention
trigger="#"
data={this.requestTag}
renderSuggestion={this.renderTagSuggestion}
/>
</MentionsInput>
멘션하고 싶은 부분을
<Mention>를 사용하여 나타내고<MentionsInput>를
사용해서 전체를 묶어준다
각 속성의 의미
1.MentionsInput
value : 멘션을 위한 마크업이 포함된 값
onChange : 사용자가 멘션 입력값을 변경할 때 호출되는 콜백함수
Mention
trigger : 데이터 소스 쿼리를 트리거하는 문자열 시퀀스
data : 멘션 가능한 데이터 항목의 배열 (id 및 display 키를 갖는 객체 또는 쿼리 매개변수를 기반으로 배열을 반환하는 필터링 함수)
renderSuggestion : 멘션 제안이 렌더링되는 방식 (optional)
위처럼 간단하게 속성을 알아보았는데, 자세한건 공식문서를 참고하면서 만들어보면 더 쉽게 와닿을 것 같다!
기본적으로
부모 컴포넌트가 리렌더링 되는 경우자식 컴포넌트로 리렌더링 된다.
이때,
자식 컴포넌트를React.memo로 감싸주어서 (ex. React.memo(자식 컴포넌트)) 부모 컴포넌트가 바뀌어도 자식컴포넌트의 Props가 바뀌지 않는다면 자식 컴포넌트가 리렌더링 되지 않는다
useMemo는 성능을 최적화시키는 react hooks중 하나로,memoization을 통해 기존의 결괏값을 어딘가에 저장해 두고 동일한 입력이 들어오면 재활용한다.
useMemo는첫번째 인자로콜백함수,두 번째 인자로의존성 배열을 받고, 두번째 인자인 배열의 요소 값이 업데이트 될때만 콜백 함수를 다시 호출해서 memoization된 값을 업데이트 한다.
만약 의존성 배열로 빈배열을 넘겨주었을 경우 컴포넌트가 처음 마운트 되었을 때만 값을 계산하고 이후에는 항상 memoization된 값을 꺼내와서 사용한다.
사용 예시
const country = useMemo(() => {
return { country : isKorea ? "한국" : "한국아님"
}
}, [isKorea])
위 코드의 경우 country의 값은 isKorea의 값에 따라 바뀌며, isKorea의 값이 바뀌지 않을 경우, memoization된 값을 사용한다.
swr의 useSWRInfinite를 사용해서 아래와 같이 구현한다.
const { data: chatData, mutate: mutateChat, revalidate, setSize } = useSWRInfinite<IDM[]>(
(index) => `/api/workspaces/${workspace}/dms/${id}/chats?perPage=20&page=${index + 1}`,
fetcher,
);
코드 설명
index :
useSWRInfinite()함수의 첫번째 인수에 있는index의 경우, page의 수를 의미한다.
setSize : page 수를 바꿔주는 역할을 한다.
setSize를 이용해서 추가 값을 받아오는 과정 + 스크롤바 유지
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],
);
react-query에도 useInfiniteQuery 라는 훅을 통해 무한 스크롤을 구현할 수 있어서 한번 찾아보고 정리해보았다.
const { data, fetchNextPage, hasNextPage, isLoading, isError } = useInfiniteQuery(
['고유 쿼리 키값'],
({ pageParam = 0 }) => fetchFn({ page: pageParam, content: searchText, view: 5 }),
{
getNextPageParam: (lastPage, allPosts) => {
return lastPage.page !== allPosts[0].totalPage ? lastPage.page + 1 : undefined;
},
},
);
fetchFn을 통해 백엔드로 부터 얻어오는 값 (백엔드에서 정해주면 되는 값들)
page: 페이지 수를 뜻한다
totalPage: 총 페이지 수를 뜻한다
view: 한 페이지에 보여줄 게시물의 수
useInfiniteQuery의 data의 속성값
data.속성값으로 접근
1) pages : 데이터에 해당한다.
2) pageParams : 각 페이지의 쿼리 함수에 전달되는 매개변수
getNetPageParams 함수의미
스크롤을 할 때마다 현재 페이지가 allPost(총 페이지 수)와 같지 않다면 page + 1을 해주어 새로운 페이지를 계속해서 불러오고, 현재 페이지와 allPosts의 페이지 수가 같다면 undefined를 하여 데이터 호출을 중단한다.
위와 같은 코드를 기반으로, 스크롤 위치가 페이지 하단에 맞닿았을 때마다 page가 +1이 되면서 다음 페이지의 데이터들이 스크롤 할 때마다 새롭게 불러와져 보여지도록 하면 된다.
스크롤 위치가 페이지 하단에 맞닿았을 때마다 page + 1을 해주면서 다음 페이지의 데이터들을 스크롤 할 때마다 새롭게 불러주기 위해서는 먼저 스크롤의 위치를 확인해야 할 필요가 있다.
이때
Scroll 이벤트,Intersection Observer등을 사용할 수 있지만, 여기서는 스크롤 위치가 페이지 하단에 맞닿았을 때마다 확인해주는 라이브러리인react-infinite-scroller를 통해 확인해보겠다.
import { useInfiniteQuery } from 'react-query';
import InfiniteScroll from 'react-infinite-scroller';
const test = () => {
// 피드 API 불러오는 함수
const getFeedPost = async ({ page, content, view, tag }: IDetailPost) => {
const {data} = await axios.get(`/post`, {
params: {
page,
view,
content,
tag,
},
});
return data;
};
// 데이터 패칭
const { data, fetchNextPage, hasNextPage, isLoading, isError } = useInfiniteQuery(
['page', search],
({ pageParam = 0 }) => getFeedPost({ page: pageParam, content: search, view: 5 }),
{
getNextPageParam: (lastPage, allPosts) => {
return lastPage.page !== allPosts[0].totalPage ? lastPage.page + 1 : undefined;
},
},
);
if (isLoading) return <h3>로딩중</h3>;
if (isError) return <h3>잘못된 데이터 입니다.</h3>;
return (
<main>
{/* 피드 게시물 */}
<InfiniteScroll hasMore={hasNextPage} loadMore={() => fetchNextPage()}>
<FeedItem data={data} />}
</InfiniteScroll>
</main>
);
};
export default test;
위의 속성값의 의미
pageParam : 기본 초기값으로 0을 지정하였고, fetchNextPage가 이 pageParam의 다음페이지가 있는지 결정
hasNextPage : 받아올 데이터가 남았는지 결정하는 boolean값
fetchNextPage : 추가로 받아올 데이터가 필요한 경우,<InfiniteScroll>에 알려주는 역할을 한다.
즉,<InfiniteScroll>에서 loadMore속성값에 넣어주면, 더 추가로 불러올 데이터가 있을 때 마다 알아서 로드해준다.
getNextPageParam : 다음 페이지를 가져오기 위한 함수이다.
UI에 보여주는 작업을 먼저하고, 서버로 요청하는 작업을 나중에한다. (일종의 더미 데이터를 집어 넣어주는 방식)
const onSubmitForm = useCallback(
(e) => {
e.preventDefault();
console.log(chat);
if (chat?.trim() && chatData) {
const savedChat = chat;
mutateChat((prevChatData) => {
prevChatData?.[0].unshift({
id: (chatData[0][0]?.id || 0) + 1,
content: savedChat,
SenderId: myData.id,
Sender: myData,
ReceiverId: userData.id,
Receiver: userData,
createdAt: new Date(),
});
return prevChatData;
}, false).then(() => {
setChat('');
scrollbarRef.current?.scrollToBottom();
});
axios
.post(`/api/workspaces/${workspace}/dms/${id}/chats`, {
content: chat,
})
.then(() => {
revalidate();
})
.catch(console.error);
}
},
[chat, chatData, myData, userData, workspace, id],
);
facebook 좋아요 기능, 유튜브 실시간 좋아요 개수 등에 사용되는 것 같은데 추가적으로 알아봐야겠다...
react-custom-scrollbars 참고
luxon 라이브러리 참고
react-mentions 공식문서
정규 표현식 테스트
react-query infinite scroll
infinite scroll using react query
리액트 쿼리 무한스크롤 정리 velog
react-query 무한 스크롤 공식문서