조코딩 AI 챌린지를 보고 친구들아 모여랏~해서 진행하게된 해커톤st 프로젝트! 이틀 안에 끝내는 게 목표였는데, 그건 실패했다.... 4일만에 frontend는 개발을 완료했고, 이모저모 배우고 쓴게 많아서 써보는 회고 글이다.
gemini API 응답이 꽤 느린편이라 개선이 필요했다. frontend 담당이었던 나는 화면 전체에 loading을 넣기로 했다. 그래서 loading UX를 찾아보며 고민했다. 그러다 보게된 Loading UX 게시글!! (도움이 많이 되었다 ㅎㅎ)
개선되어도 꽤나 오래걸릴 수 있기 때문에 Skeleton UX Loading 을 넣기로 했다. tailwind css를 쓰고 있어서 간단하게! 처리했다. tailwind css 에서는 spin, ping, pulse, bounce 와 같은 다양한 animation css를 지원하고 있다. skeleton UX는 pulse를 쓰면 돼서 다음과 같이 처리했다.
import React from 'react';
const SkeletonAskVitamins = () => {
return (
<div className="flex space-x-4 pl-8 pr-8 pt-3">
<div className="w-1/2 p-4 bg-white rounded-lg shadow-lg">
<!-- 여기가 포인트!!!!! >> animate-pulse -->
<div className="animate-pulse space-y-4">
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gray-200 rounded-full"></div>
<div className="w-40 h-6 bg-gray-200 rounded"></div>
</div>
<div className="space-y-2">
<div className="w-full h-8 bg-gray-200 rounded"></div>
<div className="w-full h-8 bg-gray-200 rounded"></div>
<div className="w-full h-8 bg-gray-200 rounded"></div>
<div className="w-full h-8 bg-gray-200 rounded"></div>
<div className="w-full h-8 bg-gray-200 rounded"></div>
<div className="w-full h-8 bg-gray-200 rounded"></div>
</div>
<div className="flex justify-end">
<div className="w-24 h-8 bg-gray-200 rounded"></div>
</div>
</div>
</div>
<div className="w-1/2 p-4 bg-white rounded-lg shadow-lg">
<div className="animate-pulse space-y-4">
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gray-200 rounded-full"></div>
<div className="w-40 h-6 bg-gray-200 rounded"></div>
</div>
<div className="flex space-x-4">
<div className="w-1/2 h-60 bg-gray-200 rounded"></div>
<div className="w-1/2 h-60 bg-gray-200 rounded"></div>
</div>
<div className="flex justify-end">
<div className="w-32 h-8 bg-gray-200 rounded"></div>
</div>
</div>
</div>
</div>
);
};
export default SkeletonAskVitamins;
...
<!-- loading이 완료되지 않으면 skeleton ux loading 화면을 보여준다!! -->
{loading?
<SkeletonAskVitamins/>
:
<>
...
</>
역시 tailwind css가 짱이다. 더 많은 animation 사용법을 보고싶다면 공식문서를 참고하자!
화면이 불필요하게 렌더링되는 현상이 발생해서 함수를 메모이제이션하는 useCallback을 사용했다.
const handleSubmit = useCallback((event) => {
event.preventDefault();
(async () => {
try {
setCompleteAsk(false);
...
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setCompleteAsk(true);
}
})();
}, []);
✔️useCallback
함수가 불필요하게 다시 생성되지 않도록 메모이제이션한다. 주로 자식 컴포넌트에 콜백 함수를 전달할 때 사용한다.
✔️useMemo
값의 계산을 메모이제이션한다. 계산 비용이 높은 작업의 결과를 캐싱하여 성능을 최적화하는 데 사용한다.
✔️공통점
두 훅 모두 의존성 배열을 가지고 있으며, 의존성 배열의 값이 변경될 때만 메모이제이션된 함수나 값을 재생성한다. 이를 통해 불필요한 연산과 렌더링을 줄여 성능을 최적화한다.
상태가 불필요하게 업데이트될 때
상태가 변경되면 해당 상태를 사용하는 모든 컴포넌트가 다시 렌더링되므로, 상태 업데이트를 최소화해야한다.
애니메이션으로 인한 깜빡임
애니메이션으로 인해 깜빡임이 발생할 수 있다. 따라서 하드웨어 가속을 사용하거나, will-change 속성을 사용하여 성능을 개선할 수 있다.
.some-element {
will-change: transform;
transform: translateZ(0); /* 하드웨어 가속 */
}
로딩상태로 인한 깜빡임
컴포넌트가 로딩 중일 때 깜빡임이 발생한다. 따라서 이를 방지하려면, 로딩 상태를 관리하고 로딩 중임을 나타내는 스켈레톤 UI나 로딩 스피너를 표시하는 것이 좋다.
loading bar를 추가하면서 전역으로 사용하는 변수? state?가 필요했다. 어떤 라이브러리를 사용할까 고민하다가 러닝커브가 낮고, 가벼운 zustand를 사용하기로 했다.
npm install zustand
// store.js
import create from "zustand";
const useStore = create((set, get) => ({
loading: true, // 로딩 정의
defaultVitaminList: [ // 기본 영양제 조합 정의
...
],
setLoading: (loading) => set({ loading: loading }), // 로딩세팅
}));
export default useStore;
const Carousel = () => {
// zustand 에서 loading 가져오기
const loading = useStore((state) => state.loading);
// zustand 에 loading 세팅하기
const setLoading = useStore((state) => state.setLoading);
useEffect(() => {
(async () => {
try {
// loading 세팅
setLoading(true)
} catch (error) {
...
}
})();
}, []);
미들웨어가 무엇인지 와닿지 않아서 공부해보니, 상태를 로컬 스토리지에 저장하고 싶을 때 사용할 수 있다고 한다. 미들웨어를 통해 상태 로깅, 로컬 스토리지 저장 등을 할 수 있다.
✔️ zustand/middleware의 persist 미들웨어를 사용하기
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(persist((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
}), {
name: 'count-storage', // 로컬 스토리지 키
}));
테스트 API를 사용하니까 이미지가 액박 즉 제대로 로드되지 않는 현상을 마주했다.. UX에 좋지 않다고 생각이 들어 이미지 로드 오류를 처리하는 onError 이벤트를 사용해 대체이미지를 표시했다.
<span>
<!-- onError 이벤트로 이미지가 제대로 로드되지 않을 때 대체 이미지 적용하기 -->
<img src={data.imgUrl} alt='' onError={(e) => (e.target.src = '/images/example.png')} />
</span>