실습했던 스켈레톤 UI을 팀프로젝트에 적용해보자!

하영·2024년 10월 15일
0

Next.js

목록 보기
13/19
post-thumbnail

지난번 들었던 인프런 강의에서 스켈레톤 을 배웠던게 생각이 났다.
지금 api에서 데이터를 불러오는 과정에서 조금 딜레이가 생기는데 단순히 글씨로 "로딩중" 이라고 뜨는게 조금 아쉽다고 생각했다.
그래서 이번 기회에 한번 간단하게 적용해보기로 했다!


스켈레톤 UI 구현하기

적용할 컴포넌트는 검색페이지로 검색한 키워드의 결과가 리스트로 쭉 뜨는 페이지이다.

이런 느낌!인데 이 이미지가 뜨기 전에 빈 화면만 보이거나 아래에 있는 페이지네이션이 먼저 보이는게 아쉬웠던 것...!

스켈레톤 UI 구현하기 👩🏻‍💻

이것저것 알아보다가 우선 배운 코드가 익숙하니까 그대로 적용시켜보고자 했다.

01. 스켈레톤 기본 뼈대 만들기

type SkeletonCardProps = {
  layout: "horizontal" | "vertical"; //세로일 때, 가로일 때 선택가능
  width?: string; // width는 선택적으로 부여
  height: string; // height는 필수부여
};

export default function SkeletonCard({ layout, width = "150px", height = "150px" }: SkeletonCardProps) {
  return (
    <div className={`flex ${layout === "horizontal" ? "gap-6" : "flex-col items-center"}`}>
      <div className="bg-gray-300 animate-pulse" style={{ width, height }}></div>

      <div className={`flex ${layout === "horizontal" ? "flex-col gap-2" : "flex-col text-center mt-2"} w-full`}>
        <div className="bg-gray-300 animate-pulse h-6 w-3/4"></div>
        <div className="bg-gray-300 animate-pulse h-4 w-1/2"></div>
        <div className="bg-gray-300 animate-pulse h-4 w-1/4"></div>
      </div>
    </div>
  );
}

tailwind를 쓰고 있어서 장황해보이지만 색상값을 없애보면 비교적 간단한 코드이다.

layout === "horizontal" ? "gap-6" : "flex-col items-center"

layout을 가로버전(이미지 옆에 글 있는 레이아웃) / 세로버전(이미지 아래에 글 있는 레이아웃) 으로 선택한 후 그에 맞게 속성을 부여할 수 있게 만들었다.

그리고 animate-pulse 를 사용해서 로딩중일 때 약간의 효과를 주었다.


02. 반복문으로 List 만드는 함수 생성

import SkeletonCard from "./SkeletonCard";

export default function SkeletonList({ count }: { count: number }) {
  return (
    <div className="flex flex-col gap-4">
      {new Array(count).fill(0).map((_, idx) => (
        <SkeletonCard key={`skeleton-${idx}`} layout="horizontal" width="150px" height="150px" />
      ))}
    </div>
  );
}

앞에서 만들었던 SkeletonCard를 import 해서 count 매개변수에 따라 개수가 채워져서 반복해서 리스트가 만들어지게 했다.
이렇게 작성해두면 5개만 사용하고 싶을 때, 10개를 사용하고 싶을 때 모두 동적으로 값을 받아서 사용할 수 있게 된다.


03. 함수 hook 수정하기

문제는 이 부분이었다. Suspense를 사용해서 적용할 생각이었는데 Suspense는 서버컴포넌트에서 주로 사용한다고한다. 데이터를 불러오는 동안 스켈레톤 UI를 보여주는 방식으로 활용하는데 내가 작성한 코드는 클라이언트 컴포넌트에서 데이터를 처리하는 방식이므로, Suspense는 사용할 수 없었다...

useSearchData 코드

import { useEffect, useState } from "react";
import { searchTracks } from "@/lib/spotifyToken";
import { SearchTrack } from "@/types/search";

export const useSearchData = (query: string, pageQuery: number, limit = 20) => {
  const [results, setResults] = useState<SearchTrack[]>([]);
  const [totalPages, setTotalPages] = useState(1);
  const [currentPage, setCurrentPage] = useState(1);

  // 전체 데이터 수 페이지네이션 생성
  const fetchTotalPages = async (): Promise<void> => {
    try {
      const response = await searchTracks(query, 0, limit);
      const { total } = response.tracks;
      setTotalPages(Math.ceil(total / limit));
    } catch (error) {
      console.error("전체 데이터 가져오기 실패:", error);
    }
  };

  // 해당 페이지의 데이터만 가져오기
  const fetchData = async (page: number): Promise<void> => {
    const offset = (page - 1) * limit;
    try {
      const response = await searchTracks(query, offset, limit);
      const { items } = response.tracks;
      setResults(items);
    } catch (error) {
      console.error("데이터 가져오기 실패:", error);
    }
  };

  // query 바뀔 때 실행
  useEffect(() => {
    if (query) {
      setResults([]);
      setCurrentPage(pageQuery);
      fetchTotalPages();
      fetchData(pageQuery);
    }
  }, [query, pageQuery]);

  return { results, totalPages, currentPage, setCurrentPage, fetchData};
};

페이지네이션을 만들면서 함수 로직이 길어지면서 만든 Hook이다.
여기에 함수를 넣어놓았으니... 클라이언트 컴포넌트라면 TanStack Query를 사용했을 때 isLoading 으로 "로딩중..." 이렇게 작성한 것처럼 만들어보면 되지 않을까 하는 생각이 들어서 추가해보았다.


isLoading State 추가한 코드

export const useSearchData = (query: string, pageQuery: number, limit = 20) => {

  const [isLoading, setIsLoading] = useState(true);

  // 전체 데이터 수 페이지네이션 생성
  const fetchTotalPages = async (): Promise<void> => {
		// 중략
  };

  // 해당 페이지의 데이터만 가져오기
  const fetchData = async (page: number): Promise<void> => {
    setIsLoading(true); // ✅ 로딩 추가하기
    const offset = (page - 1) * limit;
    try {
      const response = await searchTracks(query, offset, limit);
      const { items } = response.tracks;
      setResults(items);
    } catch (error) {
      console.error("데이터 가져오기 실패:", error);
    } finally {
      setIsLoading(false); // ✅ 로딩 추가하기
    }
  };

  // query 바뀔 때 실행
  useEffect(() => {
    if (query) {
      setResults([]);
      setCurrentPage(pageQuery);
      fetchTotalPages();
      fetchData(pageQuery);
    }
  }, [query, pageQuery]);

  return { results, totalPages, currentPage, setCurrentPage, fetchData, isLoading }; // ✅ 로딩 추가하기
};

페이지의 데이터가 가져와지는 부분에서 스켈레톤이 나오면 되니까 불러오기 시작하면 true가 되어서 보여지게 만들고 데이터가 다 불러와지면 false를 줘서 사라지게 만들었다.


04. 수정한 hook , page.tsx 에 적용하기

isLoading 상태를 추가하였으니 useSearchData를 사용하고 있는 페이지로 들어가서 코드를 수정했다.

"use client";

import { SearchTrack } from "@/types/search";
import { useSearchData } from "@/hooks/useSearchData";
import TrackCard from "@/components/TrackCard";
import SkeletonList from "@/components/SkeletonList"; // 스켈레톤 컴포넌트 가져오기

export default function SearchPage({
 // 코드 중략

  // ✅ useSearchData 훅 적용!
const { results, totalPages, currentPage, setCurrentPage, fetchData, isLoading } = useSearchData(query, pageQuery);  
  
  return (
    <div>
      <div >
        <h1>Pick your Track!</h1>

        {/* 로딩 상태일 때 스켈레톤 UI를 표시 */}
        {isLoading ? (
          <SkeletonList count={10} /> // ✅ 스켈레톤 UI 카드 10개 표시
        ) : (
          <ul>
            {results.map((track: SearchTrack) => (
              <li
                key={track.id}
              >
                <TrackCard track={track} />
              </li>
            ))}
          </ul>
        )}
      </div>
    
      <div>
        <Pagination totalPages={totalPages} currentPage={currentPage} pageRange={10} movePage={movePage} />
      </div>
    </div>
  );
}

딱 스켈레톤 관련된 함수만 보면 이렇게 작성되어있다.
isLoading 추가한 useSearchData 훅을 불러오고 (isLoading 값만 추가함) count 매개변수 값을 받은 만큼 뿌려주므로 10개만 보여지게 작성했다.


05. 결과화면 보기

중간에 회색으로 보이는게 내가 만든 스켈레톤!!
다행히 잘 적용이 된 듯 하다!


06. 생각해볼 부분

  1. 클라이언트 컴포넌트에서 적용할 때 이 방법이 맞을까?
    작성한 코드가 맞는 방식인지 잘 모르겠다. 이왕 써보기로 한 거 올바른 방식으로 적용해보고 싶은데 새벽에 하다보니 컨펌 받을 수 없어서 이건 내일 튜터님께 여쭤볼 예정!

  2. 다른 팀원들도 이 스켈레톤을 사용하게 하려면 어떻게 해야할까? 🤔
    함수로 만들었으니까 import 해서 쓰면 될 것 같은데 정확히 어떤 코드에 적용을 하라고 공유하면 좋을지 정리가 안 되었다...
    우선 다른 분들 코드를 보면서 생각해보고 방법을 찾아야겠다.

  3. 약간 둔탁하게 투둑투둑 끊기는 느낌이라 아쉽다.
    자연스러운 느낌을 원했는데 스켈레톤을 적용하고 보니 생각보다 로딩 속도가 그렇게 길지 않았나보다..ㅋㅋ 과제 마감까지 시간이 부족해서 시간 부여라든가 하는 디테일한 부분까지 할 수 있을지 모르겠지만 안 되더라도 꼭 개인적으로 리펙도링 해야지 🔥


profile
왕쪼랩 탈출 목표자의 코딩 공부기록

0개의 댓글