scroll event ์ตœ์ ํ™”๋กœ ์›นํŽ˜์ด์ง€ ์„ฑ๋Šฅ ๊ฐœ์„ ํ•˜๊ธฐ

Rscroll event ์ตœ์ ํ™”๋กœ ์›นํŽ˜์ด์ง€ ์„ฑ๋Šฅ ๊ฐœ์„ ํ•˜๊ธฐ

Profile Picture
adultlee
2023-12-01

์›๋ณธ

์ €ํฌํŒ€ 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๋ฅผ ์จ์„œ ๊ณ ์ •ํ• ๊นŒ?

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)๋งˆ๋‹ค ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์Šคํฌ๋กค๊ณผ ๊ฐ™์€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋ฅผ ๋” ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

requestAnimationFrame ์ ์šฉ

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์— ๊ณผ๋ถ€ํ™”๊ฐ€ ๊ฑธ๋ฆฌ๋Š”๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋ Œ๋”๋ง ๊ณผ์ •์—์„œ์˜ top๊ณผ transform

CSS ์†์„ฑ top๊ณผ transform์€ ์‹œ๊ฐ์ ์ธ ํšจ๊ณผ ๋ฉด์—์„œ ์œ ์‚ฌํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ทธ๋“ค์ด ์›น ๋ธŒ๋ผ์šฐ์ €์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋˜๊ณ  ๋ Œ๋”๋ง๋˜๋Š” ๊ณผ์ •์—์„œ๋Š” ์ค‘์š”ํ•œ ์ฐจ์ด์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ฐจ์ด์ ์€ ์ฃผ๋กœ ๋ Œ๋”๋ง ์„ฑ๋Šฅ๊ณผ ๊ด€๋ จ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

top

top ์†์„ฑ์€ ์ฃผ๋กœ ์ ˆ๋Œ€ ์œ„์น˜๋‚˜ ์ƒ๋Œ€ ์œ„์น˜๋กœ ์ง€์ •๋œ ์š”์†Œ์˜ ์œ„์น˜๋ฅผ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค.

๋ Œ๋”๋ง ๊ณผ์ •

top์„ ๋ณ€๊ฒฝํ•  ๋•Œ, ๋ธŒ๋ผ์šฐ์ €๋Š” ๋ ˆ์ด์•„์›ƒ ๊ณ„์‚ฐ์„ ๋‹ค์‹œ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํ•ด๋‹น ์š”์†Œ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ทธ ์ฃผ๋ณ€์˜ ์š”์†Œ๋“ค๋„ ์˜ํ–ฅ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋ ˆ์ด์•„์›ƒ ๊ณ„์‚ฐ์ด๋ž€ ์š”์†Œ์˜ ํฌ๊ธฐ์™€ ์œ„์น˜๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๊ณผ์ •์„ ๋งํ•ฉ๋‹ˆ๋‹ค.

์„ฑ๋Šฅ

top ์†์„ฑ์˜ ๋ณ€๊ฒฝ์€ "reflow"๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, ๋ฌธ์„œ์˜ ์ผ๋ถ€ ๋˜๋Š” ์ „์ฒด ๋ ˆ์ด์•„์›ƒ์„ ๋‹ค์‹œ ๊ณ„์‚ฐํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด, ์ด๋Š” ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ๋งŽ์€ ์š”์†Œ๋“ค์ด ํ™”๋ฉด์— ์žˆ๋Š” ๊ฒฝ์šฐ, ์ด๋Ÿฌํ•œ ๋ฆฌํ”Œ๋กœ์šฐ๋Š” ๋น„์šฉ์ด ๋งŽ์ด ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ œ๊ฐ€ ๊ณผ๊ฑฐ์— ์ž‘์„ฑํ•œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ทธ๋ฆฌ๋Š” ๋ฒ• ์—์„œ reflow ๊ณผ์ •์€ ๋ Œ๋”๋ง ๊ณผ์ •์ค‘ 3๋ฒˆ์งธ์ธ layout์— ์†ํ•ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰ cpu์˜ ๋ถ€ํ•˜๊ฐ€ ํ›จ์”ฌ ๋งŽ์ด ๊ฑธ๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

transform

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" (์ฆ‰, ์ œํ•œ)ํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ์˜ ์ฃผ์š” ๋ชฉ์ ์€ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋กœ, ์Šคํฌ๋กค ์ด๋ฒคํŠธ๊ฐ€ ๋งค์šฐ ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ณ , ๋Œ€์‹  ์ง€์ •๋œ ์ง€์—ฐ ์‹œ๊ฐ„์ด ์ง€๋‚œ ํ›„์—๋งŒ ์Šคํฌ๋กค ์œ„์น˜๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

  1. useState: scrollPosition ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ์Šคํฌ๋กค ์œ„์น˜๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐ๊ฐ’์€ top ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์„ค์ •๋ฉ๋‹ˆ๋‹ค.

  2. useRef: throttleTimeout๊ณผ requestRef ๋‘ ๊ฐœ์˜ ref๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  3. throttleTimeout๋Š” ์ง€์—ฐ ์‹œ๊ฐ„์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ setTimeout์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

  4. requestRef๋Š” requestAnimationFrame ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ์œ„ํ•œ ID๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

  5. useEffect ๋‚ด๋ถ€์˜ handleScroll ํ•จ์ˆ˜: ์Šคํฌ๋กค ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜ ๋‚ด์—์„œ throttleTimeout.current๊ฐ€ null์ผ ๊ฒฝ์šฐ์—๋งŒ setTimeout๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. setTimeout์€ ์ง€์ •๋œ ์ง€์—ฐ์‹œ๊ฐ„(delay) ํ›„์— ์Šคํฌ๋กค ์œ„์น˜๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” requestAnimationFrame์„ ์˜ˆ์•ฝํ•ฉ๋‹ˆ๋‹ค.

  6. window.addEventListener: ์œˆ๋„์šฐ์— ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

  7. return ๊ตฌ๋ฌธ์—์„œ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋  ๋•Œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , setTimeout๊ณผ requestAnimationFrame์„ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค.

  8. ์Šคํฌ๋กค ์œ„์น˜ ์—…๋ฐ์ดํŠธ: setTimeout ๋‚ด๋ถ€์— requestAnimationFrame์„ ์‚ฌ์šฉํ•˜์—ฌ setScrollPosition์„ ํ˜ธ์ถœํ•จ์œผ๋กœ์จ ์Šคํฌ๋กค ์œ„์น˜๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. requestAnimationFrame์€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋‹ค์Œ repaint๋ฅผ ํ•  ๋•Œ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ์˜ˆ์•ฝํ•˜๋Š”๋ฐ, ์ด๋Š” ์Šคํฌ๋กค ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ์˜ ์„ฑ๋Šฅ์„ ํฌ๊ฒŒ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค

  9. 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์ดˆ๊ฐ€๋Ÿ‰์„ ๊ธฐ์ ์œผ๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

requestAnimationFrame + top ๋ถ„์„ ๊ฒฐ๊ณผ

์ „์ฒด ์†Œ์š”์‹œ๊ฐ„ : 16์ดˆ

Scripting : 8.2์ดˆ

Rendering : 3.0์ดˆ

Painting : 1.2์ดˆ

๊ฐ€์žฅ ๊ธฐ์ค€์ด ๋˜๋Š” ์ง€ํ‘œ

requestAnimationFrame + transform ๋ถ„์„ ๊ฒฐ๊ณผ

์ „์ฒด ์†Œ์š”์‹œ๊ฐ„ : 16์ดˆ

Scripting : 8.3์ดˆ

Rendering : 2.6์ดˆ

Painting : 0.6์ดˆ

top ๋Œ€์‹  transform์„ ์‚ฌ์šฉํ•ด์„œ rendering ์‹œ๊ฐ„๊ณผ painting ์‹œ๊ฐ„์ด ๊ฐ์†Œํ–ˆ์Šต๋‹ˆ๋‹ค. Scripting ์‹œ๊ฐ„ : 8.2์ดˆ -> 8.3์ดˆ (๊ฐœ์„ ๋˜์ง€ ์•Š์Œ) Rendering ์‹œ๊ฐ„ : 3์ดˆ -> 2.6์ดˆ (์•ฝ 13.33% ๊ฐœ์„ ) Painting ์‹œ๊ฐ„ : 1.2์ดˆ -> 0.6์ดˆ (์•ฝ 50% ๊ฐœ์„ )

requestAnimationFrame + Throttling + transform ๋ถ„์„ ๊ฒฐ๊ณผ

์ „์ฒด ์†Œ์š”์‹œ๊ฐ„ : 16์ดˆ

Scripting : 1.5์ดˆ

Rendering : 1.7์ดˆ

Painting : 0.4์ดˆ

์ „์ฒด ๋กœ์ง์— Throttling ๊ณผ transform ์„ ์‚ฌ์šฉํ•œ ๊ฒฐ๊ณผ ์ „์ฒด์ ์œผ๋กœ ํฌ๊ฒŒ ์„ฑ๋Šฅ์ด ๊ฐœ์„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Scripting ์‹œ๊ฐ„ : 8.3์ดˆ -> 1.5์ดˆ (์•ฝ 82.93% ๊ฐœ์„ ) Rendering ์‹œ๊ฐ„ : 3์ดˆ -> 1.5์ดˆ (์•ฝ 50% ๊ฐœ์„ ) Painting ์‹œ๊ฐ„ : 1.2์ดˆ -> 0.4์ดˆ (์•ฝ 66.66% ๊ฐœ์„ )

๊ฒฐ๋ก 

๊ณผ๊ฑฐ์— ๋ธŒ๋ผ์šฐ์ € ๋ Œ๋”๋ง ๊ณผ์ •์„ ํ•™์Šตํ•˜๋ฉด์„œ, ์‹ค์ œ๋กœ ๋ Œ๋”๋ง ์ตœ์ ํ™”์— ๋Œ€ํ•ด ๊ถ๊ธˆํ–ˆ์—ˆ๋Š”๋ฐ ์ด๋ฒˆ ๊ธฐํšŒ๋กœ ์„ฑ๋Šฅ๋„ ์ธก์ •ํ•ด๋ณด๋ฉฐ, ์‹ค์ œ๋กœ ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋˜๋Š”๊ฒƒ์„ ๋ˆˆ์œผ๋กœ ํ™•์ธํ•˜๋Š” ๊ณผ์ •์€ ๊ฝค๋‚˜ ํฅ๋ฏธ๋กœ์› ์Šต๋‹ˆ๋‹ค.

๋ˆ„๊ตฐ๊ฐ€๊ฐ€ ๋งž๋‹ค๊ณ  ํ•˜๋Š”๊ธธ์— ์กฐ๊ธˆ์€ ์˜์‹ฌํ•˜๋Š” ๋ฒ„๋ฆ‡์„ ํ‚ค์šฐ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์กฐ๊ธˆ์”ฉ ์ €๋งŒ์˜ ๊ทผ๊ฑฐ๋ฅผ ๋งŒ๋“ค์–ด ๊ฐ€๋Š”๊ฒƒ ๊ฐ™์•„์„œ ์˜๋ฏธ ์žˆ๋˜ ๊ณผ์ œ์˜€๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•˜๊ณ  ์‹ถ์œผ์‹œ๋‹ค๋ฉด PR์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

ยฉ 2024 Adultlee. All rights reserved.Made with โค by ์ด์„ฑ์ธ