Rscroll event ์ต์ ํ๋ก ์นํ์ด์ง ์ฑ๋ฅ ๊ฐ์ ํ๊ธฐ
์ ํฌํ NDD๋ ๊ณฐํฐ๋ทฐ ์๋น์ค๋ฅผ ๊ฐ๋ฐ์ค์ ๋๋ค!
๊ณฐํฐ๋ทฐ ์๋น์ค์ pre-alpha test๋ฅผ ์งํํ๊ธฐ ์ํด , ๋ค์๊ณผ ๊ฐ์ด ๋ค์ด๋ฒ ๋ถ์คํธ์บ ํ 8๊ธฐ ๋ถ๋ค๊ป ์ค๋ฌธ์ ์์ฒญ ๋๋ ธ์ต๋๋ค.
ํน์๋ ๋ค์ด๊ฐ๋ณด์ค๊น ์ถ์ด์ ๋งํฌ๋ฅผ ์ถ๊ฐ๋ก ๋จ๊ธฐ๊ฒ ์ต๋๋ค! [Gomterview ๋ฐฐํฌ ์ฌ์ดํธ] > https://www.gomterview.com/ (ํน์ ๊ตฌ๊ธ์์ "๊ณฐํฐ๋ทฐ"๋ฅผ ๊ฒ์ํด์ฃผ์ธ์!) [Gomterview ์ค๋ฌธ] > https://forms.gle/FjLDygaGBZm8tnbP6 > [Demo ์์] > https://youtu.be/LtpJC6bO-2c
์ค๋ฌธ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์๋๋ฐ, ๋ค๋ฅธ ๋ต๋ณ๋ค๋ ๋ง์์ง๋ง! ๊ฐ์ฅ ํผ๋๋ฐฑ์ ๋ฐ์๋ ๊ธฐ๋ฅ์ ๋ค๋ฅธ ์ ์ ์ ์ง๋ฌธ๋ฆฌ์คํธ๋ฅผ ๊ณต์ ํ๊ณ ์ฌ์ฉํ๋ ๊ธฐ๋ฅ์ด์์ต๋๋ค!
๊ทธ์ ๋ฐ๋ผ ๊ทธ ์ฃผ์ฐจ์ ๋ฐ๋ก ๋ฉด์ ๋ฌถ์ ๋ฆฌ์คํธ (๋ฉด์ set๋ผ ๋ช ๋ช ํ์ต๋๋ค.) ๋ฅผ ๊ณต์ ํ ์ ์๋ ํ์ด์ง์ ๋ํ ๊ธฐํ์ด ์งํ๋์์ต๋๋ค.
~~ํ ์ค ๊ฐ๋ฐ์์ 100ํผ ์ทจ์ ๋ณด์ฅ์ด๋ผ๋... ๋๋ฌด ๊ถ๊ธํ๊ฑธ...?~~
ํด๋น ํ์ด์ง์ Side Menu๋ ํ๋ฉด์ด ์คํฌ๋กค๋๋ฉด์ ์ ์ ์ view ๋ด๋ถ์์ ์ด๋ ํด์ผํ๋ ์ปดํฌ๋ํธ์์ต๋๋ค.
fixed๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด ํธํ๊ฒ ์ปดํฌ๋ํธ๋ฅผ ์ ์ ์ view๋ด๋ถ์ ๊ณ ์ ์ํฌ ์ ์์ต๋๋ค. ํ์ง๋ง ํ๋ฉด์ด ๊ต์ฅํ ๋์ (๋์ผ๋ชจ๋ํฐ๋ฅผ ์ฌ์ฉํ๋ ์ ์ ์ ๊ฒฝ์ฐ, ๋ฉ์ธ ๋ชจ๋ํฐ๊ธ ์ฝ 30์ธ์น ์ด์) ํ๋ฉด ์์์ ๋์์ด ๋ถ๋๋ฝ์ง ๋ชปํฉ๋๋ค. ์ ๋ ๋ฉด์ ๋ฌถ์๋ฆฌ์คํธ๋ฅผ ๋ ๋๋ง ํ๋ ์ปดํฌ๋ํธ๋ฅผ ๊ธฐ์ค์ผ๋ก ์ด๋์ํค๊ธฐ ์ํด, absolute ์์ฑ์ ์ฌ์ฉํด ๋ถ๋ชจ ์ปดํฌ๋ํธ๋ฅผ relative๋ก ๋๊ณ ์์น ์์ผฐ์ต๋๋ค.
์ฌ๊ธฐ์ ์ฌ์ํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ํด๋น ์ปดํฌ๋ํธ(์ฌ๊ธฐ์ ๋ถ๋ชจ์ธ ๋ฉด์ ๋ฌถ์ ๋ฆฌ์คํธ ๋ ์ด์์ ์ปดํฌ๋ํธ)๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ณ ์ ์์ผ์ ์๋์ ์ธ ์์น๋ฅผ ์ ์ฉํด์ผ ํ๋ค๋ฉด, left ์์ฑ์ ์ ํจํ๋ top ์์ฑ์ ๊ณ ์ ๋์ง ์๊ณ , ์ง์์ ์ผ๋ก ๋ณ๊ฒฝ์์ผ์ค์ผ๋ง ํ๋ค๋ ์ ์ด์์ต๋๋ค.
๊ทธ์ ๋ฐ๋ผ ์ ๋ ๋ค์๊ณผ ๊ฐ์ด ์๋ํ์ต๋๋ค.
import React, { useEffect, useState } from "react";
import { Box } from "@foundation/index";
import { css } from "@emotion/react";
import { HTMLElementTypes } from "@/types/utils";
type CategoryMenuType = HTMLElementTypes<HTMLDivElement>;
const CategoryMenu: React.FC<CategoryMenuType> = ({ children, ...arg }) => {
// ์คํฌ๋กค์ ๋ฐ๋ผ ๋ณํ top ์์น๋ฅผ ์ํ๋ก ๊ด๋ฆฌ
const [topPosition, setTopPosition] = useState(200);
useEffect(() => {
const handleScroll = () => {
// ์ฌ๊ธฐ์ ์คํฌ๋กค์ ๋ฐ๋ฅธ topPosition ๊ณ์ฐ ๋ก์ง์ ์ถ๊ฐ
// ์: ์คํฌ๋กค ์์น์ ๋ฐ๋ผ topPosition ๊ฐ์ ์กฐ์
const newTopPosition = 200 + window.scrollY;
setTopPosition(newTopPosition > 0 ? newTopPosition : 0);
};
// ์คํฌ๋กค ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ถ๊ฐ
window.addEventListener("scroll", handleScroll);
// ์ปดํฌ๋ํธ๊ฐ ์ธ๋ง์ดํธ ๋ ๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ ๊ฑฐ
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<Box
css={css`
position: absolute;
top: ${topPosition}px; // ๋์ ์ผ๋ก ๊ณ์ฐ๋ top ์์น ์ฌ์ฉ
left: -120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: start;
row-gap: 0.75rem;
padding: 1.5rem;
border: 1px solid blue;
width: auto;
height: auto;
`}
{...arg}
>
{children}
</Box>
);
};
export default CategoryMenu;
์ค๊ฐ์ค๊ฐ์ ๋๊ธฐ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค!
์ฆ scroll ์ด๋ฒคํธ๋ฅผ ๋ฐ์ ์ฃผ๊ธฐ์ ์ผ๋ก top ์์ฑ์ ๊ฐฑ์ ์์ผ์ฃผ๋ ๊ฒ์ด์์ต๋๋ค. ๋น์ฐํ๊ฒ๋ scroll ์ด๋ฒคํธ๋ ๊ต์ฅํ ์์ฃผ ์ผ์ด๋๋ ์ด๋ฒคํธ์ด๊ธฐ ๋๋ฌธ์ ์ต์ ํ๋ ํ์์ ์ด์์ต๋๋ค. ์คํฌ๋กค๊ณผ ๊ฐ์ด ์ ํด์ง ์ฃผ๊ธฐ์ ํ๋ฒ ์ด๋ฒคํธ๋ฅผ ์คํ์์ผ์ผ ํ๋ค๋ฉด throttling ์ด ์ ํฉํฉ๋๋ค.
๋ํ์ ์ธ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก๋ ์ ๊ฐ ์ฌ์ฉํ Debounce ๋ฐฉ์์ด ์์ต๋๋ค๋ง, ์ฌ๊ธฐ์ ์ ํฉํ์ง ์์ต๋๋ค.
์ฑ๋ฅ ํ๊ณผ ๊ฒฐ๊ณผ ์ฉ ์ข์ง ์์์ ํ์ธํ ์ ์์์ต๋๋ค.
import React, { useEffect, useState } from "react";
import { Box } from "@foundation/index";
import { css } from "@emotion/react";
import { HTMLElementTypes } from "@/types/utils";
type CategoryMenuType = HTMLElementTypes<HTMLDivElement>;
const CategoryMenu: React.FC<CategoryMenuType> = ({ children, ...arg }) => {
const [topPosition, setTopPosition] = useState(200);
useEffect(() => {
let throttleTimeout = null; // ์ค๋กํ๋ง ํ์์์์ ๊ด๋ฆฌํ ๋ณ์
const handleScroll = () => {
if (throttleTimeout === null) {
throttleTimeout = setTimeout(() => {
throttleTimeout = null;
const newTopPosition = 200 + window.scrollY;
setTopPosition(newTopPosition);
}, 100); // 100ms ๊ฐ๊ฒฉ์ผ๋ก ์ค๋กํ๋ง
}
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
if (throttleTimeout) {
clearTimeout(throttleTimeout);
}
};
}, []);
return (
<Box
css={css`
position: absolute;
top: ${topPosition}px;
left: -120px;
transition: top 0.3s ease; // ๋ถ๋๋ฌ์ด ์ ํ ํจ๊ณผ
display: flex;
flex-direction: column;
justify-content: center;
align-items: start;
row-gap: 0.75rem;
padding: 1.5rem;
border: 1px solid blue;
width: auto;
height: auto;
`}
{...arg}
>
{children}
</Box>
);
};
export default CategoryMenu;
์ฐ๋ก์ธ๋ง์ ์ ์ฉํ์ง๋ง ์ฌ์ ํ ๋ฒ๋ฒ ์ด๋๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
์น ๊ฐ๋ฐ์์ ์ฑ๋ฅ ์ต์ ํ๋ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๋ ์ค์ํ ์์์ ๋๋ค. ํนํ, ์คํฌ๋กค, ๋ฆฌ์ฌ์ด์ฆ, ๋ง์ฐ์ค ์ด๋ ๊ฐ์ ๋น๋ฒํ ์ด๋ฒคํธ๋ค์ ์ฑ๋ฅ ๋ฌธ์ ์ ์ฃผ๋ฒ์ด ๋ ์ ์์ต๋๋ค. ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด '์ฐ๋กํ๋ง'์ด๋ผ๋ ๊ธฐ๋ฒ์ ์ฌ์ฉํ๊ฒ ๋์์ต๋๋ค. ์ฐ๋กํ๋ง์ ํตํด ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ํธ์ถ ๋น๋๋ฅผ ์ ํํจ์ผ๋ก์จ ์ฑ๋ฅ์ ๊ฐ์ ํ ์ ์์ต๋๋ค. ํ์ง๋ง ์ฐ๋กํ๋ง์ ๋์ ํ ํ ์๋น์ค์ ์ฑ๋ฅ์ ๊ฐ์ ๋์์ง๋ง ์ฌ์ ํ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค. ์ฌ์ ํ ํ๋ฉด์ ๋ฒ๋ฒ ๊ฑฐ๋ฆฐ๋ค๋ ์ ... ์ด๋ ์ฐ๋กํ๋ง์ผ๋ก ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋น๋๋ฅผ ์ ํํ๋๋ผ๋, ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ์คํ๋๋ ์์ ์ด ๋ธ๋ผ์ฐ์ ์ Repaint ์ฃผ๊ธฐ์ ๋ง์ง ์์ ์ ์์ต๋๋ค.
์น ๋ธ๋ผ์ฐ์ ๋ ํ๋ฉด์ ์ฃผ๊ธฐ์ ์ผ๋ก ๊ฐฑ์ ํ๊ฑฐ๋ "๋ฆฌํ์ธํธ(repaint)"ํ๋๋ฐ, ์ด ๊ณผ์ ์ ์ผ๋ฐ์ ์ผ๋ก ์ด๋น 60๋ฒ ์ ๋ ๋ฐ์ํฉ๋๋ค (์ฆ, ๋๋ต ๋งค 16.7 ๋ฐ๋ฆฌ์ด๋ง๋ค). ์ด ๊ฐฑ์ ์ฃผ๊ธฐ๋ ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ฉด์ ๋ด์ฉ์ ๊ทธ๋ฆฌ๋ ์๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ์คํ๋๋ ์์ ์ด ๋ธ๋ผ์ฐ์ ์ ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ์ ์ผ์นํ์ง ์๋๋ค๋ ๊ฒ์, ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ฉด์ ๊ฐฑ์ ํ๋ ํ์ด๋ฐ๊ณผ ๋ง๋ฌผ๋ ค ์คํ๋์ง ์๋๋ค๋ ๋ป์ ๋๋ค. ์ฆ, ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ์คํ์ด ๋ธ๋ผ์ฐ์ ์ ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ์ ๋๊ธฐํ๋์ง ์์ผ๋ฉด, ํ๋ฉด์ ํ์๋๋ ๋ด์ฉ์ด ๋ถ๊ท์นํ๊ฒ ๊ฐฑ์ ๋ ์ ์์ผ๋ฉฐ, ์ด๋ ์ฌ์ฉ์์๊ฒ ๋ฒ๋ฒ ์ด๋ ๋ฏํ ๋๋์ ์ค ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์คํฌ๋กค ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ๋งค์ฐ ๋น ๋ฅด๊ฒ ์ฌ๋ฌ ๋ฒ ์คํ๋๋ฉด (ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ๋ณด๋ค ๋ ์์ฃผ), ๋ธ๋ผ์ฐ์ ๋ ์ด ๋ชจ๋ ๋ณ๊ฒฝ์ฌํญ์ ํ๋ฉด์ ๋ฐ์ํ๊ธฐ ์ํด ๊ณผ๋ํ ๋ฆฌํ์ธํธ๋ฅผ ์ํํด์ผ ํ ์ ์์ต๋๋ค. ์ด๋ก ์ธํด ์ฑ๋ฅ ์ ํ๋ ์ ๋๋ฉ์ด์ ์ ๋ฒ๋ฒ ์์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด requestAnimationFrame์ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค. requestAnimationFrame์ ๋ธ๋ผ์ฐ์ ์ ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ์ ๋ง์ถ์ด ํจ์๋ฅผ ํธ์ถํ๋ ๋ฐฉ๋ฒ์ผ๋ก, ํ๋ฉด์ด ์๋ก ๊ทธ๋ ค์ง ๋(Repaint)๋ง๋ค ํจ์๊ฐ ์คํ๋ฉ๋๋ค. ์ด๋ฅผ ํตํด ์คํฌ๋กค๊ณผ ๊ฐ์ ์ ๋๋ฉ์ด์ ํจ๊ณผ๋ฅผ ๋ ๋ถ๋๋ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์์์ต๋๋ค.
import React, { useEffect, useState } from "react";
import { Box } from "@foundation/index";
import { css } from "@emotion/react";
import { HTMLElementTypes } from "@/types/utils";
type CategoryMenuType = HTMLElementTypes<HTMLDivElement>;
const CategoryMenu: React.FC<CategoryMenuType> = ({ children, ...arg }) => {
const [translateY, setTranslateY] = useState(100); // ์ํ๋ฅผ translateY๋ก ๋ณ๊ฒฝ
useEffect(() => {
let lastKnownScrollPosition = 0;
let ticking = false;
const handleScroll = () => {
lastKnownScrollPosition = 100 + window.scrollY;
if (!ticking) {
window.requestAnimationFrame(() => {
setTranslateY(lastKnownScrollPosition); // translateY๋ฅผ ์คํฌ๋กค ์์น์ ๋ฐ๋ผ ์
๋ฐ์ดํธ
ticking = false;
});
ticking = true;
}
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<Box
css={css`
position: absolute;
transform: translateY(${translateY}px);
left: -120px;
transition: transform 0.3s linear; // ๋ถ๋๋ฌ์ด ์ ํ ํจ๊ณผ
display: flex;
flex-direction: column;
justify-content: center;
align-items: start;
row-gap: 0.75rem;
padding: 1.5rem;
border: 1px solid blue;
width: auto;
height: auto;
`}
{...arg}
>
{children}
</Box>
);
};
export default CategoryMenu;
๋ถ๋๋ฌ์ด ๋ฏํ ๋์์ ๋ณด์ฌ์ฃผ์ง๋ง..! CPU์ฑ๋ฅ์ ์ต์๋ก ๋ฎ์ท๋๋ ๋ค์๊ณผ ๊ฐ์ด CPU์ ๊ณผ๋ถํ๊ฐ ๊ฑธ๋ฆฌ๋๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
CSS ์์ฑ top๊ณผ transform์ ์๊ฐ์ ์ธ ํจ๊ณผ ๋ฉด์์ ์ ์ฌํ ๊ฒฐ๊ณผ๋ฅผ ์ ๊ณตํ ์ ์์ง๋ง, ๊ทธ๋ค์ด ์น ๋ธ๋ผ์ฐ์ ์ ์ํด ์ฒ๋ฆฌ๋๊ณ ๋ ๋๋ง๋๋ ๊ณผ์ ์์๋ ์ค์ํ ์ฐจ์ด์ ์ด ์์ต๋๋ค. ์ด ์ฐจ์ด์ ์ ์ฃผ๋ก ๋ ๋๋ง ์ฑ๋ฅ๊ณผ ๊ด๋ จ์ด ์์ต๋๋ค.
top ์์ฑ์ ์ฃผ๋ก ์ ๋ ์์น๋ ์๋ ์์น๋ก ์ง์ ๋ ์์์ ์์น๋ฅผ ์ ์ดํฉ๋๋ค.
top์ ๋ณ๊ฒฝํ ๋, ๋ธ๋ผ์ฐ์ ๋ ๋ ์ด์์ ๊ณ์ฐ์ ๋ค์ ์ํํด์ผ ํฉ๋๋ค. ์ด๋ ํด๋น ์์๋ฟ๋ง ์๋๋ผ ๊ทธ ์ฃผ๋ณ์ ์์๋ค๋ ์ํฅ์ ๋ฐ์ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค. ๋ ์ด์์ ๊ณ์ฐ์ด๋ ์์์ ํฌ๊ธฐ์ ์์น๋ฅผ ๊ฒฐ์ ํ๋ ๊ณผ์ ์ ๋งํฉ๋๋ค.
top ์์ฑ์ ๋ณ๊ฒฝ์ "reflow"๋ฅผ ์ผ์ผํฌ ์ ์์ต๋๋ค. ์ฆ, ๋ฌธ์์ ์ผ๋ถ ๋๋ ์ ์ฒด ๋ ์ด์์์ ๋ค์ ๊ณ์ฐํด์ผ ํ๋ ์ํฉ์ด ๋ฐ์ํ ์ ์์ด, ์ด๋ ์ฑ๋ฅ ์ ํ๋ฅผ ์ผ์ผํฌ ์ ์์ต๋๋ค. ํนํ ๋ง์ ์์๋ค์ด ํ๋ฉด์ ์๋ ๊ฒฝ์ฐ, ์ด๋ฌํ ๋ฆฌํ๋ก์ฐ๋ ๋น์ฉ์ด ๋ง์ด ๋ค ์ ์์ต๋๋ค.
์ ๊ฐ ๊ณผ๊ฑฐ์ ์์ฑํ ๋ธ๋ผ์ฐ์ ๊ฐ ๊ทธ๋ฆฌ๋ ๋ฒ ์์ reflow ๊ณผ์ ์ ๋ ๋๋ง ๊ณผ์ ์ค 3๋ฒ์งธ์ธ layout์ ์ํด ์์ต๋๋ค. ์ฆ cpu์ ๋ถํ๊ฐ ํจ์ฌ ๋ง์ด ๊ฑธ๋ฆฌ๊ฒ ๋ฉ๋๋ค.
transform ์์ฑ์ ์์์ ๋ณํ์ ์ ์ํฉ๋๋ค. ์ฌ๊ธฐ์๋ ์ด๋(translate), ํ์ (rotate), ํฌ๊ธฐ ์กฐ์ (scale), ๊ธฐ์ธ์(skew) ๋ฑ ๋ค์ํ ๋ณํ ์์ ์ด ํฌํจ๋ ์ ์์ต๋๋ค.
transform์ ๋ ์ด์์ ๊ณ์ฐ ๊ณผ์ ์ ์ํฅ์ ๋ฏธ์น์ง ์์ต๋๋ค. ๋์ , ์ด๋ "compositing" ๋จ๊ณ์์ ์ฒ๋ฆฌ๋ฉ๋๋ค. ์ปดํฌ์งํ ์ ์์์ ์์น๋ฅผ ๋ณ๊ฒฝํ๊ฑฐ๋ ๋ณํํ๋ ๊ณผ์ ์ด์ง๋ง, ๊ธฐ์กด ๋ ์ด์์์ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.
compositing ๋จ๊ณ๋ ๋ ๋๋ง ๊ณผ์ ์ ๋ง์ง๋ง ๊ณผ์ ์ ๋๋ค. ์ฆ ๋ ๋๋ง ๊ณผ์ ์์ CPU์ ๋ถํ๊ฐ ๊ฑฐ์ ์์ฉํ์ง ์์ต๋๋ค.
transform์ ์ฌ์ฉ์ ๋ฆฌํ๋ก์ฐ๋ฅผ ์ผ์ผํค์ง ์์ต๋๋ค. ๋ฐ๋ผ์ transform์ ์ฌ์ฉํ๋ ๊ฒ์ด top๊ณผ ๊ฐ์ ๋ ์ด์์ ์์ฑ์ ๋ณ๊ฒฝํ๋ ๊ฒ๋ณด๋ค ์ฑ๋ฅ์ ์ด์ ์ด ์์ต๋๋ค. ํนํ ์ ๋๋ฉ์ด์ ๊ณผ ์ ํ ํจ๊ณผ์ ์์ด์ transform์ ๋ ๋ถ๋๋ฝ๊ณ ํจ์จ์ ์ธ ๋ ๋๋ง์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค.
๋ธ๋ผ์ฐ์ ๋ ๋๋ง์ ์์ธํ ์ฌํญ์ ๋ํด์๋ ํด๋น ๊ธ ์ ์ฐธ๊ณ ํ์ ๋ ์ข์ต๋๋ค!
import { useEffect, useRef, useState } from "react";
const useThrottleScroll = (delay: number, top: number): number => {
const [scrollPosition, setScrollPosition] = useState(top);
const throttleTimeout = (useRef < NodeJS.Timeout) | (null > null);
const requestRef = (useRef < number) | (null > null);
useEffect(() => {
const handleScroll = () => {
if (!throttleTimeout.current) {
throttleTimeout.current = setTimeout(() => {
requestRef.current = requestAnimationFrame(() => {
setScrollPosition(top + window.scrollY);
});
throttleTimeout.current = null;
}, delay);
}
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
if (throttleTimeout.current) {
clearTimeout(throttleTimeout.current);
}
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
}
};
}, [delay, top]);
return scrollPosition; // ํด๋น ํ
์ scrollPosition์ ๋ฐํํฉ๋๋ค.
};
export default useThrottleScroll;
์ฃผ์ด์ง useThrottleScroll ํจ์๋ React์ ์ปค์คํ ํ ์ ๋๋ค. ์ด ํ ์ ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ฉด์, ์ง์ ๋ ์ง์ฐ์๊ฐ(delay) ๋์ ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ "throttle" (์ฆ, ์ ํ)ํ๋ ์ญํ ์ ํฉ๋๋ค. ์ด ์ฝ๋์ ์ฃผ์ ๋ชฉ์ ์ ์ฑ๋ฅ ์ต์ ํ๋ก, ์คํฌ๋กค ์ด๋ฒคํธ๊ฐ ๋งค์ฐ ๋น๋ฒํ๊ฒ ๋ฐ์ํ๋ ๊ฒ์ ๋ฐฉ์งํ๊ณ , ๋์ ์ง์ ๋ ์ง์ฐ ์๊ฐ์ด ์ง๋ ํ์๋ง ์คํฌ๋กค ์์น๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค.
useState: scrollPosition ์ํ๋ฅผ ์ฌ์ฉํ์ฌ ํ์ฌ ์คํฌ๋กค ์์น๋ฅผ ์ถ์ ํฉ๋๋ค. ์ด๊ธฐ๊ฐ์ top ๋งค๊ฐ๋ณ์๋ก ์ค์ ๋ฉ๋๋ค.
useRef: throttleTimeout๊ณผ requestRef ๋ ๊ฐ์ ref๋ฅผ ์ฌ์ฉํฉ๋๋ค.
throttleTimeout๋ ์ง์ฐ ์๊ฐ์ ๊ด๋ฆฌํ๊ธฐ ์ํ setTimeout์ ๋ฐํ๊ฐ์ ์ ์ฅํฉ๋๋ค.
requestRef๋ requestAnimationFrame ํจ์ ํธ์ถ์ ์ํ ID๋ฅผ ์ ์ฅํฉ๋๋ค.
useEffect ๋ด๋ถ์ handleScroll ํจ์: ์คํฌ๋กค ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋๋ง๋ค ํธ์ถ๋ฉ๋๋ค. ์ด ํจ์ ๋ด์์ throttleTimeout.current๊ฐ null์ผ ๊ฒฝ์ฐ์๋ง setTimeout๋ฅผ ์ค์ ํฉ๋๋ค. setTimeout์ ์ง์ ๋ ์ง์ฐ์๊ฐ(delay) ํ์ ์คํฌ๋กค ์์น๋ฅผ ์ ๋ฐ์ดํธํ๋ requestAnimationFrame์ ์์ฝํฉ๋๋ค.
window.addEventListener: ์๋์ฐ์ ์คํฌ๋กค ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐํฉ๋๋ค.
return ๊ตฌ๋ฌธ์์๋ ์ปดํฌ๋ํธ๊ฐ ์ธ๋ง์ดํธ๋ ๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ ๊ฑฐํ๊ณ , setTimeout๊ณผ requestAnimationFrame์ ์ทจ์ํฉ๋๋ค.
์คํฌ๋กค ์์น ์ ๋ฐ์ดํธ: setTimeout ๋ด๋ถ์ requestAnimationFrame์ ์ฌ์ฉํ์ฌ setScrollPosition์ ํธ์ถํจ์ผ๋ก์จ ์คํฌ๋กค ์์น๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค. requestAnimationFrame์ ๋ธ๋ผ์ฐ์ ๊ฐ ๋ค์ repaint๋ฅผ ํ ๋ ํจ์๋ฅผ ํธ์ถํ๋๋ก ์์ฝํ๋๋ฐ, ์ด๋ ์คํฌ๋กค ์ด๋ฒคํธ ์ฒ๋ฆฌ์ ์ฑ๋ฅ์ ํฌ๊ฒ ๊ฐ์ ํฉ๋๋ค
scrollPosition์ ๊ณ์ฐ๋ ํ์ฌ ์คํฌ๋กค ์ ๋๋ค
ํด๋น hook ์ ๋ค์๊ณผ ๊ฐ์ด ์์ฃผ ์ฝ๊ฒ ์ฌ์ฉ๋จ์ ํ์ธํ ์ ์์ต๋๋ค.
์ธ๋ป ๋ณด๊ธฐ์๋ ๊ธฐ์กด์ ํ๊ฐ๋ ๊ฒฐ๊ณผ๋ค๋ณด๋ค ์ฑ๋ฅ์ด ๋ฐ์ด๋๊ฒ์ผ๋ก ๋ณด์ ๋๋ค. ํ์ง๋ง ๋์ฑ ์ ํํ ์์น๊ฐ ํ์ํฉ๋๋ค.
๊ฐ๋ฐ์ ๋๊ตฌ์ performance tab์ CPU ์ฑ๋ฅ์ ์ต๋ํ ๋ฎ์ถฅ๋๋ค.
function infiniteScroll() {
const interval = setInterval(() => {
// ํ์ฌ ์คํฌ๋กค ์์น์์ ์๋๋ก ์กฐ๊ธ์ฉ ์ด๋
window.scrollBy(0, 2);
// ํ์ด์ง ๋์ ๋๋ฌํ๋์ง ํ์ธ
if (window.scrollY + window.innerHeight >= document.body.scrollHeight) {
// ํ์ด์ง ๋์ ๋๋ฌํ๋ฉด setInterval์ ๋ฉ์ถค
clearInterval(interval);
}
}, 300); // 300ms๋ง๋ค ์คํฌ๋กค ์คํ
}
// ํจ์ ์คํ
infiniteScroll();
๋ชจ๋ ๋์ผํ ์คํฌ๋กค ํ๊ฒฝ์ ์ ์ฉ ํ๋๋ก ์ฝ๋๋ฅผ ํตํด์ ํ๊ฒฝ์ ํต์ ํฉ๋๋ค. ๋ชจ๋ ๋์ผํ ์๊ฐ์ธ 16์ด๊ฐ๋์ ๊ธฐ์ ์ผ๋ก ์งํํฉ๋๋ค.
๊ฐ์ฅ ๊ธฐ์ค์ด ๋๋ ์งํ
top ๋์ transform์ ์ฌ์ฉํด์ rendering ์๊ฐ๊ณผ painting ์๊ฐ์ด ๊ฐ์ํ์ต๋๋ค. Scripting ์๊ฐ : 8.2์ด -> 8.3์ด (๊ฐ์ ๋์ง ์์) Rendering ์๊ฐ : 3์ด -> 2.6์ด (์ฝ 13.33% ๊ฐ์ ) Painting ์๊ฐ : 1.2์ด -> 0.6์ด (์ฝ 50% ๊ฐ์ )
์ ์ฒด ๋ก์ง์ Throttling ๊ณผ transform ์ ์ฌ์ฉํ ๊ฒฐ๊ณผ ์ ์ฒด์ ์ผ๋ก ํฌ๊ฒ ์ฑ๋ฅ์ด ๊ฐ์ ๋์์ต๋๋ค. Scripting ์๊ฐ : 8.3์ด -> 1.5์ด (์ฝ 82.93% ๊ฐ์ ) Rendering ์๊ฐ : 3์ด -> 1.5์ด (์ฝ 50% ๊ฐ์ ) Painting ์๊ฐ : 1.2์ด -> 0.4์ด (์ฝ 66.66% ๊ฐ์ )
๊ณผ๊ฑฐ์ ๋ธ๋ผ์ฐ์ ๋ ๋๋ง ๊ณผ์ ์ ํ์ตํ๋ฉด์, ์ค์ ๋ก ๋ ๋๋ง ์ต์ ํ์ ๋ํด ๊ถ๊ธํ์๋๋ฐ ์ด๋ฒ ๊ธฐํ๋ก ์ฑ๋ฅ๋ ์ธก์ ํด๋ณด๋ฉฐ, ์ค์ ๋ก ์ฑ๋ฅ์ด ํฅ์๋๋๊ฒ์ ๋์ผ๋ก ํ์ธํ๋ ๊ณผ์ ์ ๊ฝค๋ ํฅ๋ฏธ๋ก์ ์ต๋๋ค.
๋๊ตฐ๊ฐ๊ฐ ๋ง๋ค๊ณ ํ๋๊ธธ์ ์กฐ๊ธ์ ์์ฌํ๋ ๋ฒ๋ฆ์ ํค์ฐ๋ ค๊ณ ํฉ๋๋ค. ์กฐ๊ธ์ฉ ์ ๋ง์ ๊ทผ๊ฑฐ๋ฅผ ๋ง๋ค์ด ๊ฐ๋๊ฒ ๊ฐ์์ ์๋ฏธ ์๋ ๊ณผ์ ์๋ค๊ณ ์๊ฐํฉ๋๋ค.
ํด๋น ์ฝ๋๋ฅผ ํ์ธํ๊ณ ์ถ์ผ์๋ค๋ฉด PR์์ ํ์ธํ์ค ์ ์์ต๋๋ค