사용자가 다양한 와인 리뷰를 확인하고, 구매 여부를 쉽게 판단할 수 있는 와인 리뷰 플랫폼
-
기존 디자인 시안은 정적인 랜딩페이지로 사용자가 최초 접속했을 때 웹사이트에 대한 매력도가 떨어진다고 생각하였습니다.
-
그렇기에 브랜드컬러를 확실하게 각인시킬 수 있도록 팀원들과 협의끝에 디자인시안을 작업하였습니다.
-
또한 GSAP 라이브러리를 활용하여 동적 애니메이션을 추가해 사용자 흥미도를 향상 시켰습니다.
// 공통 무한스크롤 훅 코드
export function useInfiniteScroll<T>({
queryKey,
fetchFn,
enabled = true,
staleTime = 60 * 1000,
rootMargin = "200px",
threshold = 0.1,
}: UseInfiniteScrollOptions<T>) {
const observerRef = useRef<HTMLDivElement>(null);
const queryResult = useInfiniteQuery({
queryKey,
queryFn: ({ pageParam }) => fetchFn(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: undefined,
enabled,
staleTime,
});
const { fetchNextPage, hasNextPage, isFetchingNextPage } = queryResult;
useEffect(() => {
if (!observerRef.current || !hasNextPage || isFetchingNextPage || !enabled) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) fetchNextPage();
},
{ threshold, rootMargin }
);
observer.observe(observerRef.current);
return () => observer.disconnect();
}, [hasNextPage, isFetchingNextPage, fetchNextPage, enabled, threshold, rootMargin]);
const allItems = queryResult.data?.pages.flatMap((p) => p.list) ?? [];
return { ...queryResult, allItems, observerRef };
}-
리뷰, 등록 와인, 마이페이지 등 여러 화면에서 동일하게 사용되는 무한 스크롤 로직을 하나의 훅으로 통합했습니다.
-
React Query의 useInfiniteQuery와 IntersectionObserver를 활용하여 커서 기반 자동 페이징을 구현했습니다.
문제 상황
-
Storybook UI 변경 여부와 관계없이 모든 PR에서 Chromatic이 실행되면서, 빌드 시간이 길어지고 CI 자원이 불필요하게 소모되는 문제가 있었다.
-
디자인 변경이 없거나 단순 로직 수정만 있는 PR에서도 동일하게 Chromatic이 돌기 때문에, 리뷰 속도와 피드백 루프가 느려지는 문제가 있었다.
문제 원인
-
GitHub Actions 워크플로우에서 Chromatic 작업이 브랜치나 라벨 조건 없이 항상 실행되도록 설정되어 있었다.
-
Storybook/디자인 관련 변경이 있는지와 무관하게, PR이 생성되거나 동기화되면 매번 Chromatic 빌드가 수행되었다.
문제 해결방법
jobs:
chromatic:
# develop 브랜치이거나, PR에 chromatic 라벨이 있을 때만 실행
if: |
github.ref == 'refs/heads/develop' ||
contains(github.event.pull_request.labels.*.name, 'chromatic')
steps:
- uses: actions/checkout@v4
- run: yarn
- uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}-
기준 브랜치를 develop로 두고, 해당 브랜치에 푸시될 때는 항상 Chromatic을 실행하도록 유지했다.
-
그 외 브랜치의 PR에서는, 리뷰어/작성자가 chromatic 라벨을 직접 추가한 경우에만 Chromatic을 실행하도록 조건부 실행을 추가했다.
-
이를 통해 Storybook/디자인 변경이 포함된 PR에만 선택적으로 시각 회귀 테스트를 수행할 수 있도록 했다.
아쉬운 점
- Chromatic 실행 결과(빌드 링크, 변경 스냅샷 요약)를 PR 코멘트로 자동 남기도록까지는 확장하지 못해, 추후 워크플로우를 더 발전시킬 여지가 남아 있다.
문제 상황
- 리뷰 좋아요 기능에서 서버 응답 지연으로 인해, 사용자가 좋아요 버튼을 눌렀을 때 UI 반응이 즉각적으로 보이지 않는 문제가 있었다.
- 버튼 클릭 후 상태가 반영되기까지 짧지만 눈에 보이는 딜레이가 있어 사용자 경험(UX)이 떨어졌다.
문제 원인
- 기존 로직은 서버에 좋아요/취소 요청을 보내고, 응답을 받은 뒤에야 화면 상태를 업데이트하고 있었다.
- 이 과정에서 네트워크 지연 시간이 그대로 사용자 인터랙션에 드러나면서 UI 피드백이 느릴 수밖에 없는 구조였다.
문제 해결방법
onMutate: async (nextLike) => {
await queryClient.cancelQueries({ queryKey: ["wine", wineId] });
const previous = queryClient.getQueryData(["wine", wineId]);
queryClient.setQueryData(["wine", wineId], (old) => ({
...old,
reviews: old.reviews.map((item) =>
item.id === reviewId ? { ...item, isLiked: nextLike } : item
),
}));
return { previous };
};
onError: (_err, _nextLike, context) => {
if (context?.previous) {
queryClient.setQueryData(["wine", wineId], context.previous);
}
};-
좋아요 버튼을 클릭하면 서버 응답을 기다리지 않고, 즉시 캐시 데이터를 업데이트하도록 구현했다.
-
onMutate에서 기존 데이터를 저장해두고 UI를 먼저 변경하며, 에러 발생 시 이전 상태로 되돌리는 롤백 전략을 적용했다.
-
이를 통해 즉각적인 시각적 피드백을 제공하여 사용자 경험을 크게 개선했다.
아쉬운 점
-
좋아요 개수 변경(count)까지도 동시에 낙관적 업데이트를 적용하고 싶었으나, API 구조가 단일 리뷰 단위가 아니어서 개선 범위를 넓히지 못한 점이 아쉽다.
-
추후 리뷰 리스트 전체가 아닌 "리뷰 단일 캐싱 구조"로 리팩토링한다면 더 정교한 낙관적 업데이트를 적용할 수 있을 것으로 보인다.
