Skip to content
Merged
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
"dependencies": {
"@emotion/css": "^11.11.2",
"@emotion/react": "^11.11.4",
"@gsap/react": "^2.1.2",
"@tanstack/react-query": "^5.24.1",
"axios": "^1.6.7",
"dotenv": "^16.4.5",
"es-toolkit": "^1.4.0",
"gsap": "^3.12.7",
"jotai": "^2.9.0",
"jsdoc-builder": "^0.0.6",
"lodash": "^4.17.21",
Expand Down
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 94 additions & 1 deletion src/app/home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Button from "@/components/common/Button";
import Lottie from "lottie-react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";
import { BREAK_POINTS, DESIGN_SYSTEM_COLOR } from "@/style/variable";
import { css } from "@emotion/react";
import {
Expand All @@ -21,13 +24,15 @@ import MAINBOTTOMIMG from "@/assets/main-bottom.gif";
import NewletterAni from "@/assets/newletter.json";
import CommunityAni from "@/assets/community.json";
import { useGetChartDataQuery } from "@/api/chartApi";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useModal } from "@/hooks/useModal.ts";
import { useAtom } from "jotai";
import { loginState } from "@/store/user";
import { SubscriptionModalContent } from "@/components/common/modal/SubscriptionModalContent";
import FloatingWidget from "@/components/app/home/floating/floatingWidget";
import { useApiTotalView } from "@/hooks/api/visitor/useApiTotalView";
import { VisitorCount } from "@/components/app/home/VisitorCount";

type CoinInfoType = {
[coin in "BTC" | "ETH" | "XRP"]: {
Expand All @@ -37,6 +42,8 @@ type CoinInfoType = {
};
};

gsap.registerPlugin(ScrollTrigger);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gsap 이라는 라이브러리를 처음봐서 검색을 조금 해보았는데요!

https://www.npmjs.com/package/@gsap/react
이런 라이브러리가 있는데, gsap 그대로 사용을 하신 이유가 있을까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gsap 정보가 많아서 그걸로 개발했었는데, 댓글 보고 @gsap/react를 알게 됐습니다!
찾아보고 써보니까 React에서 GSAP을 좀 더 깔끔하게 쓸 수 있더라구요
@gsap/react로 수정해서 올리겠습니다~!


export default function HomePage() {
// 차트 데이터 가지고 오기
const getBTCData = useGetChartDataQuery("BTC");
Expand All @@ -48,6 +55,77 @@ export default function HomePage() {
const { open, close } = useModal();
const navigate = useNavigate();

const { data: totalView, refetch, sendTotalView } = useApiTotalView();
const topNumberRef = useRef<HTMLSpanElement>(null);
const bottomNumberRef = useRef<HTMLSpanElement>(null);
const topSectionRef = useRef<HTMLDivElement>(null);
const bottomSectionRef = useRef<HTMLDivElement>(null);
const [currentNum, setCurrentNum] = useState<number|string>(0);
const [animationTotal, setAnimationTotal] = useState(0);

const slotAnimation = async () => {
if (!topNumberRef.current || !bottomNumberRef.current) return;

await refetch();

setCurrentNum(0);

let current = 0;
const step = Math.ceil(animationTotal / 20000);

const animate = () => {
current += step;
if (current >= animationTotal) {
setCurrentNum(animationTotal);
setCurrentNum(animationTotal.toLocaleString());
return;
}

setCurrentNum(current.toLocaleString());
requestAnimationFrame(animate);
};

requestAnimationFrame(animate);
};
Comment on lines +66 to +89
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize animation and API calling strategy.

The current implementation has several issues:

  1. await refetch() is called on every animation run, which can lead to excessive API calls
  2. There are two consecutive setCurrentNum calls at lines 80-81, with the second overriding the first
  3. No mechanism to prevent multiple animations from running simultaneously when scrolling rapidly
 const slotAnimation = async () => {
+  // Prevent multiple animations from running simultaneously
+  if (isAnimating.current) return;
   if (!topNumberRef.current || !bottomNumberRef.current) return;
+  isAnimating.current = true;

-  await refetch();
+  // Only refetch if needed (could add a time-based check)
+  // await refetch();

   setCurrentNum(0);
   
   let current = 0;
   const step = Math.ceil(animationTotal / 20000);
   
   const animate = () => {
     current += step;
     if (current >= animationTotal) {
-      setCurrentNum(animationTotal);
       setCurrentNum(animationTotal.toLocaleString());
+      isAnimating.current = false;
       return;
     }
     
     setCurrentNum(current.toLocaleString());
     requestAnimationFrame(animate);
   };

   requestAnimationFrame(animate);
 };

To implement this, add this ref at the top of your component:

const isAnimating = useRef(false);


useGSAP(() => {
if (!topSectionRef.current || !bottomSectionRef.current) {
return;
}

const ctx = gsap.context(() => {
ScrollTrigger.create({
trigger: topSectionRef.current,
start: "top 5%",
end: "top 5%",
toggleActions: "restart complete restart reset",
onEnter: () => slotAnimation(),
onEnterBack: () => slotAnimation(),
});

ScrollTrigger.create({
trigger: bottomSectionRef.current,
start: "bottom 80%",
toggleActions: "restart complete restart reset",
onEnter: () => slotAnimation(),
onEnterBack: () => slotAnimation(),
});

slotAnimation();
});

return () => ctx.revert();
}, { dependencies: [animationTotal] });

useEffect(() => {
sendTotalView();
}, []);
Comment on lines +120 to +122
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

API call should have appropriate dependency.

The sendTotalView function is called in a useEffect with an empty dependency array, which means it will only run once when the component mounts. However, this function lacks an explicit dependency.

If sendTotalView depends on any state or props, they should be included in the dependency array. If it's truly a side-effect that should only run once, add a comment explaining this intention.


🏁 Script executed:

#!/bin/bash
# Check how sendTotalView is implemented in the hook
rg -A 5 "useApiTotalView" --type ts

Length of output: 1334


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' src/hooks/api/visitor/useApiTotalView.ts

Length of output: 678


Stabilize sendTotalView in useEffect dependencies

The sendTotalView function returned by useApiTotalView is re‑created on every render, so React’s exhaustive‑deps rule will flag its omission from the dependency array. You have two options:

• Memoize sendTotalView inside the hook and include it in deps:

  • In src/hooks/api/visitor/useApiTotalView.ts wrap it in useCallback:
    const sendTotalView = useCallback(async () => {
      const response = await api.post("/user-service/api/v1/visitor");
      return response.data;
    }, []);
  • In your component’s useEffect, add it to the array:
    useEffect(() => {
      sendTotalView();
    }, [sendTotalView]);

• If you truly intend a “run once on mount” side‑effect, explicitly disable the lint rule with a comment:

useEffect(() => {
  sendTotalView();
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Please pick one approach to satisfy exhaustive‑deps and document your intent.

Locations to update:

  • src/hooks/api/visitor/useApiTotalView.ts (sendTotalView → wrap in useCallback)
  • src/app/home/index.tsx (adjust useEffect deps or add ESLint disable)


useEffect(() => {
if (!totalView) return;
setAnimationTotal(totalView.data);
}, [totalView]);

useEffect(() => {
if (!getBTCData.isSuccess || !getETHData.isSuccess || !getXRPData.isSuccess)
return;
Expand Down Expand Up @@ -161,10 +239,16 @@ export default function HomePage() {
>
{/* LEFT SIDE */}
<div>
<VisitorCount
currentNum={currentNum}
numberRef={topNumberRef}
sectionRef={topSectionRef}
/>
<h1
css={css`
font-size: 4.8rem;
line-height: 7.2rem;
margin-top: 20px;

${BREAK_POINTS.TABLET} {
}
Expand Down Expand Up @@ -529,6 +613,15 @@ export default function HomePage() {
alt=""
/>
</section>
{/* 하단 배너 */}
<section>
<VisitorCount
currentNum={currentNum}
numberRef={bottomNumberRef}
sectionRef={bottomSectionRef}
isBottom
/>
</section>
</article>
);
}
9 changes: 9 additions & 0 deletions src/assets/black_ether.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/assets/green_bit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/assets/orange_bit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions src/assets/skyblue_L.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/assets/yellow_bit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading