[Refactoring] 사용자가 경험하는 loading indicator 줄이기

junjeong·2025년 1월 31일

Linkbrary

목록 보기
3/6
post-thumbnail

👣 사건의 발단

두번째 과제이다. linkbrary를 마치며... 포스팅에서 언급했듯이 사용자가 linkbrary를 사용할 때 로딩이 지연되고 있음을 나타내는 loading indicator, 예를 들면 스피너라던가 스켈레톤ui를 마주하는 일이 너무 잦다. 라는 이슈이다.

🕵️‍♂️ 문제 접근

우선 스피너가 노출되는 비동기 요청을 하나씩 찾아보기로 했다. 어떤 경우들이 있을까??

  • 랜딩페이지에서 "링크 추가하기 버튼" 눌렀을 때
  • 로그인 페이지 "로그인 버튼" 눌렀을 때
  • 링크 페이지에서 링크목록 불러올 때
  • 폴더 추가하기
  • 새로운 링크 폴더에 추가할 때
  • 폴더 간 이동 시
  • 즐겨찾기 페이지에서 링크목록 불러올 때

나열하고 보니 성격이 비슷한 것들도 있는 것 같고 생각보다 많았다 ㅎㅎㅎ...

하지만 여기저기 리서치해보고 자문도 구해본 결과 모든 비동기 요청은 곧 "로딩"이기 때문에 인디케이터가 반드시 노출되는게 오히려 UX적으로 좋은 선택지라는 의견이 대부분이었다.

이유는 개발자 PC환경이 대부분 사용자보다 빠르기에 오히려 사용자 환경에서 예상치 못한 딜레이가 발생할 수 있어, "진행 중"이라는 피드백은 반드시 필요하다는 결론이었다.

그렇다면 현재 상황에서 무엇을 개선할 수 있을까?

💡 해결

1️⃣ 로딩 인디게이터의 노출 시간을 최소로 줄이자.

구글 리서치 자료에 따르면 모바일 웹 사이트의 로딩 시간이 3초 이상일 때 32%, 5초 이상은 90%, 6초 이상은 106% 마지막으로 10초가 넘으면 123%의 이탈률이 발생한다고 한다.
쉽게 말해 사용자 인내심의 한계의 시간은 3초 밖에 되지 않는다는 이야기이다.

그렇다면 앞서 나열했던 indicator들은 각각 몇초동안 노출되고 있을까??
아래는 모두 "느린 4g" 환경에서 측정한 데이터이며, 5번 측정했을 때의 평균값이다.

  • 랜딩페이지에서 "링크 추가하기 버튼" 눌렀을 때 -> 2.5초‼️
  • 로그인 페이지 "로그인 버튼" 눌렀을 때 -> 2.5초‼️
  • 링크 페이지에서 링크목록 불러올 때 -> 2.5초‼️
  • 폴더 추가하기 -> 1초
  • 새로운 링크 폴더에 추가할 때 -> 1.5초
  • 폴더 간 이동 시 -> 1.5초
  • 즐겨찾기 페이지에서 링크목록 불러올 때 -> 2.5초‼️

‼️ 기호가 들어간 애들은 모두 위험군에 속한다. 위험군에 속하는 애들 위주로 코드를 살펴보자.

⚙️ 랜딩페이지에 "링크 추가하기 버튼"

<SubmitButton
          onClick={handleClick}
          className="sm:mt-[24px] sm:w-[200px] sm:h-[50px] sm:text-[14px] md:mt-[40px] md:w-[350px] md:h-[53px] md:text-[18px] lg:mt-[40px] lg:w-[350px] lg:h-[53px] lg:text-[18px]"
        >
          링크 추가하기
        </SubmitButton>

첫번째 문제가 되는, 랜딩페이지에서 렌더링 되는 SubmitButotn이다. SubmitButton의 내부 로직은 크게 중요하지 않다. 문제는 handleClick이다.

  const handleClick = async () => {
    if (user) {
      await router.push("/link");
    } else {
      await router.push("/login");
    }
  };

handleClick은 단순히 user 데이터가 존재하면 "/link" 페이지로 보내고 없으면 "/login" 페이지로 보내는 기능을 하는 함수이다.

Next에서 router.push는 사용자 경험 개선을 위해 비동기를 지원한다.(페이지 이동 이전에 동기 코드가 작동하게 하기 위함) 하지만 여기서는 반드시 비동기일 필요는 없다고 판단해(페이지 이동 이전에 필요한 선행이 딱히 없기 떄문) 때문에 프리패칭이 가능한 Link 컴포넌트로 대체해주기로 했다.

<Link legacyBehavior href={user ? "/link" : "/login"}>
          <SubmitButton className="sm:mt-[24px] md:mt-[40px] lg:mt-[40px] sm:w-[200px] md:w-[350px] lg:w-[350px] sm:h-[50px] md:h-[53px] lg:h-[53px] sm:text-[14px] md:text-[18px] lg:text-[18px]">
            링크 추가하기
          </SubmitButton>
        </Link>

Link 컴포넌트란?
Link 컴포넌트는 기본적으로 해당 링크의 페이지를 미리 로드합니다. 사용자가 링크에 접근할 가능성이 있는 경우, 해당 페이지의 JavaScript와 데이터를 미리 가져와 페이지 전환이 더 빠르게 이루어집니다.

Link 컴포넌트로 바꾸니 미리 만들어둔 정적 HTML을 바로 로드하기 때문에 스피너를 거칠 것도 없이 페이지 이동이 바로 되는 모습이다;; UX가 훨씬 좋아졌다ㅎㅎ😅

⚙️ 로그인 페이지에 "로그인 버튼"

import Link from "next/link";
import useForm from "@/hooks/useForm";
import AuthInput from "@/components/Auth/AuthInput";
import SnsLogin from "@/components/Auth/SnsLogin";
import SubmitButton from "@/components/SubMitButton";
import AuthLayout from "@/components/Layout/AuthLayout";
import useAuthStore from "@/store/useAuthStore";
import { useEffect } from "react";
import { useRouter } from "next/router";

const LoginPage = () => {
  const { values, errors, handleChange, handleBlur, handleSubmit } =
    useForm(false);
  const router = useRouter();
  const { user } = useAuthStore();

  useEffect(() => {
    if (user) {
      router.replace("/");
    }
  }, [user, router]);

  return (
    <div className="bg-gray100 min-h-screen">
      <AuthLayout>
        <p className="mt-[16px] text-base font-normal">
          회원이 아니신가요?{" "}
          <Link
            href="/signup"
            className="cursor-pointer text-purple100 underline font-semibold"
          >
            회원가입하기
          </Link>
        </p>
        <form
          className="w-full sm:max-w-[325px] md:max-w-[400px] lg:max-w-[400px] mt-[30px]"
          aria-labelledby="login-form"
        >
          <AuthInput
            text="이메일"
            type="text"
            name="email"
            placeholder="이메일을 입력해주세요."
            value={values.email}
            onChange={handleChange}
            onBlur={handleBlur}
            error={errors.email}
          />
          <AuthInput
            text="비밀번호"
            type="password"
            name="password"
            placeholder="비밀번호를 입력해주세요."
            value={values.password}
            onChange={handleChange}
            onBlur={handleBlur}
            error={errors.password}
          />
          <SubmitButton
            type="submit"
            width="w-full"
            height="h-[53px]"
            className="mt-[30px]"
            onClick={handleSubmit}
          >
            로그인
          </SubmitButton>
          <SnsLogin />
        </form>
      </AuthLayout>
    </div>
  );
};

export default LoginPage;

로그인 페이지의 코드이다.

react-hook-form 라이브러리를 쓰고 있으며 단순히 로그인 버튼을 눌렀을 때 AuthInput에 담겨져 있는 value를 useForm이 반환하는 values에 담아 handleSubmit 함수 인자로 제담아 제출하는 방식이다.

handleSubmit은 react-hook-form이 자체적으로 지원해주는 함수이지만 꼭 라이브러리를 사용하는 것이 아닌 따로 커스텀한 함수를 사용했다고 할지라도 코드를 다르게 한다고 해서 요청 자체의 속도를 이보다 빠르게 하는 것은 불가능하다는 것이 GPT의 답변이었다.

고로 프론트 단에서 페이지 요청 자체의 비동기 요청을 빠르게 할 수 있는 방법은 따로 없는 것 같다.

앞서 했던 방법인 단순 페이지 이동에 버튼은 router.push가 아닌 Link 컴포넌트로 대체하는 것이 최선이었다.

2️⃣ 비동기 요청 자체를 줄이는 방법은 어떨까??

단순히 indicator에 초점을 맞추기보다 더 넓은 시각으로 "비동기 요청"에 초점을 맞춰보는 것이 좋을 것 같다.

시야를 다르게 하니 문제에 대한 해답이 더 선명하게 보이는 것 같다.

이전에 모든 비동기 요청은 일절 캐시 기능을 지원하지 않았다.
위에는 Link 컴포넌트로 전환하기 이전에 gif이다. 똑같은 버튼을 반복해서 눌렀을 때 api 요청 또한 반복 실행되는 모습이다.

페이지 요청을 캐싱해서 하면 어떨까?

비동기 요청 캐싱??

비동기 요청 캐싱이란 전적이 있는 비동기 요청이 다시 들어왔을 때, 요청을 새롭게 하는 것이 아닌 이전의 결과값만 그대로 다시 가져다주는 것을 의미한다.

만약 이것이 가능하다면 위에서 나열한 indicator 중 많은 것들이 불필요해진다.
1. 링크 페이지에서 목록 불러오기
2. 폴더 간 이동
3. 즐겨찾기 페이지 접속 시 링크목록 불러오기

이 중 요청이 2.5초나 걸리는 위험군에 속하는 케이스는 1,3번이니 이 친구들만 최적화를 해주어도 좋을 것 같다.

한가지 고려해야 하는 점이 있다면 캐싱이기 떄문에 이전에 결과값을 그대로 다시 받아오는 것이다. 고로 새롭게 받아오는 데이터에 최신화된 데이터가 하나라도 있을 경우 서버에 데이터와 사용자가 직면하는 데이터가 상이할 수 있다. 반드시 이전과 동일한 경우에만 해당한다는 이야기이다.

또 문제가 있다. 리액트 자체에서 fetch 요청을 할때 캐싱을 하는 기능을 지원해주지 않는다. 따로 SWR, React-Query라고 하는 대중적인 라이브러리를 사용해야 한다.(현재는 Tanstack-Query라고 한다...ㅎㅎ)

고로 이 작업은 시간이 비교적 많이 걸리는 작업이기 때문에 포스팅을 따로 나누어 진행하기로 했다. to be continue...

⭐️ 요약

오늘의 리팩토링 회고록을 간단히 정리해 보았다.
과정은 길었지만 핵심은 간단하다.😢

  1. 단순 페이지 이동에 쓰이는 상호작용 요소라면 router.push가 아닌 Link 컴포넌트를 활용하자.
  2. 순수 비동기 요청에 대한 최적화는 프론트가 아닌 백엔드의 몫이다. 서버를 최적화 해달라고 찡찡대는 방법은 있다!ㅎㅎ
  3. 똑같은 데이터를 반복적으로 받아올 필요는 없다. 비동기 요청이 불필요하게 반복될 경우 fetch를 캐싱하는 전략을 펼쳐보자. (ex.SWR, tanstack-Query)
profile
Whether you're doing well or not, just keep going👨🏻‍💻🔥

0개의 댓글