
요구 사항
- Tablet에서 상단 네비게이션 영역의 좌우 여백은 50px을 유지합니다
- 카드 리스트 영역의 좌우 최소 여백은 32px 입니다
- 카드 컴포넌트의 최소 너비는 186px 입니다
- 카드 리스트 영역이 줄어드는 것에 따라 카드 크기가 작아지다가 186px보다 작아질 때 하나의 행에 4개 → 3개씩 보이도록 합니다.

문제상황
- 렌더 시 렌더링이 완료되기 전까지 아무런 UI가 없어 사용자가 페이지의 맥락을 이해할 수 없다.(요소와 페이지네이션의 존재를 알 수 없다.)

- 반응형 요소 변화 시 요소 개수 오류(렌더링 속도에 따라 요소의 개수가 변하지 않는 오류가 발생)
- (8개 -> 6개)

- (6개 -> 8개)

- (8개 -> 6개)
시도
- 사용자에게 페이지 맥락을 이해시키기 위한 Skeleton UI를 적용하여 렌더링 시 요소 위치를 미리 보여줌
code
// before
//...
return (
<>
<UserCard name={name} src={src} count={questionCount} />
</>
);
};
export default SubjectCard;
// after
// ...
const shortName = name.length > 6 ? name.slice(0, 6) + '...' : name;
return (
<>
{
isLoading ? (
<SkeletonUserCard />
) : (
<UserCard name={shortName} src={src} count={questionCount} />
)
}
</>
);
};
export default SubjectCard;
- 적용 내용 확인

- 최초 렌더링 시 8개/6개 API를 모두 호출하여 반응형 변경 시 추가적인 API 호출을 방지
- 페이지네이션 시 총 페이지 수 개산이 달라져 페이지네이션에서 오류가 남(미해결)
- code
// before
// ...
const SubjectListPagination = ({ order }: SubjectListPaginationProps) => {
// ...
useEffect(() => {
setCurrentIndex(1);
}, [order]);
const handleSubjects = async ({ limit, offset, sort }: SubjectListParams) => {
const { count, results } = await getSubjectList({ limit, offset, sort });
setSubjects(results);
setMaxIndex(Math.ceil(count / pageSize));
};
useEffect(() => {
const offset = pageSize * (currentIndex - 1);
handleSubjects({ limit: pageSize, offset, sort: order });
}, [order, currentIndex, pageSize]);
// ...
};
export default SubjectListPagination;
// after
// ...
const useSubjectList = ({ order }: SubjectListFuncParams) => {
// ...
// 8 개일 때 상태
const {
data: data8,
isError: isError8,
isSuccess: isSuccess8,
} = useQuery({
queryKey: ['subjectList8', currentIndex, order],
queryFn: () => handleQueryRequest(8),
refetchOnWindowFocus: false,
});
// 6 개일 때 상태
const {
data: data6,
isError: isError6,
isSuccess: isSuccess6,
} = useQuery({
queryKey: ['subjectList6', currentIndex, order],
queryFn: () => handleQueryRequest(6),
refetchOnWindowFocus: false,
});
const handleCurrentIndex = (newValue: number) => {
setCurrentIndex(newValue);
};
// 에러 핸들링 및 로딩 처리
useEffect(() => {
// ...
const { count, results } = data8;
setItemCount(count);
setSubjects8(results);
// ...
}, [data8, isError8, isSuccess8]);
// 에러 핸들링 및 로딩처리
useEffect(() => {
//...
const { count, results } = data6;
setItemCount(count);
setSubjects6(results);
// ...
}, [data6, isError6, isSuccess6]);
// ...
적용 결과


- API 호출 캐싱을 활용한 네트워크 지연 최소화(Tanstack React Query를 이용하여 API 통신 캐싱)
- React Query의 패칭 방식을 이용하여 캐싱 내용을 미리 보여주고 API 통신 후, 비교하여 변동 사항이 있으면 리렌더링
- 페이지 당 최초 렌더링 이후 부드럽게 움직임 (해결)
code
// 반영 내용
// useSubjectListQuery.ts
const useSubjectListQuery = ({
currentIndex,
order,
pageSize,
}: SubjectListQueryParams) => {
const handleQueryRequest = async (page: number) => {
// ...
return useQuery({
queryKey: ['subjectList', currentIndex, order, pageSize],
queryFn: () => handleQueryRequest(pageSize),
refetchOnWindowFocus: false,
gcTime: 1000 * 60 * 2,
staleTime: 1000 * 5,
});
// useSubjectList.ts
// ...
const { data, isError, isSuccess, isLoading } = useSubjectListQuery({
order,
currentIndex,
pageSize,
});
// ...
useEffect(() => {
if (isLoading) {
setSubjects(getDefaultArray(pageSize));
} else if (isSuccess) {
const { count, results } = data;
setItemCount(count);
setSubjects(results);
setCurrentIndex((prev) => {
return prev > Math.ceil(count / pageSize)
? Math.ceil(count / pageSize)
: prev;
});
} else if (isError) {
}
}, [data, isError, isSuccess]);
// ...
- 적용 결과


개선 전 후 비교
초기 렌더링 레이아웃


반응형 레이아웃
- 6개 → 8개


- 8개 → 6개


느낀점
- 이 내용을 개선하고 있을 때, 스켈레톤 UI는 그럴 수 있지만 반응형 UI에서 이런 오류가 자주 있는 내용이며 대부분 빠르게 너비가 변경되는 경우가 적어 신경쓰지 않는다고 피드백을 받았다.
- 사소한 디테일이지만 이런 디테일에서 프로젝트 완성도와 프로젝트에 얼마나 진심이며, 사용자가 불편한 상황들을 고려해보는가가 중요한 내용이라고 생각한다.
- 또한, 이번 기회에 API 캐싱을 공부해보면서 React Query뿐만 아니라 Http 호출 단계에서의 캐싱도 가능하다는 것을 알았다.
- 다음에 기회가 된다면 HTTP 호출 단계에서 캐싱을 해보면서 다양한 캐싱 방식을 공부해볼 예정이다.
GitHub Link
반응형
'PROJECT > 에러핸들링' 카테고리의 다른 글
| [LinkBrary] Next.js 쿠키 만들기 ( with. API Routes ) (2) | 2024.09.25 |
|---|