Skip to content

Commit

Permalink
[FE] 방 목록 페이지네이션 기능 구현 (#189)
Browse files Browse the repository at this point in the history
* refactor: useEffect 하나로 동작하도록 변경

* feat: Pagination을 위한 데이터 구조, 상태, API 변경

* feat: Pagination 버튼 컴포넌트 데이터 사용

* feat: 페이지네이션 기능 추가

SSE, RoomList, RoomListPage 로직 변경 및 상수 추가

* docs: README.md 업데이트
  • Loading branch information
studioOwol authored Nov 28, 2024
1 parent 83a62b2 commit ea36048
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 118 deletions.
25 changes: 25 additions & 0 deletions fe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,28 @@ npm install react@latest react-dom@latest
- Player 컴포넌트 내부에 isMuted 초기 상태를 정해주고, muteStatus 데이터 상태가 변경되었을 때 isMuted를 변경해 주는 방식으로 바꿨다.
- 이렇게 해서 Player 컴포넌트와 GameScreen 컴포넌트를 독립적으로 리렌더링 해줄 수 있게 됐다.
- 문제: muteStatus의 initial state를 null로 설정하니까 처음에 가져올 때 에러 발생해서 빈 객체로 초기화

### 검색과 실시간성은 분리하자

- 한 사용자가 검색 중일 때 새로운 방이 생성되거나 삭제된 경우 이를 실시간으로 반영해서 필터링해야 하나 고민을 했다.
- 사용자 입장에서 필터링된 방이 갑자기 새로 생기거나 없어지는 게 이상할 것 같다는 생각이 들었다.
- 그래서 검색어를 지우면 1페이지의 방 목록을 보여주도록 함
- 대신 검색으로 필터링된 방이 삭제된 경우에는 `삭제된 방입니다.`와 같은 알림을 띄우는 게 어떨까
- 검색 중 필터링된 방이 삭제되면 에러 발생 -> 이 경우는 나중에 해결하기로
- 아.. 근데 1페이지가 꽉 차지 않은 경우에는 방 생성, 방 삭제 시 방 목록이 업데이트돼서 SSE로 리렌더링 될 텐데.. 하 모르겠다

### 실시간 통신 페이지네이션 이렇게 어려울 일이야?

- 페이지네이션 하려면 서버에서 전체 방 개수, 혹은 페이지 개수 등의 정보를 내려줘야 한다.
- Taskify 할 때 엄청 고민했던 부분이었다. DB에 count 컬럼을 두고 관리했던..
- 그래야 페이지를 이동시킬 수 있고, 페이지를 이동할 수 있어야 해당 페이지 번호로 요청을 보내서, 해당 페이지의 방 목록을 받아와 렌더링 해줄 수 있기 때문이다.
- 그래서 데이터 구조도 바뀌었고, SSE 부분도 다 바꿔줘야 했다. 머리가 터질 것 같다.
- SSE는 해결된 걸 확인하긴 했는데, REST API 쪽이 문제인 것 같다. 초기 데이터가 null로 오는 건가..? 나는 뭔지 모르겠다.
- 서버 쪽도 갑자기 많은 걸 바꿔서 그런지 자꾸 서버 에러가 나서, 나는 아무래도 기다려야 할 것 같다.🫠
- 원인을 알아냈다.
- 지금 페이지네이션 버튼 생성은 REST API에 의존하고 있다. 위에서 말한 것처럼 전체 방 개수나, 총 페이지 개수를 받아와야 얘로 버튼을 만들 수 있고, 띄워줄 수 있다.
- 초기 페이지 번호를 0으로 Store에서 가지고 있고(프론트에서 먼저 페이지네이션을 구현할 때 인덱스로 사용한 부분이 있어서 0으로 했음), 초기 데이터 가져올 때 이 상태 값을 가지고 REST API 요청하고 받아 온 데이터를 렌더링 해준다.
- 지금 REST API 응답은 data: {rooms: [], pagination: {}} 구조인데, SSE 응답은 rooms 배열뿐이다.
- 메인 페이지에 입장해서 방 생성, 삭제를 하지 않은 사용자에게 실시간으로 페이지네이션 버튼이 뜨도록 하려면 SSE 응답에도 pagination 정보, 적어도 현재 페이지 정보를 같이 받아와서 상태를 변경시켜 줄 수 있어야 한다. 그렇지 않으면 초기에 설정된 0(1페이지)으로만 계속 SSE 요청을 보내게 된다.
- REST API를 또 요청하면 되지 않나? 싶어서 refetch도 시켜줬지만 무의미한 일이었다. 버튼이 생겨야 현재 페이지 상태를 변경시켜 줄 수 있기 때문이다.
- 현재 상황에서 새로고침을 하지 않는 한 실시간으로 페이지네이션 버튼을 띄울 수 없는 것 같다는 게 결론이다..! -> 서버에 데이터 구조 맞춰서 내려달라고 요청함
16 changes: 5 additions & 11 deletions fe/src/components/common/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,23 @@ import { getRoomsQuery } from '@/stores/queries/getRoomsQuery';
const SearchBar = () => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 200); // 200ms 디바운스
const { setRooms } = useRoomStore();
const { setRooms, userPage } = useRoomStore();
const { data: searchResults } = searchRoomsQuery(debouncedSearch);
const { data: allRooms, refetch: refetchAllRooms } = getRoomsQuery();
const { data: roomsData, refetch: refetchAllRooms } = getRoomsQuery(userPage);

// 검색 결과 또는 전체 방 목록으로 업데이트
useEffect(() => {
if (!debouncedSearch.trim()) {
if (!debouncedSearch.trim() && roomsData?.rooms) {
refetchAllRooms();
setRooms(roomsData.rooms);
return;
}

// 검색 결과가 있으면 방 목록 업데이트
if (searchResults) {
setRooms(searchResults);
}
}, [debouncedSearch, searchResults, setRooms, refetchAllRooms]);

// allRooms가 업데이트되면 방 목록 갱신
useEffect(() => {
if (!debouncedSearch.trim() && allRooms) {
setRooms(allRooms);
}
}, [allRooms, debouncedSearch, setRooms]);
}, [debouncedSearch, searchResults, roomsData, setRooms, refetchAllRooms]);

return (
<div className="relative w-full mt-6">
Expand Down
1 change: 1 addition & 0 deletions fe/src/constants/rules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const RULES = Object.freeze({
maxPage: 9,
maxPlayer: 4,
pageLimit: 9,
});
31 changes: 0 additions & 31 deletions fe/src/hooks/usePagination.ts

This file was deleted.

54 changes: 30 additions & 24 deletions fe/src/hooks/useRoomsSSE.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,53 @@
import useRoomStore from '@/stores/zustand/useRoomStore';
import { useEffect } from 'react';
import { Room } from '@/types/roomTypes';
import { ENV } from '@/config/env';
import { getRoomsQuery } from '@/stores/queries/getRoomsQuery';

export const useRoomsSSE = () => {
const { data: initialRooms } = getRoomsQuery();
const { setRooms } = useRoomStore();
let eventSource: EventSource | null = null;

useEffect(() => {
// 초기 데이터 설정
if (initialRooms) {
setRooms(initialRooms);
}
export const useRoomsSSE = () => {
const { setRooms, setPagination, setUserPage } = useRoomStore();
const userPage = useRoomStore((state) => state.userPage);
const { data } = getRoomsQuery(userPage);

// SSE 연결
const eventSource = new EventSource(ENV.SSE_URL);
const connectSSE = (userPage: number) => {
eventSource = new EventSource(`${ENV.SSE_URL}?page=${userPage}`);

// rooms 데이터 수신 처리
eventSource.onmessage = (event) => {
try {
const rooms = JSON.parse(event.data) as Room[];
setRooms(rooms);
const sseData = JSON.parse(event.data);
setRooms(sseData.rooms);
setPagination(sseData.pagination);

if (!sseData.rooms.length && userPage > 0) {
setUserPage(sseData.pagination.currentPage - 1);
return;
}

setUserPage(sseData.pagination.currentPage);
} catch (error) {
console.error('Failed to parse rooms data:', error);
}
};

// 연결 시작
eventSource.onopen = () => {
console.log('SSE Connection opened');
};

// 에러 처리
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
eventSource.close();
};
};

useEffect(() => {
if (data) {
setRooms(data.rooms);
setPagination(data.pagination);
connectSSE(userPage);
}

// 컴포넌트 언마운트 시 연결 정리 (메모리 누수 예방)
return () => {
console.log('Closing SSE connection');
eventSource.close();
if (eventSource) {
eventSource.close();
eventSource = null;
}
};
}, [initialRooms, setRooms]);
}, [data?.pagination, data?.rooms, userPage]);
};
38 changes: 22 additions & 16 deletions fe/src/pages/RoomListPage/RoomList/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
import { Button } from '@/components/ui/button';
import { getRoomsQuery } from '@/stores/queries/getRoomsQuery';
import useRoomStore from '@/stores/zustand/useRoomStore';
import { useEffect } from 'react';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6';

interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
const Pagination = () => {
const { pagination, setUserPage } = useRoomStore();
const userPage = useRoomStore((state) => state.userPage);
const { totalPages } = pagination;
const { refetch } = getRoomsQuery(userPage);

useEffect(() => {
refetch();
}, [userPage, refetch]);

const handlePageChange = async (newPage: number) => {
setUserPage(newPage);
};

const Pagination = ({
currentPage,
totalPages,
onPageChange,
}: PaginationProps) => {
return (
<div className="flex items-center justify-center gap-4 mt-6">
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 0}
disabled={!userPage}
onClick={() => handlePageChange(userPage - 1)}
>
<FaChevronLeft className="w-4 h-4" />
</Button>
<div className="flex items-center gap-2">
{Array.from({ length: totalPages }, (_, i) => (
<Button
key={i}
variant={currentPage === i ? 'default' : 'outline'}
variant={userPage === i ? 'default' : 'outline'}
size="sm"
onClick={() => onPageChange(i)}
onClick={() => handlePageChange(i)}
className="w-8 h-8"
>
{i + 1}
Expand All @@ -38,8 +44,8 @@ const Pagination = ({
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages - 1}
onClick={() => handlePageChange(userPage + 1)}
disabled={userPage === totalPages - 1}
>
<FaChevronRight className="w-4 h-4" />
</Button>
Expand Down
47 changes: 23 additions & 24 deletions fe/src/pages/RoomListPage/RoomList/RoomList.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import GameRoom from './GameRoom';
import Pagination from './Pagination';
import { usePagination } from '@/hooks/usePagination';
import { RULES } from '@/constants/rules';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import JoinDialog from '../RoomDialog/JoinDialog';
import useRoomStore from '@/stores/zustand/useRoomStore';

const RoomList = () => {
const {
currentPage,
setCurrentPage,
totalPages,
currentRooms,
isEmpty,
showPagination,
} = usePagination();

const rooms = useRoomStore((state) => state.rooms);
const pagination = useRoomStore((state) => state.pagination);
const [isJoinDialogOpen, setIsJoinDialogOpen] = useState(false);
const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null);
const [showPagination, setShowPagination] = useState(false);
const isEmpty = rooms.length === 0;

useEffect(() => {
if (pagination?.totalPages > 1) {
setShowPagination(true);
}

if (pagination?.totalPages === 1) {
setShowPagination(false);
}
}, [pagination]);

const onJoinRoom = (roomId: string) => {
setSelectedRoomId(roomId);
Expand All @@ -26,23 +31,17 @@ const RoomList = () => {
return (
<div className="space-y-6 max-h-screen mt-2">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{currentRooms.map((room) => (
{rooms.map((room) => (
<GameRoom key={room.roomId} room={room} onJoinRoom={onJoinRoom} />
))}
{currentRooms.length > 0 &&
currentRooms.length < RULES.maxPage &&
Array.from({ length: RULES.maxPage - currentRooms.length }).map(
(_, i) => <div key={`empty-${i}`} className="w-full h-0"></div>
)}
{rooms.length > 0 &&
rooms.length < RULES.maxPage &&
Array.from({ length: RULES.maxPage - rooms.length }).map((_, i) => (
<div key={`empty-${i}`} className="w-full h-0"></div>
))}
</div>

{showPagination && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
)}
{showPagination && <Pagination />}

{isEmpty && (
<div className="font-galmuri text-center py-8 text-muted-foreground">
Expand Down
22 changes: 17 additions & 5 deletions fe/src/pages/RoomListPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import SearchBar from '@/components/common/SearchBar';
import RoomHeader from './RoomHeader/RoomHeader';
import RoomList from './RoomList/RoomList';
import { useRoomsSSE } from '@/hooks/useRoomsSSE';
import { useEffect, useState } from 'react';
import CustomAlertDialog from '@/components/common/CustomAlertDialog';
import useRoomStore from '@/stores/zustand/useRoomStore';
import { useRoomsSSE } from '@/hooks/useRoomsSSE';

const RoomListPage = () => {
const [showAlert, setShowAlert] = useState(false);
const [kickedRoomName, setKickedRoomName] = useState('');
const { rooms } = useRoomStore();
const isEmpty = rooms.length === 0;

useRoomsSSE();

Expand All @@ -22,10 +25,19 @@ const RoomListPage = () => {
}, []);

return (
<div>
<RoomHeader />
<SearchBar />
<RoomList />
<div className="min-h-screen flex flex-col pb-16 relative">
<div className="flex-1">
<RoomHeader />
<SearchBar />
{isEmpty ? (
<div className="h-[calc(100vh-220px)] flex items-center justify-center font-galmuri text-muted-foreground text-xl">
생성된 방이 없습니다.
</div>
) : (
<RoomList />
)}
</div>

<CustomAlertDialog
open={showAlert}
onOpenChange={setShowAlert}
Expand Down
11 changes: 7 additions & 4 deletions fe/src/stores/queries/getRoomsQuery.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { Room } from '@/types/roomTypes';
import { PaginatedResponse, Room } from '@/types/roomTypes';
import { ENV } from '@/config/env';

const gameAPI = axios.create({
Expand All @@ -9,12 +9,15 @@ const gameAPI = axios.create({
withCredentials: false,
});

export const getRoomsQuery = () => {
export const getRoomsQuery = (currentPage: number) => {
return useQuery({
queryKey: ['rooms'],
queryFn: async () => {
const response = await gameAPI.get<Room[]>('/api/rooms');
return response.data;
const { data } = await gameAPI.get<PaginatedResponse<Room>>(
`/api/rooms?page=${currentPage}`
);

return data;
},
});
};
Loading

0 comments on commit ea36048

Please sign in to comment.