프로필페이지 속도개선

김현준·2025년 6월 3일

문제

마이페이지 저장된 로그 등 콘텐츠 로딩시간이 김

측정방법

서버 데이터 패치 함수에서 시간 측정

export async function fetchBookmarkedLogs({ userId, currentPage = 1, pageSize = 12 }: logBookmarkListParmas): Promise<LogsReseponse> {
  const start = Date.now(); // 측정 시작

  try {
    ...
    const duration = Date.now() - start;
    console.log(fetchBookmarkedLogs 처리 시간: ${duration}ms);

    return {
      success: true,
      data: filterBookmarkedLogs,
      meta: {
        ...
        httpStatus: 200,
      },
    };
  } catch (_error) {
    console.error(_error);
    return {
      success: false,
      ...
    };
  }
}

측정 결과

  • 3초 내외로 줄이지 않으면 유저 이탈 우려

적용할 방법

  • 현재는 라우터 핸들러를 이용한 api + 클라이언트 fetch 사용
  • dehydrate + HydrationBoundary 방식으로 리팩토링

기존방식과 dehydrate + HydrationBoundary 방식의 차이

항목전략 2번 (/api/logs/bookmark + CSR fetch)전략 3번 (dehydrate + HydrationBoundary)
데이터 fetch 시점클라이언트 진입 후 시작됨서버에서 먼저 fetch 완료 후 렌더
초기 렌더 속도느릴 수 있음 (로딩 스피너 필요)빠름 (로딩 없이 즉시 렌더)
SEO 노출데이터 없이 HTML만 렌더됨데이터 포함된 HTML 렌더
중복 fetch브라우저 새로고침마다 fetch 발생dehydrate로 SSR 캐시 재사용
사용자 경험잠깐 깜빡이거나 스켈레톤 필요거의 깜빡임 없음

개선 전 코드

1. /api/logs/bookmark

export async function GET(req: NextRequest) {
  const { searchParams } = req.nextUrl;

  const userId = searchParams.get('userId');
  const currentPage = parseInt(searchParams.get('currentPage') ?? '1', 10);
  const pageSize = parseInt(searchParams.get('pageSize') ?? '12', 12);

  if (!userId) {
    return NextResponse.json(
      {
        success: false,
        msg: ERROR_MESSAGES.COMMON.UNAUTHORIZED,
        errorCode: ERROR_CODES.COMMON.UNAUTHORIZED,
      },
      { status: 400 }
    );
  }

  try {
    // 서버 패치 함수로부터 데이터를 가져옴
    const result = await getBookmarkedLogs({ userId, currentPage, pageSize });

    if (!result.success) {
      return NextResponse.json(result, {
        status: 404,
      });
    }

    return NextResponse.json(result, { status: result.meta?.httpStatus ?? 200 });
  } catch (_error) {
    console.error(_error);
    return NextResponse.json(
      {
        success: false,
        msg: ERROR_MESSAGES.COMMON.INTERNAL_SERVER_ERROR,
        errorCode: ERROR_CODES.COMMON.INTERNAL_SERVER_ERROR,
      },
      { status: 500 }
    );
  }
}

2. useLogsBookmark 훅

async function fetchUseLogsBookmark(params: BookmarkParams): Promise<LogsReseponse> {
  const query = toQueryString(params);
  const res = await fetch(`/api/logs/bookmark?${query}`);
  const data = await res.json();
  return data;
}

export default function useLogsBookmark({
  userId,
  currentPage = 1,
  pageSize = 12,
}: BookmarkParams) {
  const params = {
    userId,
    currentPage,
    pageSize,
  };
  const tab = useProfileTabStore((state) => state.tab);
  return useQuery({
    queryKey: logKeys.bookmarkList(params),
    queryFn: () => fetchUseLogsBookmark(params),
    enabled: !!userId && tab === 'savedLog',
    placeholderData: keepPreviousData,
  });
}

dehydrate + HydrationBoundary 적용

profile
기록하자

0개의 댓글