π React μ€ν¬λ‘€ μ λλ©μ΄μ ꡬννκΈ°
μ¬μ©μμ λκΈΈμ λκΈ° μν΄ λ€μν λ°©λ²λ€μ΄ μ¬μ© λλλ°,
κ·Έ μ€μμλ κ°μ₯ λμ λλ κ²μ΄ μ€ν¬λ‘€ μ λλ©μ΄μ
μ΄μμ΅λλ€.
μ€μ μ¬μ΄λ νλ‘μ νΈμμ ꡬνν λͺ¨μ΅μ λ€μ λ§ν¬μμ νμΈν μ μμ΅λλ€.
μ§μ ꡬνν΄λ³΄λ μκ°νλ λ§νΌ μ΄λ ΅μ§λ μμλ μ€ν¬λ‘€ μ λλ©μ΄μ
μ Reactλ‘ κ°λ¨νκ² κ΅¬νν΄λ³Έ ν
hookμΌλ‘ λΆλ¦¬ν΄ μ¬μ¬μ©μ΄ μ©μ΄νλλ‘ κ°μ ν μ½λλ₯Ό μκ°ν©λλ€.
π λΌμ΄λΈλ¬λ¦¬λ‘ μ½κ² ꡬννκΈ°
[React] 리μ‘νΈ μ€ν¬λ‘€ μ λλ©μ΄μ λΌμ΄λΈλ¬λ¦¬ κ°λ°
μ€ν¬λ‘€ μ λλ©μ΄μ κΈ°λ₯μ μ½κ³ λΉ λ₯΄κ² ꡬννκ³ μΆμΌμ λΆλ€μ μ κΈμ μ°Έκ³ ν΄μ£ΌμΈμ!
π IntersectionObserver
IntersectionObserverμ JavaScriptμ API μ€ νλλ‘,
μμκ° λ·°ν¬νΈ(νλ©΄)μ λνλκ±°λ μ¬λΌμ§ λλ₯Ό κ°μ§νλ μν μ ν©λλ€.
μ΄λ₯Ό ν΅ν΄ μ€ν¬λ‘€, λ μ΄μ§ λ‘λ©(lazy loading), 무ν μ€ν¬λ‘€(infinite scrolling) λ±
λ€μν μν©μμ μμλ€μ κ°μ§νκ³ νΈλ¦¬κ±°νλλ° μ μ©νκ² μ¬μ©ν μ μμ΅λλ€.
μλλ κ°λ¨νκ² κ΅¬νν΄λ³Έ μμ μ λλ€.
π styled-componentsλ‘ μ λλ©μ΄μ ꡬν
μ°μ IntersectionObserverλ₯Ό μ€λͺ
νκΈ° μμ μ€μ§μ μΌλ‘ μμμ μ λλ©μ΄μ
ν¨κ³Όλ₯Ό μ μ©νλ λΆλΆμ
styled-componentλ‘ κ΅¬νν μ½λ λ¨Όμ λ³΄κ² μ΅λλ€.
import styled, { keyframes } from "styled-components";
const frameInAnimation = keyframes`
0% {
opacity: 0;
transform: translateX(-100%);
}
100%{
opacity: 1;
transform: translateX(0%);
}
`;
export const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
&.frame-in {
animation: ${frameInAnimation} 2s forwards;
}
`;
styled-componentsμ keyframes
λ‘ ν¬λͺ
λμ μμΉκ° μ΄λνλ μ λλ©μ΄μ
μ μμ±ν©λλ€.
κ·Έ ν μ λλ©μ΄μ
μ΄ μ μ©λ div νκ·Έμ μνλ layoutμ μ‘μμ£Όκ³ &.
μ μ΄μ©ν΄ frame-in
μ΄λ classNameμ΄ μ μ©λλ©΄ μ λλ©μ΄μ
ν¨κ³Όκ° μλνκ² ν΄μ€λλ€.
animation μμ±μλ μμμ μμ±ν
μ λλ©μ΄μ
μ΄λ¦(animation-name
)κ³Ό λλΆμ΄
μ λλ©μ΄μ
μκ°(animation-duration
),
μ λλ©μ΄μ
κ° μ μ©(animation-fill-mode
)λ₯Ό λ£μ΄μ£Όμμ΅λλ€.
μ΄ μ€μμλ animation-fill-mode
λ μμ±μ μ λλ©μ΄μ
μ΄ λλ νμ μνλ₯Ό μ€μ ν©λλ€.
μ΄ μμ±μ λ€μκ³Ό κ°μ κ°μ κ°μ§ μ μλλ° forwards
λ₯Ό λΆμ¬ν΄ μ λλ©μ΄μ
μ μ’
λ£λ λμ μνκ° κ³μ μ§μλλλ‘ ν©λλ€.
- none
μ λλ©μ΄μ μ΄ λλ ν μνλ₯Ό μ€μ νμ§ μμ΅λλ€. - forwards
μ λλ©μ΄μ μ΄ λλ ν κ·Έ μ§μ μ κ·Έλλ‘ μμ΅λλ€. - backwards
μ λλ©μ΄μ μ΄ λλ ν μμμ μΌλ‘ λμμ΅λλ€. - both
μ λλ©μ΄μ μ΄μ μ λ€ κ²°κ³Όλ₯Ό μ‘°ν©νμ¬ μ€μ ν©λλ€. - inherit
μ λλ©μ΄μ μ μνλ₯Ό μμ μμνν μμλ°μ΅λλ€.
π Reactμ IntersectionObserver μ μ©
import { useState, useRef, useEffect } from "react";
import { Container } from "./styled";
export default function App() {
const [isInViewport, setIsInViewport] = useState(false);
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!ref.current) return; // μμκ° μμ§ μ€λΉλμ§ μμ κ²½μ° μ€λ¨
const callback = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// μμκ° λ·°ν¬νΈμ λνλ¬μ κ²½μ°
setIsInViewport(true);
} else {
// μμκ° λ·°ν¬νΈλ₯Ό λ²μ΄λ κ²½μ°
setIsInViewport(false);
}
});
};
const options = { root: null, rootMargin: "0px", threshold: 0 };
const observer = new IntersectionObserver(callback, options);
observer.observe(ref.current); // μμ κ΄μ°° μμ
return () => {
observer.disconnect(); // μ»΄ν¬λνΈ μΈλ§μ΄νΈ μ κ΄μ°° μ€λ¨
};
}, []);
return (
<>
<Container>
<h1>μλλ‘ μ€ν¬λ‘€ νμΈμ</h1>
</Container>
<Container className={isInViewport ? "frame-in" : ""} ref={ref}>
<h1>μλ
νμΈμ</h1>
</Container>
</>
);
}
useEffectλ₯Ό μ΄μ©ν΄ IntersectionObserverμ κ°μ²΄(observer)κ° refλ₯Ό κ°μ§ν΄μ callbackμ νΈλ¦¬κ±°νλ μ½λμ λλ€.
μλνλ μμλ₯Ό λ°μ§μλ©΄ λ€μκ³Ό κ°μ΅λλ€.
- observerκ° μ€ν¬λ‘€μ κ°μ§νκ³ callback νΈμΆ
- callbackμ entryμμ μ¬μ©μμ viewportμ refλ₯Ό μ μ©ν Containerκ° μ§μ νλμ§ μ¬λΆ νλ¨
- λ§μ½ μ§μ
νμ§ μμλ€λ©΄ isInViewport μνλ₯Ό falseλ‘ set,
μ§μ νλ€λ©΄ trueλ‘ set - isInViewportκ° trueκ° λλ©΄ Containerμ classNameμ frame-inμ μ½μ
- μ λλ©μ΄μ μλ
μ μ½λμμ useEffect λ΄ μ€ν¬λ‘€ κ°μ§λ₯Ό μν μμλ€μ κΈ°λ₯κ³Ό μλ―Έλ λ€μκ³Ό κ°μ΅λλ€.
π IntersectionObserverEntry
IntersectionObserverEntry
λ IntersectionObserver
μ μ½λ°± ν¨μ λ΄μμ
κ° κ΄μ°° λμ μμμ κ°μμ± μ 보λ₯Ό λ΄κ³ μλ κ°μ²΄μ΄λ©° μ€μν μμ±μ λ€μκ³Ό κ°μ΅λλ€.
isIntersecting
κ΄μ°° λμ μμκ° νμ¬ λ·°ν¬νΈ λ΄μ 보μ΄λμ§ μ¬λΆλ₯Ό λνλ λλ€. Boolean κ°μΌλ‘ λ°νλ©λλ€.intersectionRatio
κ΄μ°° λμ μμμ λ·°ν¬νΈμ κ΅μ°¨ μμμ λΉμ¨μ λνλ λλ€.
μ΄ κ°μ 0λΆν° 1κΉμ§μ λ²μλ₯Ό κ°μ§λ©°, 0μ κ΅μ°¨ μμμ΄ μμμ, 1μ κ΄μ°° λμ μμκ° μμ ν λ·°ν¬νΈ λ΄μ μμμ μλ―Έν©λλ€.
π IntersectionObserverμ options
const options = { root: null, rootMargin: "0px", threshold: 0 };
const observer = new IntersectionObserver(callback, options);
IntersectionObserver
μ κ°μ²΄λ₯Ό μμ±νλ μμ±μμ λ€μ΄κ°λ options μΈμμ κ°λ€μ λ€μκ³Ό κ°μ μλ―Έλ₯Ό μ§λκ³ μμ΅λλ€.
root
- μ΄ μ΅μ μ λ·°ν¬νΈλ₯Ό κΈ°μ€μΌλ‘ κ΄μ°°ν λμ μμλ€μ μ ννλ μν μ ν©λλ€.
- κΈ°λ³Έκ°μ
null
λ‘, λ·°ν¬νΈ μ μ²΄κ° κ΄μ°° λμμ΄ λ©λλ€. - λ€λ₯Έ DOM μμλ₯Ό μ§μ νμ¬ ν΄λΉ μμ λ΄μμμ κ°μμ±μ κ°μ§νλλ‘ μ€μ ν μ μμ΅λλ€.
rootMargin
- λ£¨νΈ μμμ νκ² μμ μ¬μ΄μ μ¬λ°±(λ§μ§)μ μ€μ ν©λλ€.
- κ°μ CSSμ λ§μ§ κ°κ³Ό λμΌν νμμΌλ‘ μ§μ νλ©°, "top right bottom left" μμμ λλ€.
- λ§μ§ κ°μ ν΅ν΄ μμκ° λ·°ν¬νΈ λ΄μ λ€μ΄μ¬ λ μΌλ§λ μ΄μ μ, λλ λκ° λ μΌλ§λ λκ°κΈ° μ μ κ°μμ±μ κ°μ§ν μ§λ₯Ό μ‘°μ ν μ μμ΅λλ€.
threshold
- νκ² μμμ κ°μμ±μ νλ¨νλ κΈ°μ€μΌλ‘, κ°μ 0λΆν° 1 μ¬μ΄μ μμμ μ«μμ λλ€.
- κΈ°λ³Έκ°μ 0μΌλ‘, νκ² μμκ° λ·°ν¬νΈμ μ‘°κΈμ΄λΌλ λ€μ΄μ€λ©΄
isIntersecting
μ΄true
κ° λ©λλ€. - κ°μ΄ 컀μ§μλ‘ νκ² μμκ° λ·°ν¬νΈ λ΄μμ λ λ§μ λΉμ¨μ μ°¨μ§ν΄μΌ κ°μμ±μ΄ νμΈλ©λλ€.
π IntersectionObserverλ₯Ό μ¬μ©ν λ κ³ λ €ν΄μΌ ν μ¬ν
μ λ§ νΈλ¦¬νκ² μμλ€μ κ°μ§ν μ μλλ‘ κΈ°λ₯μ μ 곡νλ IntersectionObserver
μ΄μ§λ§
λ€μκ³Ό κ°μ μ¬νλ€μ κ³ λ €νλ©° μ μ©ν΄λ΄μΌ ν©λλ€.
- λΈλΌμ°μ μ§μ
IntersectionObserver
λ λλΆλΆμ νλ λΈλΌμ°μ μμ μ§μλμ§λ§, μ€λλ λΈλΌμ°μ μμλ μ§μλμ§ μμ μ μμ΅λλ€. μ΄ λλ¬Έμ ν¬λ‘μ€ λΈλΌμ°μ§μ κ³ λ €ν΄μΌ ν©λλ€. νμνλ€λ©΄ ν΄λ¦¬ν(polyfill)μ μ¬μ©νμ¬ λ―Έμ§μ λΈλΌμ°μ μμλ μ¬μ©ν μ μλλ‘ ν μ μμ΅λλ€. - λ λλ§ μ±λ₯
IntersectionObserver
λ₯Ό μ¬μ©νμ¬ μμμ κ°μμ±μ κ°μ§νλ©΄μ μμ£Ό μ€νλλ μ½λ°± ν¨μλ λ λλ§ μ±λ₯μ μν₯μ μ€ μ μμ΅λλ€. λ°λΌμ μ μ νthreshold
κ°μ μ€μ νμ¬ μ½λ°± ν¨μκ° λΆνμνκ² μμ£Ό μ€νλμ§ μλλ‘ μ‘°μ ν΄μΌ ν©λλ€. - μ½λ°± ν¨μ λΉμ©
IntersectionObserver
μ μ½λ°± ν¨μ λ΄μμ μ€νλλ μ½λκ° λ¬΄κ±°μ΄ κ²½μ°,
λ·°ν¬νΈ λ΄ μμμ κ°μμ± λ³κ²½ μ μ±λ₯ μ νκ° λ°μν μ μμ΅λλ€. - λ©λͺ¨λ¦¬ λμ
IntersectionObserver
μ μ½λ°± ν¨μμμ λ©λͺ¨λ¦¬ λμκ° λ°μνμ§ μλλ‘ μ£Όμν΄μΌ ν©λλ€.
μ½λ°± ν¨μ λ΄μμ μΈλΆ λ³μλ₯Ό μ¬μ©ν κ²½μ° ν΄λ‘μ (closure) λ¬Έμ μ μ μν΄μΌ ν©λλ€. - λμ λ³κ²½: DOM μμκ° λμ μΌλ‘ μμ±λκ±°λ λ³κ²½λ κ²½μ°, ν΄λΉ μμμ κ°μμ±μ μ¬λ°λ₯΄κ² κ°μ§ν μ μλλ‘
IntersectionObserver
λ₯Ό μ λ°μ΄νΈν΄μΌ ν μ μμ΅λλ€. - μμ ν¬κΈ°
κ°μμ±μ νλ¨νλ λ° κΈ°μ€μ΄ λλ μμμ ν¬κΈ°κ° μκ±°λ μμ κ²½μ°, μ νν κ°μμ±μ κ°μ§νκΈ° μ΄λ €μΈ μ μμ΅λλ€. - μλ² μ¬μ΄λ λ λλ§(SSR)
μλ² μ¬μ΄λ λ λλ§μ μ¬μ©νλ κ²½μ°,IntersectionObserver
λ λΈλΌμ°μ νκ²½μμλ§ μλνλ―λ‘ ν΄λΌμ΄μΈνΈ μ¬μ΄λμμλ§ μ¬μ©νλλ‘ μ£Όμν΄μΌ ν©λλ€.
π hookκ³Ό μ»΄ν¬λνΈλ‘ λΆλ¦¬νκΈ°
μ무λλ μ μμ κ°μ λ°©λ²μΌλ‘ μ€μ νλ‘μ νΈμ μ μ©νκΈ°μλ λͺ κ°μ§ λΆνΈν μ μ΄ μμμ΅λλ€.
- useEffect μ¬μ©μΌλ‘ μΈν μ»΄ν¬λνΈ μ½λ λΌμΈ μ¦κ° λ° κ°λ μ± μ ν
- μ λλ©μ΄μ μ μ© μμλ§λ€ λμ΄λλ refμ useEffect
μ΄λ₯Ό ν΄κ²°νκΈ° μν΄ μ λΆνΈ μ¬νλ€μ κ°μ νκ³ ScrollAnimationμ λ΄λΉν΄μ μ²λ¦¬ν hookμ λ§λ€μμ΅λλ€.
hookμμ refλ₯Ό λ°°μ΄λ‘ λ§λ€μ΄ μ¬λ¬ μμλ€μ νκΊΌλ²μ κ΄λ¦¬νλ λ°©λ²λ ꡬνν΄ λ΄€λλ°
λΆλ³μ± λλ¬Έμ λ‘μ§μ΄ λΆνμνκ² λ³΅μ‘ν΄μ§κ³ κ°λ
μ±λ μ’μ§ μμμ Έ μ»΄ν¬λνΈ λ°©μμΌλ‘ λ§λ€μ΄λ΄€μ΅λλ€.
π useScrollAnimation.ts
import { useRef, useState, useEffect } from "react";
export const useScrollAnimation = () => {
const [isInViewport, setIsInViewport] = useState(false);
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!ref.current) return; // μμκ° μμ§ μ€λΉλμ§ μμ κ²½μ° μ€λ¨
const callback = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// μμκ° λ·°ν¬νΈμ λνλ¬μ κ²½μ°
setIsInViewport(true);
} else {
// μμκ° λ·°ν¬νΈλ₯Ό λ²μ΄λ κ²½μ°
setIsInViewport(false);
}
});
};
const options = { root: null, rootMargin: "0px", threshold: 0 };
const observer = new IntersectionObserver(callback, options);
observer.observe(ref.current); // μμ κ΄μ°° μμ
return () => {
observer.disconnect(); // μ»΄ν¬λνΈ μΈλ§μ΄νΈ μ κ΄μ°° μ€λ¨
};
}, []);
return { isInViewport, ref };
};
μμμ λ΄€λ μ€ν¬λ‘€ κ°μ§ λ‘μ§μ κ·Έλλ‘ hookμΌλ‘ λΆλ¦¬νμ΅λλ€.
hookμμ refμ animation μ μ© μ¬λΆλ₯Ό νλ¨νλ λ‘μ§κ³Ό μνλ₯Ό μ μΈνκ³ return ν΄μ€λλ€.
π ScrollAnimationContainer.tsx
import { Container } from "./styled";
import { useScrollAnimation } from "./useScrollAnimation";
type PropsType = {
children: React.ReactNode;
};
export const ScrollAnimationContainer = ({ children }: PropsType) => {
const { ref, isInViewport } = useScrollAnimation();
return (
<Container ref={ref} className={isInViewport ? "frame-in" : ""}>
{children}
</Container>
);
};
κ·Έ λ€μ useScrollAnimation ν μ μ΄μ©ν΄ childrenμ κ°μΈμ£ΌκΈ°λ§ νλ©΄ μ λλ©μ΄μ μ΄ μ μ©λλ μ»΄ν¬λνΈλ₯Ό λ§λ€μ΄μ€λλ€.
π μ»΄ν¬λνΈ μ¬μ©
import { ScrollAnimationContainer } from "./ScrollAnimationContainer";
import { Container } from "./styled";
export default function App() {
return (
<>
<Container>
<h1>μλλ‘ μ€ν¬λ‘€ νμΈμ</h1>
</Container>
<ScrollAnimationContainer>
<h1>μλ
</h1>
</ScrollAnimationContainer>
<ScrollAnimationContainer>
<h1>νμΈμ</h1>
</ScrollAnimationContainer>
<ScrollAnimationContainer>
<h1>λ°κ°μ΅λλ€</h1>
</ScrollAnimationContainer>
</>
);
}
μμμ λͺ¨λ λ‘μ§μ μ²λ¦¬ν΄μ£Όλ μ»΄ν¬λνΈλ₯Ό λ§λ€μ΄λ λλΆμ
μμλ€μ κ°νΈνκ² wrapping νκΈ°λ§ ν΄λ μ€ν¬λ‘€ μ λλ©μ΄μ
μ΄ μ μ©λλ λͺ¨μ΅μ νμΈν μ μμ΅λλ€.
κ·ΈλΌ μμΈν μ½λμ ꡬ쑰λ μμ μ¬λ €λ CodeSandBoxλ₯Ό μ°Έκ³ ν΄μ£Όμκ³
μ€ν¬λ‘€ μ λλ©μ΄μ
μ μ μ©ν΄ λ λμ μΈ μΉμ κ°λ°ν΄λ΄μΌκ² μ΅λλ€.
λκΈ