Experience/네이버 부스트캠프

[네이버 부스트캠프] 퍼널 패턴으로 실시간 퀴즈 애플리케이션 상태 관리하기

krokerdile 2024. 11. 15. 20:06
728x90

들어가며

실시간 퀴즈 애플리케이션을 개발하면서 가장 큰 도전 과제는 복잡한 퀴즈 진행 과정의 상태 관리였습니다. '대기실', '퀴즈 진행', '결과'와 같은 주요 단계들이 있고, 각 단계마다 세부적인 상태와 데이터가 존재하는 구조를 어떻게 효율적으로 관리할 수 있을지 많은 고민이 있었습니다.

이런 복잡한 상태 관리 문제를 해결하기 위해 토스의 SLASH 23 컨퍼런스에서 소개된 퍼널 패턴을 적용해보기로 결정했습니다.

퍼널 패턴이란?

퍼널(Funnel)은 '깔때기'라는 뜻으로, 사용자가 특정 목표를 달성하기까지의 단계를 위에서 아래로 시각화했을 때 깔때기 모양이 되는 것에서 유래했습니다. 주로 마케팅에서 사용되던 이 개념을 개발에 적용한 것이 퍼널 패턴입니다.

퍼널 패턴의 특징

  1. 단방향성: 사용자는 정해진 순서대로 단계를 진행
  2. 단계별 데이터 관리: 각 단계마다 필요한 데이터를 독립적으로 관리
  3. 상태 추적: 현재 진행 단계와 이전 단계들의 이력 관리
  4. 검증: 각 단계 진입 시 필요한 조건 검증

퀴즈존 상태 관리 구현

1. 퍼널 구조 설계

현재 구현된 QuizZone은 다음과 같은 퍼널 구조를 가집니다:

graph TD
    A[LOBBY] --> B[QUIZ_PROGRESS]
    B --> C[RESULT]

    subgraph QUIZ_PROGRESS State
        D[WAITING]
        E[IN_PROGRESS]
        F[COMPLETED]

        D --> E
        E --> F
        F --> |Next Quiz|D
    end

    B === D
    F --> |Last Quiz|C

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#f0f0f0,stroke:#333,stroke-width:2px
    style C fill:#9ff,stroke:#333,stroke-width:2px
    style D fill:#fff,stroke:#666
    style E fill:#fff,stroke:#666
    style F fill:#fff,stroke:#666

2. 상태 및 데이터 구조 정의

// 메인 퍼널 상태
type QuizZone = 'LOBBY' | 'QUIZ_PROGRESS' | 'RESULT';

// 서브 퍼널 상태 (QUIZ_PROGRESS 내부)
type SolveStage = 'WAITING' | 'IN_PROGRESS' | 'COMPLETED';

// 각 단계별 데이터 구조
interface QuizZoneData {
    Lobby: {
        participants: number;
        totalQuizCount: number;
        isHost: boolean;
        quizTitle: string;
        description?: string;
    };
    quizProgress: {
        currentQuiz: {
            question: string;
            timeLimit: number;
            type?: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
        };
        progress: QuizProgress;
    };
    result: {
        score: number;
        quizzes: any;
        submits: any;
    };
}

3. 단계별 상태 전환 관리

// "대기실", "퀴즈 진행", "퀴즈 결과" 상태 관리
function changeMainStage(stage: QuizZone, data?: any) {
    setIsTransitioning(true);
    try {
        setQuizZone(stage);
        if (stage === 'QUIZ_PROGRESS') {
            setSolveStage('WAITING');
            prepareTimer.start();
            solutionTimer.stop();
        }
        // ... 데이터 업데이트 로직
    } finally {
        setIsTransitioning(false);
    }
}

// "퀴즈 대기" "퀴즈 풀이 중" "퀴즈 풀이 후 대기"
function handleQuizCycle(stage: SolveStage, data?: any) {
    setIsTransitioning(true);
    try {
        setSolveStage(stage);
        if (stage === 'WAITING') {
            solutionTimer.stop();
        } else if (stage === 'IN_PROGRESS') {
            prepareTimer.stop();
            solutionTimer.start();
        }
        // ... 단계별 처리 로직
    } finally {
        setIsTransitioning(false);
    }
}

퍼널 패턴 vs 일반적인 상태 관리

다음 다이어그램은 퍼널 패턴과 일반적인 상태 관리의 차이를 보여줍니다:

flowchart TD
    subgraph Complex["기존 상태관리 방식"]
        direction TB
        Store[메인 스토어/페이지]

        Store --> Lobby[로비 스토어/페이지]
        Store --> Quiz[퀴즈 스토어/페이지]
        Store --> Result[결과 스토어/페이지]

        Lobby <-.-> |상태 동기화| Quiz
        Quiz <-.-> |상태 동기화| Result
        Lobby <-.-> |상태 공유| Result

        Lobby --> LobbyP[참가자 상태]
        Lobby --> LobbyH[호스트 상태]

        Quiz --> QuizQ[문제 상태]
        Quiz --> QuizT[타이머 상태]
        Quiz --> QuizS[제출 상태]

        Result --> ResultS[결과 상태]
        Result --> ResultSt[통계 상태]
    end

    Complex --> Simple

    subgraph Simple["퍼널 패턴 적용"]
        direction TB
        Manager[QuizZoneManager]

        Manager --> States[퍼널 상태]
        Manager --> Data[스테이지 데이터]

        States --> MainStates[메인 스테이지]
        States --> SubStates[서브 스테이지]

        MainStates --> MS1[LOBBY]
        MainStates --> MS2[QUIZ_PROGRESS]
        MainStates --> MS3[RESULT]

        SubStates --> SS1[WAITING]
        SubStates --> SS2[IN_PROGRESS]
        SubStates --> SS3[COMPLETED]

        Data --> D1[Lobby 데이터]
        Data --> D2[Quiz 데이터]
        Data --> D3[Result 데이터]
    end

퍼널 패턴의 이점

  1. 상태 전이의 예측 가능성: 퍼널 패턴은 미리 정의된 흐름을 따라 예측 가능하게 진행되며 데이터가 단방향으로 흐릅니다.
  2. 중앙화된 로직: 한 곳에서 모든 변경사항을 관리할 수 있어 유지보수가 용이합니다.
  3. 디버깅 효율성: 단일 상태 트리에서 모든 문제를 파악할 수 있어 효율적입니다.

실시간 통신과의 통합

퍼널 패턴은 WebSocket을 통한 실시간 통신과도 잘 통합되었습니다. 다음은 실제 구현된 이벤트 흐름입니다:

sequenceDiagram
    participant Client
    participant Gateway
    participant QuizStore
    participant EventEmitter

    Note over Client,EventEmitter: 1. 퀴즈존 생성 및 초기화
    Client->>Gateway: POST /quiz-zone (퀴즈존 생성)
    Gateway->>QuizStore: 퀴즈존 세션 저장
    Gateway->>Client: 201 Success (퀴즈존 ID)

    Note over Client,EventEmitter: 2. 초기 데이터 로딩
    Client->>Gateway: GET /quiz-zone/{id}
    Gateway->>QuizStore: 퀴즈존 데이터 조회
    Gateway->>Client: 200 Success (퀴즈존 초기 데이터)

    Note over Client,EventEmitter: 3. 웹소켓 연결 및 게임 시작
    Client->>Gateway: WebSocket 연결 요청 (/ws/play)
    Gateway-->>Client: 연결 수립
    Client->>Gateway: event:start 발생
    Gateway->>QuizStore: 게임 상태 업데이트
    Gateway-->>Client: data: OK

향후 개선 방향

  1. 타입 안정성 강화

    • 각 단계별 데이터 타입 더 명확히 정의
    • 단계 전환 시 필요한 데이터 검증 강화
  2. 에러 처리 개선

    • 각 단계별 에러 상황 정의
    • 복구 전략 수립
    • 사용자 피드백 메커니즘 강화
  3. 단계별 로딩 상태 관리

    • 전환 과정의 시각적 피드백 개선
    • 비동기 작업의 안정성 확보

마치며

퍼널 패턴을 도입한 결과, 복잡했던 퀴즈존의 상태 관리가 훨씬 더 예측 가능하고 관리하기 쉬운 형태로 개선되었습니다. 특히 WebSocket을 통한 실시간 이벤트 처리와 타이머 동기화 같은 복잡한 요구사항들을 더욱 효과적으로 처리할 수 있게 되었습니다.

이러한 경험을 통해 상태 관리 패턴의 중요성과 함께, 적절한 추상화가 가져다주는 이점을 다시 한 번 확인할 수 있었습니다.

728x90