Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ refactor(#167): 내 대시보드 페이지 서버 사이드 렌더링 적용 #199

Merged
merged 7 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@tanstack/react-query-devtools": "^5.48.0",
"@types/react-beautiful-dnd": "^13.1.8",
"axios": "^1.7.2",
"cookies-next": "^4.2.1",
"js-sha256": "^0.11.0",
"lodash": "^4.17.21",
"next": "14.2.4",
Expand Down
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 36 additions & 16 deletions src/containers/mydashboard/DashboardList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
import { useEffect, useState } from 'react';

import Pagination from '@/components/Pagination';
import useFetchData from '@/hooks/useFetchData';
import useModal from '@/hooks/useModal';
import { getDashboardsList } from '@/services/getService';
import { DashboardsResponse } from '@/types/Dashboard.interface';

export default function DashboardList() {
interface DashboardListProps {
initialDashboard: DashboardsResponse;
}

export default function DashboardList({ initialDashboard }: DashboardListProps) {
const [currentChunk, setCurrentChunk] = useState<number>(1);
const { openNewDashboardModal } = useModal();
const [dashboardData, setDashboards] = useState<DashboardsResponse>(initialDashboard);
const [isInitial, setIsInitial] = useState<boolean>(!!initialDashboard);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

초기 데이터가 있는지 확인용


// 대시보드 데이터를 가져오는 커스텀 훅 사용
const {
data: dashboardResponse,
error,
Expand All @@ -20,7 +27,19 @@ export default function DashboardList() {
getDashboardsList('pagination', currentChunk, 5),
);

const totalPage = dashboardResponse ? Math.max(1, Math.ceil(dashboardResponse.totalCount / 5)) : 1;
// 총 페이지 수 계산
const totalPage = initialDashboard?.totalCount ? Math.max(1, Math.ceil(initialDashboard.totalCount / 5)) : 1;

// 대시보드 데이터가 변경될 때 상태 업데이트
useEffect(() => {
if (dashboardResponse) {
if (!isInitial) {
setDashboards(dashboardResponse);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음 진입 이후에 사용합니다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멘토님이 useEffect 쓸 필요 없이 useQuery에서 onSuccess일때 바꾸면 된다고 했던 것 같아요!!
useFetchData에서인자로 handleSuccess주고, setDashboards 하는 함수 넘겨준 뒤에,
useFetchData에서 onSuccess에서 handleSuccess 호출해서 set 하면 될 것 같아요!!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니면 제가 추후 SSR 적용하면서 같이 이런 식으로 바꿀게요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 어떻게 사용한다는 말씀일까요?
useFetchData에 handle을 추가해서 내부에서 handle을 돌리면 된다는 말씀이신가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// 대시보드 데이터가 변경될 때 상태 업데이트
  const handleSuccess = () => {
    if (dashboardResponse) {
      setDashboards(dashboardResponse);
    }
  };
  // 대시보드 데이터를 가져오는 커스텀 훅 사용
  const {
    data: dashboardResponse,
    error,
    isLoading,
  } = useFetchData<DashboardsResponse>(
    ['dashboards', currentChunk],
    () => getDashboardsList('pagination', currentChunk, 5),
    false,
    handleSuccess,
  );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. 리스폰스 받자마다 handleSuccess 했는데도 더 늦나요?!! 신기하네요!!
방법 찾기 전까진 useEffect로 하는 게 좋겠네요!
미리 이런저런 시행착오 겪어주셔서 감사합니다!!!!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tanstack의 QueryCache 라는 훅을 써보는 건 어떨까요??
App.tsx에서 QueryClient랑 설정하면, 다른 컴포넌트에서 쓰는 쿼리들도 한 번에 처리할 수 있네요!

App.tsx

import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
...

// QueryCache로 글로벌 콜백 설정
const queryCache = new QueryCache({
  onSuccess: (data) => {
    console.log('Global onSuccess:', data);
    // 여기에 글로벌 onSuccess 로직 추가할 수 있어요!
  },
  onError: (error) => {
    console.error('Global onError:', error);
  },
});

// QueryClient
const queryClient = new QueryClient({
  queryCache,
  defaultOptions: {
    queries: {
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});

...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아까 어떤 글에서 query cache 봤는데, 어떻게 쓰는지 아직 몰라서 넘겼어요!
지금처럼 쓸 수 있으면 redirect에서도 error 상태 코드 보고 바로 할 수 있을 것 같네요!!
적용해볼까요??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵!! 전역적으로 적용해보면 좋을 것 같습니다! 이슈로 빼놓을까요??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋아요!! 새 이슈 만들고 얼른 머지해서 다른데서도 반영하는걸로 해요~~

} else {
setIsInitial(false);
}
}
}, [dashboardResponse, isInitial]);

if (error) {
return (
Expand All @@ -31,41 +50,44 @@ export default function DashboardList() {
);
}

// 다음 페이지로 이동하는 함수
const handleNext = () => {
if (currentChunk < totalPage) {
setCurrentChunk((prev) => prev + 1);
}
};

// 이전 페이지로 이동하는 함수
const handlePrev = () => {
if (currentChunk > 1) {
setCurrentChunk((prev) => prev - 1);
}
};

if (!dashboardData) return null;

return (
<section className='flex-col justify-between'>
<ul className='grid max-w-[350px] grid-rows-1 gap-3 font-semibold text-black-33 md:min-h-[216px] md:max-w-full md:grid-cols-2 md:grid-rows-3 lg:min-h-[140px] lg:max-w-screen-lg lg:grid-cols-3'>
{/* 새로운 대시보드 생성 버튼 */}
<li className='h-12 w-full rounded-lg border border-gray-d9 bg-white md:h-16'>
<button className='btn-violet-light size-full gap-4' type='button' onClick={() => openNewDashboardModal()}>
새로운 대시보드
<Image src={'/icons/plus-filled.svg'} alt='plus' width={22} height={22} />
</button>
</li>
{isLoading ? (
<>
{[...Array(5)].map((_, i) => (
{/* 대시보드 목록 표시 */}
{isLoading && !isInitial
? [...Array(5)].map((_, i) => (
<li key={i} className='h-12 w-full animate-pulse rounded-lg border border-gray-d9 bg-gray-fa md:h-16' />
))}
</>
) : (
<>
{dashboardResponse?.dashboards.map((dashboard) => (
))
: dashboardData.dashboards.map((dashboard) => (
<li className='h-12 w-full rounded-lg border border-gray-d9 bg-white md:h-16' key={dashboard.id}>
<Link href={`/dashboard/${dashboard.id}`} className={'btn-violet-light size-full rounded-md px-5'}>
<Link href={`/dashboard/${dashboard.id}`} className='btn-violet-light size-full rounded-md px-5'>
<div className='flex size-full items-center'>
<div className='rounded-full p-1' style={{ backgroundColor: dashboard.color }} />
<div className='mx-4 h-[28px] grow overflow-hidden text-ellipsis text-lg font-medium'>
<p className={`size-full`}>{dashboard.title}</p>
<p className='size-full'>{dashboard.title}</p>
</div>
{dashboard.createdByMe && (
<Image src={'/icons/crown.svg'} className='mr-3' alt='my' width={20} height={16} />
Expand All @@ -75,9 +97,7 @@ export default function DashboardList() {
</Link>
</li>
))}
</>
)}

{/* 페이지네이션 */}
<div className='md:col-span-2 lg:col-span-3 lg:row-start-3'>
<Pagination
currentChunk={currentChunk}
Expand Down
81 changes: 48 additions & 33 deletions src/containers/mydashboard/InvitedDashboardList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,71 +12,79 @@ import { getInvitationsList } from '@/services/getService';
import { putAcceptInvitation } from '@/services/putService';
import { Invitation, InvitationsResponse } from '@/types/Invitation.interface';

export default function InvitedDashboardList() {
const [invitations, setInvitations] = useState<Invitation[]>([]);
interface InvitedDashboardListProps {
initialInvitedDashboard: InvitationsResponse;
}

const InvitedDashboardList = ({ initialInvitedDashboard }: InvitedDashboardListProps) => {
const [invitations, setInvitations] = useState<Invitation[]>(initialInvitedDashboard?.invitations || []);
const [isInitial, setIsInitial] = useState<boolean>(!initialInvitedDashboard);
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const observerRef = useRef<HTMLDivElement | null>(null);
const [cursorId, setCursorId] = useState<number>(0);

const queryClient = useQueryClient();

const { data, error, isLoading } = useFetchData<InvitationsResponse>(['invitations'], () => getInvitationsList());
// 데이터를 가져오는 커스텀 훅
const { data, error, isLoading } = useFetchData<InvitationsResponse>(['invitations'], getInvitationsList);

// 데이터가 변경될 때 초대 상태 업데이트
useEffect(() => {
if (data) {
setInvitations(data.invitations);
setCursorId(data.cursorId ? data.cursorId : 0);
if (!isInitial) {
setInvitations(data.invitations);
setCursorId(data.cursorId ?? 0);
} else {
setIsInitial(false);
}
}
}, [data]);
}, [data, isInitial]);

const handleMoreInvitations = async (currentCursorId: number) => {
// 더 많은 초대 데이터를 가져오는 함수
const handleMoreInvitations = useCallback(async (currentCursorId: number) => {
if (currentCursorId !== 0) {
setIsFetchingNextPage(true);
try {
setIsFetchingNextPage(true);
const { data: nextData } = await getInvitationsList(10, currentCursorId);

if (nextData.invitations.length > 0) {
setInvitations((prevInvitations) => [...prevInvitations, ...nextData.invitations]);
}
setCursorId(nextData.cursorId || 0);
setCursorId(nextData.cursorId ?? 0);
} catch (err) {
console.error('데이터를 가져오는 중 오류가 발생했습니다:', err);
} finally {
setIsFetchingNextPage(false);
}
}
};
}, []);

// IntersectionObserver 콜백 함수
const handleObserver = useCallback(
async (entries: IntersectionObserverEntry[]) => {
(entries: IntersectionObserverEntry[]) => {
const target = entries[0];

if (target.isIntersecting && !isFetchingNextPage && cursorId && !isSearching) {
handleMoreInvitations(cursorId);
}
},
[cursorId, isFetchingNextPage, isSearching],
[cursorId, isFetchingNextPage, isSearching, handleMoreInvitations],
);

// IntersectionObserver 설정
useEffect(() => {
const observer = new IntersectionObserver(handleObserver, {
threshold: 0.8,
});

const observer = new IntersectionObserver(handleObserver, { threshold: 0.8 });
const currentObserverRef = observerRef.current;

if (currentObserverRef) {
observer.observe(currentObserverRef);
}

return () => {
if (currentObserverRef) {
observer.unobserve(currentObserverRef);
}
};
}, [observerRef, handleObserver]);

// 초대 수락/거절 처리 함수
const handleAcceptInvitation = async (invitationId: number, inviteAccepted: boolean) => {
try {
await putAcceptInvitation(invitationId, inviteAccepted);
Expand All @@ -88,17 +96,20 @@ export default function InvitedDashboardList() {
}
};

const handleChangeSearch = debounce(async (e: React.ChangeEvent<HTMLInputElement>) => {
const searchValue = e.target.value;
setIsSearching(!!searchValue);

try {
const { data: searchData } = await getInvitationsList(10, 0, searchValue);
setInvitations(searchData.invitations);
} catch (err) {
console.error('데이터를 가져오는 중 오류가 발생했습니다:', err);
}
}, 300);
// 검색어 변경 처리 함수
const handleChangeSearch = useCallback(
debounce(async (e: React.ChangeEvent<HTMLInputElement>) => {
const searchValue = e.target.value;
setIsSearching(!!searchValue);
try {
const { data: searchData } = await getInvitationsList(10, 0, searchValue);
setInvitations(searchData.invitations);
} catch (err) {
console.error('데이터를 가져오는 중 오류가 발생했습니다:', err);
}
}, 300),
[],
);

if (error) {
return (
Expand All @@ -112,10 +123,12 @@ export default function InvitedDashboardList() {
);
}

if (!invitations) return null;

return (
<section className='h-[400px] max-w-[350px] rounded-lg border-0 bg-white md:max-h-[740px] md:min-h-[530px] md:max-w-full lg:max-w-screen-lg'>
<p className='px-7 pb-5 pt-8 text-base font-bold text-black-33'>초대받은 대시보드</p>
{isLoading ? (
{isLoading && !isInitial && !initialInvitedDashboard ? (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

초기 데이터가 있으면 스켈레톤이 작동하지 않습니다

<Skeleton />
) : (
<>
Expand All @@ -140,4 +153,6 @@ export default function InvitedDashboardList() {
)}
</section>
);
}
};

export default InvitedDashboardList;
2 changes: 2 additions & 0 deletions src/hooks/useSignIn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMutation } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { setCookie } from 'cookies-next';
import { useDispatch } from 'react-redux';

import { postSignIn } from '@/services/postService';
Expand All @@ -22,6 +23,7 @@ export const useSignIn = () => {
onSuccess: (data) => {
dispatch(setUser(data));
dispatch(isLoading(false));
setCookie('token', data.accessToken);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인 할 때 쿠키로 액세스 토큰을 저장합니다

},
onMutate: async () => {
dispatch(isLoading(true));
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useUserDropdown.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { deleteCookie } from 'cookies-next';
import { useRouter } from 'next/router';
import { MouseEventHandler, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
Expand Down Expand Up @@ -29,6 +30,7 @@ const useUserDropdown = () => {
const handleLogoutClick: MouseEventHandler<HTMLButtonElement> = (e) => {
e.stopPropagation();
dispatch(clearUser());
deleteCookie('token');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그아웃 할 때 쿠키를 제거합니다( 여기에 추가하는게 맞나요? )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 테스트하니 잘 지워지네요

setIsOpen(false);
router.push('/');
};
Expand Down
Loading