-
Notifications
You must be signed in to change notification settings - Fork 3
실시간 데이터 처리 (부제: Zustand를 사용한 이유)
이 문서에서는 소켓 이벤트를 통해 받은 데이터와 HTTP API의 응답으로 온 데이터를 모두 전역 상태에서 어떻게 관리했는지 기술합니다.
실시간 데이터를 처리하기 위해 TanStack Query
와 같은 라이브러리를 사용하기보다는, Zustand
를 활용하여 소켓 이벤트 기반의 실시간 데이터 처리를 효율적으로 구현했습니다.
- 물론 소켓 이벤트가 발생할 때 특정 데이터를 다시 가져올 수 있지만, 소켓 이벤트가 빈번하고 데이터가 많아질 수 있다고 판단했습니다.
- 전역 상태로 관리하여 소켓 이벤트 자체에 데이터가 담겨 한 번에 처리가 가능하다면, 전역 상태로 두는 것이 더 효율적이라고 판단했습니다.
전역 상태 관리를 통해 각 컴포넌트에서 데이터를 자동으로 렌더링하도록 했습니다. 일반적인 HTTP API로 응답을 받았을 때도, 소켓 이벤트 기반으로 데이터를 받았을 때도 하나의 액션을 통해 목적을 이루고자 했습니다.
‘Ask-It’ 은 실시간 처리가 중심인 프로젝트입니다. Q&A와 채팅 등 여러 부분에서 실시간 이벤트 처리 요구가 많습니다.
HTTP API의 응답과 소켓 이벤트로 받은 데이터를 모두 하나의 통일된 방식으로 구현한다면, 개발의 편의성을 높일 수 있을 것이라고 판단했습니다.
다양한 소켓 이벤트와 그에 따른 데이터 업데이트를 효과적으로 관리하기 위한 구조가 필요했습니다.
- HTTP API의 응답을 전역 데이터 상태로 관리
- 소켓에서 수신한 데이터를 효율적으로 전역 상태로 관리
위 요구 사항에 따라, HTTP API 응답과 소켓에서 수신하는 데이터를 거의 동일시하도록 인터페이스를 작성하고, 전역 상태로 데이터를 다루기 위한 설계를 진행했습니다.
Zustand
를 이용해 슬라이스 패턴을 사용하여 각 상태를 기능별로 나눠 관리했습니다. 슬라이스는 특정 도메인에 대한 상태 및 상태 변경 로직을 캡슐화하여 코드의 재사용성과 유지보수성을 높입니다.
classDiagram
class SessionStore {
}
class ChattingSlice {
}
class QnASlice {
}
class SessionSlice {
}
SessionStore --> ChattingSlice
SessionStore --> QnASlice
SessionStore --> SessionSlice
- 각 슬라이스는 독립적인 상태와 상태 관리 로직을 포함하고 있습니다.
-
ChattingSlice
: 채팅의 상태와 상태 관리 로직 -
QnASlice
: 질문, 답변의 상태와 상태 관리 로직 -
SessionSlice
: 세션마다 가지는 상태와 상태 관리 로직
-
- 각 슬라이스를 하나의 스토어로 만들어 외부에서는 커스텀 훅을 통해 해당 스토어에 접근하여 각 슬라이스의 상태와 상태 관리 로직을 사용할 수 있게 했습니다.
- 여기서는 모든 것이 하나의 Q&A 세션과 관련되어 있어
SessionStore
라고 이름을 정했습니다.
- 여기서는 모든 것이 하나의 Q&A 세션과 관련되어 있어
export type SessionStore = SessionSlice & QnASlice & ChattingSlice;
export const useSessionStore = create<SessionStore>()((...a) => ({
...createQnASlice(...a),
...createChattingSlice(...a),
...createSessionSlice(...a),
}));
export interface ChattingSlice {
chatting: Chat[];
resetChatting: () => void;
addChatting: (chat: Chat) => void;
addChattingToFront: (chat: Chat) => void;
}
- 채팅 리스트와 이를 다루는 몇몇 액션을 포함합니다.
- HTTP API 응답과 소켓으로부터 수신한 데이터를 해당 액션을 통해
chatting
에 추가합니다.
export interface QnASlice {
questions: Question[];
resetQuestions: () => void;
addQuestion: (question: Question) => void;
updateQuestion: (
question: Partial<Omit<Question, 'questionId'>> & { questionId: number },
) => void;
removeQuestion: (questionId: Question['questionId']) => void;
addReply: (questionId: number, reply: Reply) => void;
updateReply: (
questionId: number,
reply: Partial<Omit<Reply, 'replyId'>> & { replyId: number },
) => void;
removeReply: (replyId: Reply['replyId']) => void;
updateReplyIsHost: (userId: number, isHost: boolean) => void;
}
-
QnASlice
에는 질문 목록과 질문 안에 있는 답변들, 그리고 질문/답변의 상태를 조절할 수 있는 액션들이 포함되어 있습니다. - 위 액션들을 이용해서 수신한 데이터를 통해 전역 상태를 업데이트할 수 있습니다.
export interface SessionSlice {
sessionId?: string;
sessionToken?: string;
isHost: boolean;
expired: boolean;
...
setParticipantCount: (participantCount: number) => void;
}
-
SessionSlice
에는 세션에서 알아야 하는 여러 상태와 그 상태를 조절할 수 있는 액션들이 포함되어 있습니다.
이러한 슬라이스들을 하나의 스토어로 만들어 외부에서는 아래처럼 사용합니다.
// ChattingList.tsx
const {
chatting,
sessionId,
sessionToken,
addChattingToFront,
} = useSessionStore();
...
getChattingList(sessionToken, sessionId, chatting[0]?.chattingId)
.then(({ chats }) => {
if (chats.length < 20) hasMoreRef.current = false;
chats.forEach(addChattingToFront);
requestAnimationFrame(() => {
const newHeight = container.scrollHeight;
const heightDiff = newHeight - prevHeightRef.current;
container.scrollTop = heightDiff;
});
})
// socket.service.ts
const store = useSessionStore.getState();
...
this.socket.on('chatMessage', (payload: ChatMessageEventPayload) => {
store.addChatting(payload);
});
위 코드를 보면 알 수 있듯이, HTTP 요청과 소켓 이벤트를 통해 수신한 데이터를 하나의 방식으로 처리하고 있습니다.
일관된 방식으로 처리함으로써 예측 가능해지고, 동시에 렌더링과 관련된 것도 신경 쓰지 않아도 되어 개발에서 속도나 경험 측면에서 편리하게 사용할 수 있었습니다.
- 전역 상태로부터 데이터를 받아서 렌더링하는 방식을 채택하여, HTTP 통신과 소켓 통신에서 받는 데이터를 모두 일관되게 처리할 수 있었습니다.
- 제공하는 액션을 사용하면 자동으로 렌더링과 데이터 관리를 보장할 수 있었습니다.
- 하나의 큰 스토어를 기능별로 상태를 분리하여 여러 슬라이스로 만들었습니다.
- 상태와 로직을 독립적으로 작성할 수 있어 관리하기에 용이했습니다.
- 상태 관리의 복잡성을 줄이고, 추가적인 기능이 생기더라도 슬라이스만 추가하면 되기에 확장성에서 유리하다고 말할 수 있습니다.