Skip to content

junye0l/WHYNE

 
 

Repository files navigation

WHYNE

사용자가 다양한 와인 리뷰를 확인하고, 구매 여부를 쉽게 판단할 수 있는 와인 리뷰 플랫폼

담당역할 및 성과

랜딩페이지 리뉴얼

portfolio-landing

  • 기존 디자인 시안은 정적인 랜딩페이지로 사용자가 최초 접속했을 때 웹사이트에 대한 매력도가 떨어진다고 생각하였습니다.

  • 그렇기에 브랜드컬러를 확실하게 각인시킬 수 있도록 팀원들과 협의끝에 디자인시안을 작업하였습니다.

  • 또한 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 파이프라인 최적화 (Chromatic 조건부 실행)

문제 상황

  • 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 코멘트로 자동 남기도록까지는 확장하지 못해, 추후 워크플로우를 더 발전시킬 여지가 남아 있다.

낙관적 업데이트 적용 (Optimistic Update)

문제 상황

  • 리뷰 좋아요 기능에서 서버 응답 지연으로 인해, 사용자가 좋아요 버튼을 눌렀을 때 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 구조가 단일 리뷰 단위가 아니어서 개선 범위를 넓히지 못한 점이 아쉽다.

  • 추후 리뷰 리스트 전체가 아닌 "리뷰 단일 캐싱 구조"로 리팩토링한다면 더 정교한 낙관적 업데이트를 적용할 수 있을 것으로 보인다.

About

코드잇 중급 프로젝트

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 98.8%
  • Other 1.2%