Skip to content

실시간 데이터 처리 (부제: Zustand를 사용한 이유)

Choi Jeongmin edited this page Dec 1, 2024 · 1 revision

📄 실시간 데이터 처리

이 문서에서는 소켓 이벤트를 통해 받은 데이터와 HTTP API의 응답으로 온 데이터를 모두 전역 상태에서 어떻게 관리했는지 기술합니다.

실시간 데이터를 처리하기 위해 TanStack Query와 같은 라이브러리를 사용하기보다는, Zustand를 활용하여 소켓 이벤트 기반의 실시간 데이터 처리를 효율적으로 구현했습니다.

  • 물론 소켓 이벤트가 발생할 때 특정 데이터를 다시 가져올 수 있지만, 소켓 이벤트가 빈번하고 데이터가 많아질 수 있다고 판단했습니다.
  • 전역 상태로 관리하여 소켓 이벤트 자체에 데이터가 담겨 한 번에 처리가 가능하다면, 전역 상태로 두는 것이 더 효율적이라고 판단했습니다.

전역 상태 관리를 통해 각 컴포넌트에서 데이터를 자동으로 렌더링하도록 했습니다. 일반적인 HTTP API로 응답을 받았을 때도, 소켓 이벤트 기반으로 데이터를 받았을 때도 하나의 액션을 통해 목적을 이루고자 했습니다.

🧩 배경 및 필요성

‘Ask-It’ 은 실시간 처리가 중심인 프로젝트입니다. Q&A와 채팅 등 여러 부분에서 실시간 이벤트 처리 요구가 많습니다.

HTTP API의 응답과 소켓 이벤트로 받은 데이터를 모두 하나의 통일된 방식으로 구현한다면, 개발의 편의성을 높일 수 있을 것이라고 판단했습니다.

다양한 소켓 이벤트와 그에 따른 데이터 업데이트를 효과적으로 관리하기 위한 구조가 필요했습니다.

  1. HTTP API의 응답을 전역 데이터 상태로 관리
  2. 소켓에서 수신한 데이터를 효율적으로 전역 상태로 관리

위 요구 사항에 따라, HTTP API 응답과 소켓에서 수신하는 데이터를 거의 동일시하도록 인터페이스를 작성하고, 전역 상태로 데이터를 다루기 위한 설계를 진행했습니다.

🗺️ 문제 해결 과정

Zustand를 이용해 슬라이스 패턴을 사용하여 각 상태를 기능별로 나눠 관리했습니다. 슬라이스는 특정 도메인에 대한 상태 및 상태 변경 로직을 캡슐화하여 코드의 재사용성과 유지보수성을 높입니다.

스토어 설계 (슬라이스 분할)

classDiagram
    class SessionStore {
    }

    class ChattingSlice {
    }

    class QnASlice {
    }

    class SessionSlice {
    }

    SessionStore --> ChattingSlice
    SessionStore --> QnASlice
    SessionStore --> SessionSlice
Loading
  • 각 슬라이스는 독립적인 상태와 상태 관리 로직을 포함하고 있습니다.
    • ChattingSlice: 채팅의 상태와 상태 관리 로직
    • QnASlice: 질문, 답변의 상태와 상태 관리 로직
    • SessionSlice: 세션마다 가지는 상태와 상태 관리 로직
  • 각 슬라이스를 하나의 스토어로 만들어 외부에서는 커스텀 훅을 통해 해당 스토어에 접근하여 각 슬라이스의 상태와 상태 관리 로직을 사용할 수 있게 했습니다.
    • 여기서는 모든 것이 하나의 Q&A 세션과 관련되어 있어 SessionStore라고 이름을 정했습니다.
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에 추가합니다.

Q&A 슬라이스

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 통신과 소켓 통신에서 받는 데이터를 모두 일관되게 처리할 수 있었습니다.
    • 제공하는 액션을 사용하면 자동으로 렌더링과 데이터 관리를 보장할 수 있었습니다.
  • 하나의 큰 스토어를 기능별로 상태를 분리하여 여러 슬라이스로 만들었습니다.
    • 상태와 로직을 독립적으로 작성할 수 있어 관리하기에 용이했습니다.
  • 상태 관리의 복잡성을 줄이고, 추가적인 기능이 생기더라도 슬라이스만 추가하면 되기에 확장성에서 유리하다고 말할 수 있습니다.
Clone this wiki locally