์ต๊ทผ ์ ์ง์ฅ์์ ํจ๊ปํ๋ ๋ฉ์ง ๋๋ฃ๋ถ์ด ๋ฐํ์๋ก ์ฐธ์ฌํ ์คํ์์ค๋ฅผ ์ฃผ์ ๋ก ํ ๋ฐ์ ์ ๋ค๋ ์๋ค. ๋ฐํ์์ ์๊ฐ๋ ์คํ์์ค๋ค์ ์ดํด๋ณด๋ค๊ฐ, React Bits ์์ ์ต๊ทผ ๋ด๊ฐ ๋์์ธ ์์คํ ์์ ๊ตฌํํ๋ ํ ์คํธ ์ ๋๋ฉ์ด์ ์ปดํฌ๋ํธ์ ๋์ผํ ๋์์ ํ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ฐ๊ฒฌํ๋ค. ์๊ฐ์ ์ผ๋ก ์์ฐ์ค๋ฝ๊ฒ ๋ณด์ด๊ธฐ ์ํด ๊ณ ๋ คํด์ผ ํ ์ ์ด ๋ง์๋ ์ปดํฌ๋ํธ์๋๋ฐ, ๋ค์๊ณผ ๊ฐ์ ๋ํ ์ผํ ๋ฌธ์ ๋ค์ ํด๊ฒฐํด์ผ ํ๋ค.
1. ๋จ์ด๊ฐ ๋ฐ๋ ๋ ๋ฌธ์ฅ ์ ์ฒด๊ฐ ํ๋ค๋ฆฌ๋ ๋ฌธ์
์๋ฅผ ๋ค์ด
- “์ค๋์ ํผ์๋ฅผ ๋จน์๊น์?”
- “์ค๋์ ์ค๋ธ์ค๋ธ๋ฅผ ๋จน์๊น์?”
๋ ๋ฌธ์ฅ์ ๋์ ์ผ๋ก ๊ต์ฒด๋๋ ๋จ์ด(๋ณผ๋๋ก ํ์๋ ๋ถ๋ถ)๋ง ๋ค๋ฅด์ง๋ง, ์ด ๋จ์ด์ ํญ์ด ๋ฌ๋ผ์ง๋ ์๊ฐ ๋ค์ชฝ ํ ์คํธ๊ฐ ์ข์ฐ๋ก ํ๋ค๋ ค ๋ณด์ธ๋ค.
์์ธ
ํ ์คํธ๊ฐ ๋ฐ๋๋ฉด ํด๋น span์ width๊ฐ ์ฆ์ ์ฌ๊ณ์ฐ๋๋ฉด์ ์ค๋ฅธ์ชฝ ํ ์คํธ๊ฐ ๋ฐ๋ฆฌ๊ฑฐ๋ ๋น๊ฒจ์ง๋ค.
ํด๊ฒฐ
๋ชจ๋ ๋จ์ด๋ฅผ ๋ ๋๋ง ์ ์ ์์ span์ผ๋ก ์ธก์ ํ์ฌ ๊ฐ์ฅ ๊ธด ๋จ์ด์ ํญ์ผ๋ก ์ปจํ ์ด๋ width๋ฅผ ๊ณ ์ ํด์ ํด๊ฒฐํ์๋ค.
- visibility: hidden, position: absolute๋ก ๊ณต๊ฐ ์ฐจ์ง ์์ด ์์ ์ธก์
- getBoundingClientRect()๋ก ์ค์ ํญ ๊ณ์ฐ
- ๊ฐ์ฅ ๊ธด ํญ์ผ๋ก ๊ณ ์ ํ๋ฉด ํ๋ค๋ฆผ์ด ์ฌ๋ผ์ง๋ค
2. ์ค๋ฅธ์ชฝ ์ ๋ ฌ์ผ ๋ ๋จ์ด๊ฐ ๋ค์ฅ๋ ์ฅํด ๋ณด์ด๋ ๋ฌธ์
๋ฌธ์ฅ์ด ์๋์ ๊ฐ์ ํํ์ผ ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋๋ฐ, ๋์ ๋จ์ด๊ฐ ๊ณ ์ ๋ฌธ์ ๊ธฐ์ค์ผ๋ก ์ผ์ชฝ์ ์๋ ๊ฒฝ์ฐ์ ์งง์ผ๋ฉด ์ผ์ชฝ์ ๋ถ๊ณ , ๊ธธ๋ฉด ์ค์์ผ๋ก ์ค๊ธฐ ๋๋ฌธ์ ์ค๋ฅธ์ชฝ ํ ์คํธ์์ ๊ฐ๊ฒฉ์ด ๊ณ์ ์ผ์ ํ์ง ์๊ฒ ๋ณด์๋ค.
[๋์ ๋จ์ด] [์ค๋ฅธ์ชฝ prefix]
ํด๊ฒฐ
๋์ ๋จ์ด๊ฐ ํญ์ ๋์ผํ ๊ธฐ์ค์ ์์ ๋ ๋๋ง ๋๋๋ก ์ต์ฅ ๋จ์ด ๊ธธ์ด๋ฅผ ๊ธฐ์ค์ผ๋ก, ๋ถ์กฑํ ๊ธธ์ด๋งํผ ์์ชฝ์ ํฌ๋ช ํ ํจ๋ฉ์ ์กฐ๊ฑด๋ถ๋ก ์ฝ์ ํ๋ ๋ฐฉ์์ ์ฌ์ฉํ๋ค.
3. ์ด๊ธฐ ๋ ๋๋ง ์ ๋ ์ด์์์ด ๊นจ์ ธ ๋ณด์ด๋ ๋ฌธ์
์ฒ์ ๋ก๋๋๋ ์๊ฐ, width ๊ณ์ฐ์ด ๋๊ธฐ ์ ์ ์ํ๊ฐ ์ ๊น ๋ณด์ด๋ฉด์ ๋ฌธ์ฅ์ด ์ด๊ธ๋ ๋ณด์๋ค.
์์ธ
useEffect๋ ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ฉด์ ๊ทธ๋ฆฐ ๋ค ์คํ๋๋ฏ๋ก, “๊นจ์ง ํ๋ฉด → ๊ณ์ฐ → ๊ณ ์ ” ์์๊ฐ ๋ฐ์ํ๋ค.
ํด๊ฒฐ
width ๊ณ์ฐ๊ณผ ์ด๊ธฐ ์คํ์ผ ์ ์ฉ์ ๋ชจ๋ useLayoutEffect๋ก ๋ณ๊ฒฝํ๋ค. useLayoutEffect๋ DOM ์ ๋ฐ์ดํธ ํ, ํ๋ฉด์ ๊ทธ๋ฆฌ๊ธฐ ์ ์ ๋๊ธฐ์ ์ผ๋ก ์คํ๋๋ฏ๋ก ๊นจ์ง๋ ์ด์๊ฐ ํด๊ฒฐ๋์๋ค.
โจ React Bits์ Rotating Text
RotatingTextDemo ์ฝ๋ ์ดํด๋ณด๊ธฐ
RotatingText ์ฝ๋ ์ดํด๋ณด๊ธฐ
<LayoutGroup>
<motion.p className="rotating-text-ptag" layout>
<motion.span
className="pt-0.5 sm:pt-1 md:pt-2"
layout
transition={{ type: 'spring', damping: 30, stiffness: 400 }}
>
Creative{' '}
</motion.span>
<RotatingText
texts={words}
mainClassName="rotating-text-main"
staggerFrom={'last'}
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '-120%' }}
staggerDuration={0.025}
splitLevelClassName="rotating-text-split"
transition={{ type: 'spring', damping: 30, stiffness: 400 }}
rotationInterval={2000}
/>
</motion.p>
</LayoutGroup>;
React Bits์ Rotating Text ์ฝ๋๋ฅผ ์ค์ ๋ก ์ดํด๋ณด๋, ๊ตฌ์กฐ์ ์ผ๋ก ๋์ ๋๋ ๋ถ๋ถ๋ค์ด ๋ช ๊ฐ์ง ์์๋ค.
1. ์ปดํฌ๋ํธ ์ ์ฒด๊ฐ Framer Motion์ LayoutGroup์ผ๋ก ๊ฐ์ธ์ ธ ์์๋ค. ๊ณ ์ ํ ์คํธ์ ๋์ ํ ์คํธ๋ฅผ ํ๋์ ๋ ์ด์์ ๊ทธ๋ฃน ์์์ ๊ด๋ฆฌํด, ์ปดํฌ๋ํธ ๋ด๋ถ์์ ๋ฐ์ํ๋ ๋ ์ด์์ ๋ณํ๋ฅผ Motion์ด ์ผ๊ด๋๊ฒ ๊ณ์ฐํ ์ ์๋๋ก ์ค๊ณ๋์ด ์์๋ค.
2. ์ ์ ํ ์คํธ ์ญ์ motion.* ์ปดํฌ๋ํธ๋ก ๊ฐ์ธ๊ณ layout ์์ฑ์ ์ ์ฉํ๊ณ ์์๋ค. <motion.playout>์ด๋ <motion.span layout>์ฒ๋ผ ๊ณ ์ ๋ ์์๊น์ง ๋ ์ด์์ ์ ๋๋ฉ์ด์ ๋์์ ํฌํจ์ํค๋ ๋ฐฉ์์ด์๋ค.
3. RotatingText ์์ฒด๋ Motion ๊ธฐ๋ฐ์ผ๋ก ๊ตฌํ๋์ด ์์๋ค. ๋จ์ด์ ๋ฑ์ฅ/ํด์ฅ ์ ๋๋ฉ์ด์
์ initial, animate, exit๋ก ์ฒ๋ฆฌํ๊ณ ,
๋ ์ด์์ ๋ณํ๋ layout๊ณผ ์์ ์์ฐ์ค๋ฝ๊ฒ ์ฒ๋ฆฌํ๋ ๊ตฌ์กฐ์๋ค.
์ ๋ฆฌํ๋ฉด, ๋ด๊ฐ ์ง์ ํญ ์ธก์ ์ด๋ baseline ์ ๋ ฌ, ์ด๊ธฐ ๊น๋นก์ ๋ฐฉ์ง ๋ฑ์ ์๋์ผ๋ก ์ฒ๋ฆฌํ๋ ๋ถ๋ถ๋ค์ React Bits๋ Framer Motion์ ๋ ์ด์์ ์์คํ ์ ์์ํ๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐํ๊ณ ์์๋ค.
โจ Framer Motion - FLIP ๊ธฐ๋ฒ
Framer Motion ๋ด๋ถ ๋์์ ๋ ์ดํด๋ณด๋ฉด, ๋ ์ด์์ ์ ํ์ FLIP(First, Last, Invert, Play) ๊ธฐ๋ฒ์ผ๋ก ์ฒ๋ฆฌ๋๊ณ ์๋ค.
๋๋ ์ด๋ฒ์ ์ฒ์ ์๊ฒ ๋ ๊ธฐ๋ฒ์ด๋ผ ๊ฐ๋จํ ์ ๋ฆฌํด๋ณด๋ฉด, ๋ ์ด์์์ด ๋ฐ๋๋ ์ํฉ์์ ํ๋ ๋๋ ์์ด ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์
์ ๋ง๋๋ ๋ฐ ์ต์ ํ๋ ๋ฐฉ์์ด๋ผ๊ณ ํ๋ค. ํต์ฌ ์์ด๋์ด๋ ๋ค์๊ณผ ๊ฐ๋ค.
- ์ค์ ๋ ์ด์์์ ๋ฐ๋ก ๋ฐ์ํ๋ค.
- ์ดํ, transform์ผ๋ก ์ด์ ์์น์ ์์๋ ๊ฒ์ฒ๋ผ ๋ณด์ด๋๋ก ๋ณด์ ํ๋ค.
- ๋ง์ง๋ง์ผ๋ก ๊ทธ transform ๊ฐ์ 0๊น์ง ์ ๋๋ฉ์ด์ ํ๋ฉฐ ์์ฐ์ค๋ฝ๊ฒ ์ ๋ ์ด์์์ ์์ฐฉ์ํจ๋ค.
1. F — First
์์๊ฐ ๋ณ๊ฒฝ๋๊ธฐ ์ ์ ์์น์ ํฌ๊ธฐ๋ฅผ ์ธก์ ํ๋ค. Motion ๋ด๋ถ์์๋ willUpdate() ์์ ์์ snapshot์ ์ ์ฅํ๋ค.
AnimatedText์์ ์๊ฐํ๋ค๋ฉด,
- “ํผ์” ์์น์ ํญ
- “ํ๋ฒ๊ฑฐ”๋ก ๋ณ๊ฒฝ๋๊ธฐ ์ง์ ์ํ
์ด ์์ ์ ๋ ์ด์์์ด First์ ํด๋นํ๋ค.
// packages/framer-motion/src/projection/node/create-projection-node.ts
willUpdate(shouldNotifyListeners = true) {
// ... ์๋ต ...
if (this.instance) {
// ํ์ฌ ์์น์ ํฌ๊ธฐ๋ฅผ ์ธก์ ํ์ฌ snapshot์ ์ ์ฅ
this.snapshot = this.measure()
}
this.updateSnapshot()
shouldNotifyListeners && this.notifyListeners("willUpdate")
}
updateSnapshot() {
if (this.snapshot || !this.instance) return
// ํ์ฌ ๋ ์ด์์ ์ค๋
์ท ์ ์ฅ
this.snapshot = this.measure()
}
2. L — Last
React ๋ ๋๋ง์ด ๋๋ ๋ค, ๋ณ๊ฒฝ๋ ์ดํ์ ์์น์ ํฌ๊ธฐ๋ฅผ ๋ค์ ์ธก์ ํ๋๋ฐ, Motion์์๋ updateLayout() ๋จ๊ณ์์ ์ธก์ ํ๋ค.
- “ํ๋ฒ๊ฑฐ”๊ฐ ๋ ๋๋ง ๋ ์ดํ์ ํญ
- ์ฃผ๋ณ ํ ์คํธ๊ฐ ๋ฐ๋ ค๋ ๊ฒฐ๊ณผ ๋ ์ด์์
// packages/framer-motion/src/projection/node/create-projection-node.ts
updateLayout() {
if (!this.instance) return
// ... ์๋ต ...
const prevLayout = this.layout
// ์๋ก์ด ๋ ์ด์์ ์ธก์ (React ๋ฆฌ๋ ๋๋ง ํ)
this.layout = this.measure(false)
this.layoutCorrected = createBox()
this.isLayoutDirty = false
this.notifyListeners("measure", this.layout.layoutBox)
}
3. I — Invert
First์ Last ์ฌ์ด์ ์ฐจ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก delta๋ฅผ ๊ณ์ฐํ๋๋ฐ, ์ด delta๋ฅผ scale/translate transform์ผ๋ก ์ญ์ ์ฉํด, ์์๊ฐ "์๋ ์์น์ ์๋ ๊ฒ์ฒ๋ผ" ๋ณด์ด๊ฒ ๋ง๋ ๋ค. ์ฆ ์ค์ DOM์ ์ด๋ฏธ ๋ณํ๋ ์ํ์ง๋ง, transform์ด ๊ทธ ์ฐจ์ด๋ฅผ ๋ณด์ ํด ์ค๋ค. ์ด ๋จ๊ณ์์ ๋ ์ด์์ ์ ํ๊ฐ ์ฌ๋ผ์ง๋ค. ๋ด๊ฐ ๊ตฌํํ๋ AnimatedText์์๋ ์ง์ width๋ฅผ ๊ณ์ฐํด ๊ณ ์ ํด ์คฌ์ง๋ง, FLIP ๋ฐฉ์์ ์ด๋ฌํ ๊ณ์ฐ ์์ฒด๋ฅผ ๋ถํ์ํ๊ฒ ๋ง๋ ๋ค.
// packages/framer-motion/src/projection/geometry/delta-calc.ts
export function calcAxisDelta(
delta: AxisDelta,
source: Axis, // First (์ด์ ์ํ)
target: Axis, // Last (์๋ก์ด ์ํ)
origin: number = 0.5
) {
delta.origin = origin
delta.originPoint = mixNumber(source.min, source.max, delta.origin)
// ์ด์ ํฌ๊ธฐ๋ฅผ ์ ํฌ๊ธฐ๋ก ๋ง๋ค๊ธฐ ์ํ scale ๊ณ์ฐ
delta.scale = calcLength(target) / calcLength(source)
// ์์น ์ด๋ ๊ณ์ฐ
delta.translate =
mixNumber(target.min, target.max, delta.origin) - delta.originPoint
// ๋ฏธ์ธํ ์ฐจ์ด๋ ๋ฌด์ํ๋ค!
if ((delta.scale >= 0.9999 && delta.scale <= 1.0001) || isNaN(delta.scale)) {
delta.scale = 1.0
}
}
export function calcBoxDelta(
delta: Delta,
source: Box, // snapshot (First)
target: Box, // layout (Last)
origin?: ResolvedValues
): void {
calcAxisDelta(delta.x, source.x, target.x, origin?.originX)
calcAxisDelta(delta.y, source.y, target.y, origin?.originY)
}
4. P — Play
๋ง์ง๋ง์ผ๋ก delta์ scale/translate 0๊น์ง ์ ๋๋ฉ์ด์
ํ๋ฉฐ ์์ฐ์ค๋ฝ๊ฒ ์๋ก์ด ๋ ์ด์์์ผ๋ก ์ด๋ํ๋ค.
์๋๋ ๊ฐ๋จํ ์์๋ค. scale์ด ๋ณํํ๋ฉด์ ์ด๋ป๊ฒ ํญ์ด ์์ฐ์ค๋ฝ๊ฒ ๋ณด์ ๋๋์ง ํ์ธํ ์ ์๋ค.
// progress = 0.0 (์์)
scaleX = 0.5 * (1 - 0.0) + 1.0 * 0.0 = 0.5 // 40px์ฒ๋ผ ๋ณด์
// progress = 0.5 (์ค๊ฐ)
scaleX = 0.5 * (1 - 0.5) + 1.0 * 0.5 = 0.75 // 60px
// progress = 1.0 (์๋ฃ)
scaleX = 0.5 * (1 - 1.0) + 1.0 * 1.0 = 1.0 // 80px (์๋ ํฌ๊ธฐ)
transform ๊ธฐ๋ฐ์ด๊ธฐ ๋๋ฌธ์
- GPU ๊ฐ์
- ๋ฆฌํ๋ก์ฐ ์๋ต
- 60fps ์ ์ง
- ์ฃผ๋ณ ๋ ์ด์์ ์ํฅ ์์
์ด ๋ชจ๋ ์กฐ๊ฑด์ ๋ง์กฑํ๋ฉฐ ์์ฐ์ค๋ฌ์ด ๋ ์ด์์ ์ ๋๋ฉ์ด์ ์ด ๊ฐ๋ฅํด์ก๋ ๊ฒ์ด์๋ค.
//๋งค ํ๋ ์ ๋ฆฌํ๋ก์ฐ
element.style.width = '40px'; // Reflow
animate(() => {
element.style.width = `${currentWidth}px`; // ๋งค ํ๋ ์ Reflow
});
// FLIP ๋ฐฉ์ (๋จ 1ํ ๋ฆฌํ๋ก์ฐ)
element.style.width = '80px'; // Reflow 1ํ
element.style.transform = 'scaleX(0.5)'; // Composite only
animate(() => {
element.style.transform = `scaleX(${scale})`; // Composite only
});
๋ค๋ง, FLIP ๊ธฐ๋ฒ๋ง์ผ๋ก๋ Rotating Text๋ฅผ ์์ ํ ๊ตฌํํ๊ธฐ ์ด๋ ต๋ค. FLIP์ ๊ธฐ๋ณธ์ ์ผ๋ก “๊ฐ๋ณ ์์๊ฐ ์์ ์ ๋ ์ด์์ ๋ณํ์ ๋ง์ถฐ ๋ถ๋๋ฝ๊ฒ ์ ํ๋๋๋ก” ๋ง๋๋ ๊ธฐ๋ฒ์ด๊ธฐ ๋๋ฌธ์, Rotating Text์ฒ๋ผ ์ฌ๋ฌ ์์๊ฐ ๋์์ ์์น๊ฐ ๋ณํ๊ณ , ๋์ ๋จ์ด์ ๊ธธ์ด ๋ณํ๊ฐ ์์ชฝ์ ์ ์ ํ ์คํธ๊น์ง ํจ๊ป ์ด๋์ํค๋ ๊ตฌ์กฐ์์๋ ์์ ํ๋๋ง ์ ์์ง์ธ๋ค๊ณ ํด๊ฒฐ๋์ง ์๋๋ค. ์ด ๊ฒฝ์ฐ์๋ ์ฌ๋ฌ ์์์ ๋ ์ด์์ ๋ณํ๋ฅผ ํ๋์ ๋จ์๋ก ๋ฌถ์ด์ฃผ๋ ์์ ๊ฐ๋ ์ด ํ์ํ๋ฐ, React Bits์ RotatingText ๋ด๋ถ ๊ตฌํ์ ๋ณด๋ฉด, ์ด๋ฅผ ์ํด Framer Motion์ LayoutGroup์ผ๋ก ์ ์ฒด ๋ฌธ์ฅ์ ๊ฐ์ธ๊ณ ์๋ค.
โจ Framer Motion - LayoutGroup
LayoutGroup์ ์ญํ ์ ํ ๋ฌธ์ฅ์ผ๋ก ์ ๋ฆฌํ๋ฉด, ๊ฐ์ ๊ทธ๋ฃน ์์ ๋ชจ๋ motion ์์๊ฐ ๋์ผํ ์๊ฐ์ FLIP ์ฌ์ดํด์ ์์ํ๋๋ก ์กฐ์จํ๋ ๊ฒ์ด๋ค. ๋๋ถ์ Rotating Text์ฒ๋ผ ์ฌ๋ฌ ์์๊ฐ ํ ๋ฒ์ ์์ง์ด๋ UI์์๋ ๊ฐ๊ฐ์ด ์ ๊ฐ๊ฐ ์ ๋๋ฉ์ด์ ์ ์์ํ์ง ์๊ณ , ๋ฌธ์ฅ ์ ์ฒด๊ฐ ํ๋์ ๋จ์์ฒ๋ผ ์์ฐ์ค๋ฝ๊ฒ ์ ํ๋๋ค. ์ค์ ๋ด๋ถ ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด ์์ ๋ชจ๋ motion ์์๋ฅผ Set์ผ๋ก ๋ชจ์๋๊ณ ์ด๋ ํ ์์์์๋ willUpdate๊ฐ ๋ฐ์ํ๋ฉด, ๊ทธ๋ฃน ์ ์ฒด์ dirtyAll()์ ํธ์ถํด ํ์ ์์ ๋ชจ๋๊ฐ FLIP์ First ๋จ๊ณ๋ฅผ ๋์์ ์์ํ๋๋ก ๋ง๋ ๋ค.
// packages/framer-motion/src/projection/node/group.ts
export function nodeGroup(): NodeGroup {
const nodes = new Set<IProjectionNode>()
const dirtyAll = () => nodes.forEach(notify)
// ์ด๋ค ๋
ธ๋์์๋ willUpdate๊ฐ ๋ฐ์ํ๋ฉด
// ๊ฐ์ ๊ทธ๋ฃน์ ๋ชจ๋ ๋
ธ๋์๊ฒ dirtyAll์ ํธ์ถํ๋ค
return {
add: (node) => {
nodes.add(node)
node.addEventListener("willUpdate", dirtyAll)
},
remove: (node) => {
nodes.delete(node)
dirtyAll()
},
dirty: dirtyAll,
}
}
const notify = (node: IProjectionNode) =>
!node.isLayoutDirty && node.willUpdate(false)
๋๋์
์ด๋ฒ ์คํ์์ค ๋ถ์์ ํตํด, ์ฌ๋ฌ ์์๊ฐ ๋์์ ์์ง์ด๋ UI์์๋ ๊ฐ๋ณ span์ ์ ๋๋ฉ์ด์ ์ ์ ์ฉํ๊ธฐ๋ณด๋ค LayoutGroup์ฒ๋ผ ๊ทธ๋ฃน ๋จ์๋ก ๋ ์ด์์์ ๋ฌถ์ด ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ด ํจ์ฌ ์์ฐ์ค๋ฝ๋ค๋ ์ ์ ๋ฐฐ์ ๋ค. ์ด๋ฒ์ ์ฒ์ ์๊ฒ ๋ FLIP ๊ธฐ๋ฒ๋ ํฅ๋ฏธ๋ก์ ๋ค. Q&A์์ ‘์คํ์์ค์์ ๋ฐฐ์ด ๊ฒ์ ์ด๋ป๊ฒ ํ์ฉํ๋๋’๋ ์ง๋ฌธ์ด ๋์๋๋ฐ, ๋ฐํ์๋ถ์ ๋น์ฅ ์ฐ์ด์ง ์์๋ ์ธ์ ๊ฐ๋ ‘์ด, ๊ทธ๊ฒ ์์์ง’ ํ๊ณ ๋ ์ฌ๋ผ ๋์์ด ๋๋ค๊ณ ๋ตํ๋ค. ๋ ์ญ์ ์ด๋ฒ ๋ฐฐ์์ด ๊ทธ๋ฐ ๊ฒฝํ์ผ๋ก ๋จ๊ธฐ๋ฅผ ๊ธฐ๋ํ๋ค.
'๐ WIL' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| TanStack Table๋ก ์ธ๋ก ๋ณํฉ ํ ์ด๋ธ ๋ง๋ค๊ธฐ (0) | 2025.12.07 |
|---|---|
| ๋ฐฐํฌ ํ๋ก์ธ์ค ์๊ฒ ๊ฐ์ ํด๋ณด๊ธฐ (0) | 2025.11.23 |
| ๋์์ธ ํ ํฐ ์ค๋ณต ์์ ๊ธฐ (0) | 2025.11.08 |
| ์ด๋ฒ ์ฃผ์ ์๊ฒ ๋ ๊ฒ๋ค (0) | 2025.11.02 |
| ์ด๋ฏธ์ง ํ๋ฆฌ๋ก๋ฉ์ ๊ตฌํํ๋ฉฐ ์๊ฒ ๋ ๊ฒ๋ค (4) | 2025.10.25 |