Skip to content

Chillin-s-Playground/ticket-rush-frontend

Repository files navigation

Ticket Rush

1. 프로젝트 설명

Next.js를 활용해 실시간 좌석 동기화가 가능한 대규모 티켓 예매 서비스를 구현했습니다. (본 레포지터리는 프론트엔드 영역만 다룹니다.

  1. 실시간 좌석 상태 반영
  - WebSocket을 통해 여러 사용자가 동시에 접속해도 좌석 상태(HOLD, SOLD, AVAILABLE)가 즉시 반영됩니다.
  - 상태 관리는 Zustand 전역 스토어를 활용하여 UI와 데이터를 일관되게 유지했습니다.
  
  2. UX & 렌더링 최적화
  - `useMemo`와 Zustand selector를 활용하여 불필요한 리렌더링을 줄이고, 상태 변경된 좌석만 다시 렌더링하도록 최적화했습니다.
  - `SeatGridSkeleton`으로 초기 진입 시 Skeleton UI를 제공해 로딩 경험을 개선했습니다.
  
  3. Presence 관리
  - 사용자가 페이지를 이탈하거나 창을 닫을 때 `navigator.sendBeacon`을 사용해 서버로 알림을 전송합니다.
  - 서버(FastAPI)는 Redis를 통해 사용자의 활성 상태 및 좌석 HOLD 정보를 정리하여 안정성을 확보했습니다.

2. 핵심 구현 포인트 ( Technical Highlights )

1. 실시간 좌석 상태 반영

  • WebSocket 기반 업데이트

    • 좌석 상태(HOLD, SOLD, AVAILABLE)를 WebSocket으로 받아와 즉시 UI 반영
    • 서버에서 다건 상태 변경을 push하면, 클라이언트는 patchStatus로 상태를 일괄 갱신
  • Zustand 전역 스토어 활용

    • 전역 상태를 관리해 페이지 전환 없이도 좌석 상태가 일관되게 유지
    • patchStatus는 seat_id 기준으로 매핑 후 필요한 좌석만 업데이트

    핵심 코드 스니펫

    useEffect(() => {
      const ws = new WebSocket(`${DEV_DOMAIN}/ws/events/777/seats`);
    
      // ...생략
    
      ws.onmessage = (evt) => {
        const msg = JSON.parse(evt.data);
        if (msg.type !== 'seat_update') return;
    
        const updates = (Array.isArray(msg.payload) ? msg.payload : [msg.payload])
          .map((p: any) =>
            p.seat_id
              ? { seat_id: p.seat_id, status: p.seat_status }
              : { prev_seat_id: p.prev_seat_id, status: 'AVAILABLE' },
          );
    
        patchStatus(updates); // ✅ 상태 일괄 갱신
      };
    
      return () => ws.close();
    }, [patchStatus]);

2. UX & 렌더링 최적화

  • 부분 렌더링만 수행

    • Zustand selector를 이용해 필요한 상태만 구독 → 좌석 상태가 바뀐 해당 좌석만 다시 렌더링
    • Seat 컴포넌트를 React.memo로 감싸 불필요한 재조합 방지
  • 계산 비용 메모이제이션

    • 수천 개 좌석 배열을 400석 단위 섹션으로 나누는 연산을 useMemo로 감싸, 실제 seats 상태가 변경될 때만 재계산 → 렌더링 성능 안정화
  • 로딩 경험 최적화 (Skeleton UI)

    • 초기 진입 시 SeatGridSkeleton을 먼저 보여주어 지각된 대기 시간을 줄이고, 데이터가 도착하면 실제 좌석 그리드로 자연스럽게 전환

    핵심 코드 스니펫

    // SeatBase.tsx — 부분 렌더링
    function SeatBase({ seatId, seatLabel, status }: Props) {
      const { myPaidSeat, setMySeat } = useSeatStore();
      if (myPaidSeat) return null;
    
      return (
        <button
          disabled={status === 'SOLD'}
          onClick={/* ...좌석 HOLD API 호출... */}
        />
      );
    }
    export default React.memo(SeatBase);
    
    // SeatGridClient.tsx — 섹션 분할 + 스켈레톤
    const sections = [];
    for (let i = 0; i < seats.length; i += 400)
      sections.push(seats.slice(i, i + 400));
    
    return sections.length === 0 ? (
      <SeatGridSkeleton />
    ) : (
      sections.map((sectionSeats) => (
        <div>{sectionSeats.map((s) => <Seat key={s.seat_id} {...s} />)}</div>
      ))
    );

3. 실행 방법

  1. 의존성 설치
npm install
  1. 개발서버 실행 ( localhost:3000 )
npm run dev 
  1. 프로덕션 빌드 및 실행
npm run build
npm run start

About

대규모 실시간 티켓예매 시스템 ( 프론트엔드 )

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors