나는 DM페이지를 다음과 같이 구상하였다.
return (
<>
{isShowPostDetail && <PostDetail onClose={handleNavigatePostDetail} />}
<S.DirectMessageLayout>
<MessageGroupList />
<MessageList />
</S.DirectMessageLayout>
</>
)
크게 왼쪽 컴포넌트는 메시지 그룹리스트, 오른쪽 컴포넌트는 메시지 리스트 이렇게 둘로 나눴고, 두 컴포넌트간 의존성이 있다면 둘을 포함하는 곳에서 state를 뿌려줘야 하지만, api의 한계상 소켓통신이 불가능하여 폴링 방식을 구현해야했고 이 경우 왼쪽,오른쪽 컴포넌트가 각각 독립적으로 의존하지 않는다고 판단하여 따로 상태를 관리하였다. 또한 상태는 서버 상태 관리 라이브러리인 리액트 쿼리를 사용하였고 사용하지 않았을때보다 비교적 편리하게 구현할 수 있었다.
페이지를 구현하는 로직 자체에는 큰 어려움은 없었지만 헷갈렸던 부분은 sender와 receiver의 구분이었다. 내가 받는사람의 입장에서도 로직을 짜야하고 보내는 사람의 입장에서도 로직을 짜야하고 이것이 모두 하나로 이뤄져야하는데 이것을 인지하고 개발하는 것이 조금 힘들었다.
그리고, 어느 순간부터 에러처리를 하지않고, 리액트 쿼리에 대한 매우 얕은 지식으로 인해 잦은 오류가 발생하였었는데 이것을 찾는데 쉽지 않았었다.
다음은 DM페이지를 구현하면서 만난 문제들이다.
import useAuthUserStore from "@/stores/useAuthUserStore"
import { Conversation } from "@/types"
import { useQuery } from "@tanstack/react-query"
import { useParams } from "react-router-dom"
import getMessageGroupListAPI from "../apis/getMessageGroupListAPI"
import usePostEditModalStore from "@/components/PostEdit/stores/usePostEditModalStore"
import usePostDetailModalStore from "@/components/PostDetail/stores/usePostDetailModalStore"
export const QUERY_KEY_GET_GROUP_MESSAGE_LIST =
"GET_GROUP_MESSAGE_LIST_72154682516375217"
const useMessageGroupList = () => {
const { user } = useAuthUserStore()
const myId = user._id
const { userId: othersUserId } = useParams()
const { isShowEditModal } = usePostEditModalStore()
const { isShowPostDetail } = usePostDetailModalStore()
const isNotShowModal = !isShowEditModal && !isShowPostDetail
const { data } = useQuery({
queryKey: [QUERY_KEY_GET_GROUP_MESSAGE_LIST],
queryFn: getMessageGroupListAPI,
initialData: [],
refetchInterval: isNotShowModal && 1000 * 2,
gcTime: 1000 * 60 * 5,
select: (GroupMessageList: Conversation[]) =>
GroupMessageList.map((MessageList) => {
return {
...MessageList,
seen:
myId === MessageList.sender._id ||
othersUserId === MessageList.sender._id
? true
: MessageList.seen,
}
}),
})
return { data }
}
export default useMessageGroupList
리액트 쿼리로 유저의 그룹 메시지, 메시지들을 받아오는데 초기 렌더링, 즉, 처음 페이지에 진입할때와 새로고침 시 캐시된 데이터를 보여주는 것이 아니라 이상하게 매번 새로 데이터를 받아오는 듯 했다. 캐시가 안되고 있는 것이다.
문제는 initialData
에 있었다.
원래 이 옵션을 사용하지 않으려 했었는데 서버에서 받아오는 data값은 항상 undefined 타입을 포함하고 있어서 initialData를 빈배열로 선언하는 옵션을 추가하면 데이터를 가지고 사용할때 undefined
를 따로 처리해줄 필요가 없었기 때문에 편리하였다.
initialData
은 쿼리가 백엔드로 부터 데이터를 가져오기 전에 지정된 초기데이터를 제공한다. 하지만, 페이지가 렌더링될때마다 이를 캐시에 저장하고 쿼리 상태를 success
로 설정한다. 따라서, 페이지 들어가서 빈 배열을 캐시에 저장하고 성공했다고 판단하기 때문에 refetch시간까지 그냥 빈배열 데이터를 가공해서 보여주려 하기 때문에 페이지에 아무것도 보이지 않는다.
해결책은 그냥 initialData
를 지워주면 된다.
이제 자연스럽게 초기 렌더링 속도도 빠르고 새로고침시에도 빠르게 데이터를 받아온다.
그렇다고 해서 이때 캐싱된 데이터를 사용하는 것이다. 나는 staleTime
을 따로 설정하지 않았기때문에(0초) 항상 새로운 데이터를 서버에서 받아온다. 지금은 데이터가 별로 없기도하고 상호작용이 적기 때문에 전혀 문제 없이 매우 빠르게 보이지만 나중에 사용자가, 데이터가 많아지면 이게 문제가 될 수 도 있을 것 같다..(어차피 refetch가 2초라 새로 받아오지만)
아직 브라우저 내에 캐시가 남아 있기 때문이다. 내가 생각했던 방식은 일단 refetch와는 별도로 DM 페이지나 알림 모달을 띄우면 다시 데이터를 바로 불러오는 줄 알았는데 그러지 않았기 때문에 캐쉬를 따로 삭제해줘야 했다. 그래서 일단 제일 쉬운 방법인 useEffect
를 이용하여, DM페이지에 처음 접속하거나 새로고침시에만 캐쉬를 삭제하고 데이터를 새로 받아오도록 하였다. 이렇게 하니까 문제를 해결되었다.
하지만, useEffec
자체가 되도록 안쓰는 것이 좋기 때문에 다른 방법은 없을까 팀원에게 물어보니 로그아웃때 이것을 하자고 하였다! 이 생각을 왜 못했을까.. 결국 캐쉬문제는 로그인하고 사용할때는 문제가 없지만 로그아웃하고 다시 로그인할때 문제가 생기는 것이다. 따라서 불필요한 useEffect
를 통한 렌더링을 할 필요가 전혀 없었다.
따라서, 기존에 로그아웃 핸들러 함수에 캐쉬를 삭제하는 코드를 집어넣었다.
// useNavMenuClick.js
const useMenuClick = () => {
const { setLogout } = useAuthUserStore()
const handleLogout = async () => {
await API.post("/logout")
.then(() => {
setLogout()
navigate("/")
})
.catch(() => {})
}
const handleMenuClick = (menu: string) => {
switch (menu) {
case "로그아웃":
handleLogout()
break
}
}
export default useMenuClick
import useLogout from "@/hooks/useLogout"
const useMenuClick = () => {
const navigate = useNavigate()
const { isShowEditModal, showEditModal } = usePostEditModalStore()
const { logoutMutate } = useLogout()
const handleMenuClick = (menu: string) => {
switch (menu) {
case "로그아웃":
logoutMutate.mutate()
break
}
}
}
export default useMenuClick
import { API } from "@/apis/Api"
import useAuthUserStore from "@/stores/useAuthUserStore"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { useNavigate } from "react-router-dom"
const QUERY_KEY_LOGOUT =
"HANDLE_LOGOUT_1238213124124124932092038590284091840918"
const useLogout = () => {
const queryClient = useQueryClient()
const { setLogout } = useAuthUserStore()
const navigate = useNavigate()
const logoutMutate = useMutation({
mutationKey: [QUERY_KEY_LOGOUT],
mutationFn: fetchLogout,
onMutate: () => {
setLogout()
queryClient.clear()
navigate("/")
},
})
return { logoutMutate }
}
export default useLogout
const fetchLogout = async () => {
await API.post("/logout")
}
위와 로그아웃 로직에 캐시를 삭제하는 부분을 추가하고 훅으로 따로 분리하였다.
로그아웃을 클릭하게 되면 일어나는 일을 순서대로 살펴보자. onMutate
옵션은 mutation이 일어난 로직들을 다룰 수 있다.
따라서, 다음과 같은 순서대로 동작한다.
서버에 로그아웃 요청전에 미리 optimistic update를 활용해 1~3번을 먼저 수행하는 것이다.
중반부부터 발생한 문제인데 후반부에 깨닫고 고쳐진 문제다 이것 때문에 서버를 조금씩 아프게 했을 것이다.
현재 나는 전역에서 두 상태를 관리하는 것이 아니라 메시지 그룹을 받는 왼쪽에 해당하는 컴포넌트, 메시지를 받는 오른쪽 컴포넌트 각각 서버에 비동기 호출을 하고 있었다.
내 의도는 스크린샷에 나온 경우(경로가 /directmessage)에서는 왼쪽의 메시지 그룹리스트 요청만 해서 렌더링을 하도록 하였다. 근데 오른쪽 컴포넌트 렌더링에 대한 에러처리를 해주지 않아서 메시지를 받는 요청도 엉뚱하게 하고 있어서 요청이 실패한 것이다. 근데 궁금한점은, 둘의 API 요청에는 의존성이 없는데 그룹 메시지는 왜 덩달아 실패하는 건가 의문이 생겼다. 심지어, 그룹 메시지를 먼저 요청하게 되면 성공하지만 메시지 요청을 하고 실패한후에 그룹 메시지 요청을 보내면 실패하게 되는 것을 확인하였다.
DM으로 서버 담당자분께 여쭤보니 API상으로 두 요청간에 의존성은 없다고 하였다..
클라이언트 단에서 호출할때도 그룹메시지는 요청시에 오직 JWT토큰만 이용되므로 더더욱 미궁으로 빠졌다..
실험을 하던중에 서버에 요청을 너무 많이 보냈는지 다운이 돼서 일단 그만두기로 하였다..(다행히 개발이 끝난 기간이었다) 정확한 원인을 찾지는 못하였지만 우선은 에러처리가 매우 중요하다는 교훈을 얻을 수 있었다.