[React] Blur ํจ๊ณผ๋ฅผ ํ์ฉํ ์ด๋ฏธ์ง ์ง์ฐ ๋ก๋ฉ Image Lazy Loading
์นํ์ด์ง์์ ์ฑ๋ฅ์ ์ํฅ์ ๊ฐ์ฅ ๋ง์ด ์ฃผ๋ ๋ถ๋ถ์ด ์ด๋ฏธ์ง / ๋น๋์ค ๊ฐ์ ๋ฏธ๋์ด ์์๋ค. ํนํ ์ด๋ฏธ์ง๋ ๋ฐฐ๋, ์ ํ ์ฌ์ง, ๋ก๊ณ ๋ฑ ํ์ด์ง ๊ตฌ์๊ตฌ์์์ ์ฌ์ฉํ๋ค. HTTP Archive Data์ ๋ฐ๋ฅด๋ฉด ์ ์ฒด ์นํ์ด์ง ์ฉ๋์ 45%
๋ฅผ ์ด๋ฏธ์ง๊ฐ ์ฐจ์งํ๋ค๊ณ ํ๋ค. ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํ์ง ์๋๊ฑด ๋ถ๊ฐ๋ฅํ์ง๋ง, ํ๋ฉด์ ๋
ธ์ถ๋ ๋๋ง ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ค๋ ๋ฐฉ์์ผ๋ก ํ์ด์ง ๋ก๋ฉ ์๊ฐ์ ๋จ์ถ์ํฌ ์ ์๋ค. ์ด๋ฐ ๋ฐฉ์์ Lazy Loading์ด๋ผ๊ณ ํ๋ค.
Lazy Loading ๊ตฌํ ๋ฐฉ๋ฒ
๐ก Lazy Loading์ด ์ ์ฉ๋ ์ด๋ฏธ์ง๊ฐ ๋ทฐํฌํธ์ ๊ทผ์ ํด์ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๋ฉด ์ฝํ ์ธ ๊ฐ ๋ฐ๋ ค๋๋ ํ์์ด ๋ฐ์ํ๋ค. ์ด๋ฅผ ๋ฐฉ์งํ๋ ค๋ฉด ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ธ๋ ์ปจํ ์ด๋ ์์์ ๋์ด / ๋๋น๋ฅผ ์ง์ ํ๋ฉด ๋๋ค.
Lazy Loading์ ํฌ๊ฒ Chrome Native ๋ฐฉ์๊ณผ JavaScript๋ฅผ ์ด์ฉํ ๋ฐฉ์์ผ๋ก ๊ตฌํํ ์ ์๋ค.
(1) Native Lazy Loading ๋ฐฉ์
Chrome 76 ๋ฒ์ ์ด์๋ถํฐ Native ๋ฐฉ์์ Lazy Loading์ ์ง์ํ๋ค. ์ ์ฉํ ์ด๋ฏธ์ง ํ๊ทธ์ loading
์์ฑ์ lazy
๋ง ์ถ๊ฐํ๋ฉด ๋๋ค. ์ฌ์ฉ๋ฒ์ ๊ฐ๋จํ์ง๋ง 76๋ฒ์ ์ด์์ ํฌ๋กฌ ๋ธ๋ผ์ฐ์ ์์๋ง ์ ์ฉ๋๋ ๋จ์ ์ด ์๋ค.
<img src="example.jpg" loading="lazy" width="200" height="200" alt="..." />
lazy
: ๋ทฐํฌํธ์ ๊ทผ์ ํ์ ๋๋ง ์ด๋ฏธ์ง ๋ก๋ — Lazy Loadingeager
: ๋ทฐํฌํธ์ ์๊ด์์ด ๋ชจ๋ ์ด๋ฏธ์ง ๋ก๋auto
:loading
์์ฑ์ ์ฌ์ฉํ์ง ์์์ ๋์ ๊ธฐ๋ณธ๊ฐ์ผ๋กlazy
์์ฑ์ ์ ์ฉํ ๊ฒ๊ณผ ๋์ผ.
(2) JavaScript ํ์ฉ ๋ฐฉ์
์ด๋ฏธ์ง ํ๊ทธ์ src
์์ฑ์ ๋ก๋ฉ ์ ๋ณด์ฌ์ง placeholder ์ด๋ฏธ์ง๋ฅผ ์ถ๊ฐํด๋๊ณ , ๋ฐ์ดํฐ ํ๋กํผํฐ๋ฅผ ์ด์ฉํด ์๋ณธ ์ด๋ฏธ์ง๋ฅผ data-src
์์ฑ์ ํ ๋นํด๋๋ค. ์ด๋ฏธ์ง ํ๊ทธ๊ฐ ํ๋ฉด์ ๋
ธ์ถ๋๋ฉด data-src
์์ฑ์ ์ง์ ํด๋์ ์๋ณธ ์ด๋ฏธ์ง๋ฅผ src
์์ฑ์ ํ ๋นํด์ ์๋ณธ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๋ค.
<!-- ํ๋ฉด ๋
ธ์ถ ์ -->
<img data-src="์๋ณธ ์ด๋ฏธ์ง ์ฃผ์" src="placeholder ์ด๋ฏธ์ง ์ฃผ์" />
<!-- ํ๋ฉด ๋
ธ์ถ ํ -->
<img data-src="์๋ณธ ์ด๋ฏธ์ง ์ฃผ์" src="์๋ณธ ์ด๋ฏธ์ง ์ฃผ์" />
์ ๋ฐฉ์์ ๊ตฌํํ๋ ค๋ฉด ์ด๋ฏธ์ง ํ๊ทธ์ ํ๋ฉด ๋ ธ์ถ ์ฌ๋ถ๋ฅผ ํ์ธํด์ผ๋๋ค. ๊ตฌํ ๋ฐฉ๋ฒ์ ์๋ 2๊ฐ์ง๊ฐ ์๋ค.
Scroll ์ด๋ฒคํธ + ๊ธฐํ ํ๋กํผํฐ ํ์ฉ
์คํฌ๋กค ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋๋ง๋ค ์์๊ฐ ํ๋ฉด์ ๋
ธ์ถ๋๋์ง ํ์ธํ๋ ๋ฐฉ์. getBoundingClientRect
๋ฉ์๋๋ฅผ ํธ์ถํด์ ๋ทฐํฌํธ ๊ธฐ์ค์ ์์ ์์น ์ขํ๊ฐ์ ์ป์ด์ผ ํ๋ค. ์ด ๋ฉ์๋๋ฅผ ํธ์ถํ ๋๋ง๋ค ๋ฆฌํ๋ก์ฐ๊ฐ ๋ฐ์ํ๋ ๋จ์ ์ด ์์ผ๋ฉฐ, ๋ง์ฐ์ค๋ฅผ ์คํฌ๋กคํ ๋๋ง๋ค ์ด๋ฒคํธ๊ฐ ๊ณ์ ํธ์ถ๋๋ฏ๋ก ์ค๋กํ๋ ์ ์ฉํด์ผ ๋๋ค.
// ์ฝ๋ ์ฐธ๊ณ via StackOverFlow
// ์์๊ฐ ํ๋ฉด์ ์์ ํ ๋ค์ด์๋์ง ํ์ธํ๋ ํจ์
function checkInView(el) {
// ์ฃผ์ด์ง ์์์ ๋ทฐํฌํธ ๊ธฐ์ค ์์น ๋ฐ ํฌ๊ธฐ ๋ฐํ
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 && // ๋ทฐํฌํธ ์๋จ๋ณด๋ค ์๋ ์๋์ง ์ฌ๋ถ
rect.left >= 0 && // ๋ทฐํฌํธ ์ผ์ชฝ๋ณด๋ค ์ค๋ฅธ์ชฝ์ ์๋์ง ์ฌ๋ถ
rect.bottom <= window.innerHeight && // ๋ทฐํฌํธ ํ๋จ ์ด๋ด์ ์๋์ง ์ฌ๋ถ
rect.right <= window.innerWidth // ๋ทฐํฌํธ ์ค๋ฅธ์ชฝ ์ด๋ด์ ์๋์ง ์ฌ๋ถ
);
}
window.addEventListener('scroll', () => {
document.querySelectorAll('.lazy-img').forEach((image) => {
if (checkInView(image)) {
image.src = image.dataset.src; // ์ด๋ฏธ์ง ํ๊ทธ๊ฐ ํ๋ฉด์ ๋
ธ์ถ๋๋ฉด src ์์ฑ์ ์๋ณธ์ฃผ์ ํ ๋น
}
});
});
Intersection Observer API ํ์ฉ โญ๏ธ
๐ก Intersection Observer API์ ๊ด๋ จํ ๋ ์์ธํ ๋ด์ฉ์ ๋งํฌ ์ฐธ๊ณ
Intersection Observer๋ ์์๊ฐ ํ๋ฉด์ ๋
ธ์ถ๋๋์ง ์๋ ค์ฃผ๋ API๋ค. ๋น๋๊ธฐ๋ก ์๋ํ๊ณ ๋ฉ์ธ ์ค๋ ๋์ ์ํฅ์ ์ฃผ์ง ์๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ๋ฉด์์ ๋ ์ ๋ฆฌํ๋ค. ์ฌ์ฉ๋ฒ๋ ๊ฐ๋จํ๋ค. Lazy Loading ์ ์ฉ ํ๊ทธ๋ฅผ IO์ ๊ด์ฐฐ ๋์์ผ๋ก ๋ฑ๋กํ๊ณ , ํด๋น ํ๊ทธ๊ฐ ํ๋ฉด์ ๋
ธ์ถ๋๋ฉด src
์์ฑ์ ์๋ณธ ์ฃผ์๋ฅผ ํ ๋นํ ๋ค ๊ด์ฐฐ์ ํด์ ํ๋ฉด ๋๋ค.
const ioHandler = (entries, io) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src; // src ์์ฑ์ ์๋ณธ ์ฃผ์ ํ ๋น
io.unobserve(entry.target); // ํ๋ฉด์ ๋
ธ์ถ๋ผ์ ์๋ณธ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ์ผ๋ฏ๋ก ๊ด์ฐฐ ํด์
}
});
};
const observer = new IntersectionObserver(ioHandler); // IO ์ธ์คํด์ค ์์ฑ
document.querySelectorAll('.lazy-img').forEach((el) => observer.observe(el)); // ๊ด์ฐฐ ๋์ ๋ฑ๋ก
IO API ํ์ฉํด์ ๊ตฌํํ๊ธฐ
์ด๋ฏธ์ง API
Lorem Picsum๋ Unsplash์ ์ผ๋ถ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์์ ์ ๊ณตํ๋ ๋ฌด๋ฃ API๋ค. ์ด๋ฏธ์ง ์ฌ์ด์ฆ ์ง์ , ํ๋ฐฑ / ๋ธ๋ฌ ํจ๊ณผ, ๋๋ค ์ด๋ฏธ์ง ๋ฑ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค. Lorem Picsum์์ ์ด๋ฏธ์ง ๋ฆฌ์คํธ๋ ์ ๊ณตํ๋๋ฐ ์ด๋ฅผ ์ด์ฉํ๋ฉด ๋ฌดํ์คํฌ๋กค + Lazy Loading์ ๊ตฌํํ๊ธฐ ๋ฑ ์ ๋นํ๋ค. ํ์ด์ง๋ณ๋ก ํธ์ถํ ์ ์์ผ๋ฉฐ 1๊ฐ ํ์ด์ง์ ๋ช๊ฐ์ ์ด๋ฏธ์ง ์ ๋ณด๋ฅผ ๋ฐ์์ฌ์ง๋ ์ ํ ์ ์๋ค.
# API ์ฃผ์ ์์
https://picsum.photos/v2/list?page=2&limit=100 [์ด๋ฏธ์ง ๋ฆฌ์คํธ]
https://picsum.photos/200/300/?blur=2 [๋ธ๋ฌ ํจ๊ณผ๊ฐ ์ ์ฉ๋ 200x300 ์ด๋ฏธ์ง]
https://picsum.photos/id/237/200/300 [ID๊ฐ 237์ธ 200x300 ์ด๋ฏธ์ง]
// ์ด๋ฏธ์ง ๋ฆฌ์คํธ GET 200 OK
[
{
"id": "0",
"author": "Alejandro Escamilla",
"width": 5616,
"height": 3744,
"url": "https://unsplash.com/...",
"download_url": "https://picsum.photos/..."
},
]
IO Custom Hook
Intersection Observer API๋ ๋ฌดํ ์คํฌ๋กค์ ๋ฌผ๋ก Lazy Loading, ๊ด๊ณ ๊ฐ์์ฑ ํ์ธ ๋ฑ ๋ค์ํ ๊ณณ์์ ์ฌ์ฉํ๋ฏ๋ก ์ปค์คํ Hook์ผ๋ก ๋ถ๋ฆฌํด์ ์ฌ์ฌ์ฉํ๋ฉด ์ข๋ค. ์ปค์คํ Hook์ ์ธ์๋ ์๋ 3๊ฐ๋ฅผ ๋ฐ๋๋ก ์์ฑํ๋ค. ํจ์ ํธ์ถ์ ์ธ์ ์์์ ๊ตฌ์ ๋ฐ์ง ์๋๋ก RORO ํจํด์ผ๋ก ์์ฑํ๋ค.
callback
: ๊ต์ฐจ ์ํ์์ ์คํํ ๋ก์ง์ด ๋ด๊ธด ์ฝ๋ฐฑ ํจ์unObserver
: ๊ต์ฐจ ์ํ ํ ํด๋น ํ๊ฒ์ ๊ด์ฐฐ์ ์ค์งํ ์ง ์ฌ๋ถ.true | false
๐ก ๋ฌดํ์คํฌ๋กค์ ๊ต์ฐจ ์ํ ์ดํ์๋ ๊ฐ์ฅ ํ๋จ์ ๋น ์์๋ฅผ ๊ณ์ ๊ด์ฐฐํด์ผ ํ์ง๋ง(false
), Lazy Loading ๋ฑ์ ๊ตฌํํ ๋ ์ด๋ฏธ ํ๋ฉด์ ๋ ธ์ถ๋ผ์ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ ์ํ๋ฉด ๋์ด์ ๊ด์ฐฐํ ํ์๊ฐ ์๋ค(true
).options
: Intersection Observer ์ธ์คํด์ค ์์ฑ ์ ๋๊ธธ ์ต์
์ปค์คํ
Hook์ Ref ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋๋ก ํ๋ค. ๊ทธ๋ผ Hook์ ํธ์ถํ๋ ์ปดํฌ๋ํธ์์ Ref ๊ฐ์ฒด๋ฅผ ๋ฐ์ ๋ค ๊ด์ฐฐํ๊ณ ์ถ์ ์ปดํฌ๋ํธ์ ref
์์ฑ์ ๋ฐ๋ก ํ ๋นํ๋ฉด ๋๋ค. ์ฌ์ฉํ๊ธฐ ๋ ํธํ๋ค.
// ์ฝ๋ ์ผ๋ถ ์ฐธ๊ณ : https://mrcoles.com/intersection-observer-react-hook/
import { useCallback, useEffect, useRef } from 'react';
export default function useIntersectionObserver({
callback,
unObserve = false,
options = {
root: null, // ๋
ธ์ถ&๋น๋
ธ์ถ ์ฌ๋ถ๋ฅผ ์ด๋ค ์์๋ฅผ ๊ธฐ์ค์ผ๋ก ํ ์ง ์ง์ ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ null(๋ทฐํฌํธ)
rootMargin: '0px', // ๋ฐ๊นฅ ์ฌ๋ฐฑ(Margin)์ ์ด์ฉํด Root ๋ฒ์๋ฅผ ํ์ฅ/์ถ์ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ 0px
threshold: 0, // ํ๋ฉด์ ์ผ๋งํผ ๋
ธ์ถ๋ผ์ผ ์ฝ๋ฐฑ ํจ์๋ฅผ ํธ์ถํ ์ง ๊ฒฐ์ ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ 0(1px ๋ผ๋ ํ๋ฉด์ ๋ณด์ด๋ฉด ์ฝ๋ฐฑ ํธ์ถ)
},
}) {
const ioRef = useRef(null);
// Intersection Observer ์ฝ๋ฐฑ ์ ์(useCallback ์ ์ฉ์ ้้กน)
// (์ธ์1) entries: ๊ด์ฐฐ ์ค์ธ ๋ชจ๋ ๋์์ด ๋ด๊ธด ๋ฐฐ์ด, (์ธ์2) observer: ๊ด์ฐฐ์ ๊ฐ์ฒด
const ioHandler = useCallback((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// ํ๋ฉด ์์ ํ๊ฒ ์์๊ฐ ๋ค์ด์๋์ง ์ฒดํฌ
callback(); // ๊ต์ฐจ ์์ ์(๊ด์ฐฐ ๋์์ด ๋ทฐํฌํธ์ ๋
ธ์ถ) ์ธ์๋ก ๋ฐ์ ์ฝ๋ฐฑ ํธ์ถ
if (unObserve) observer.unobserve(entry.target); // (์กฐ๊ฑด ๋ง์กฑ์) ํด๋น ํ๊ฒ์ ๊ด์ฐฐ ์ค์ง
}
});
}, []);
useEffect(() => {
if (window.IntersectionObserver) {
const observer = new IntersectionObserver(ioHandler, options); // ์ฝ๋ฐฑ&์ต์
๋ฑ๋ก ๋ฐ IO ๊ฐ์ฒด ์์ฑ
observer.observe(ioRef.current); // ์ธ์๋ก ๋๊ธด ์์๋ฅผ ๊ด์ฐฐ ๋์์ผ๋ก ๋ฑ๋ก
return () => observer.disconnect(); // ์ธ๋ง์ดํธ์ ๋ชจ๋ ์์์ ๋ํ ๊ด์ฐฐ ์ค์ง
}
}, []);
return ioRef;
}
// ์ฝ๋ ์ผ๋ถ ์ฐธ๊ณ : https://mrcoles.com/intersection-observer-react-hook/
import { useCallback, useEffect, useRef } from 'react';
interface IOProps {
callback: VoidHandler;
unObserve?: boolean;
options?: IntersectionObserverInit;
}
export default function useIntersectionObserver<T extends HTMLElement>({
callback,
unObserve = false,
options = {
root: null, // ๋
ธ์ถ&๋น๋
ธ์ถ ์ฌ๋ถ๋ฅผ ์ด๋ค ์์๋ฅผ ๊ธฐ์ค์ผ๋ก ํ ์ง ์ง์ ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ null(๋ทฐํฌํธ)
rootMargin: '0px', // ๋ฐ๊นฅ ์ฌ๋ฐฑ(Margin)์ ์ด์ฉํด Root ๋ฒ์๋ฅผ ํ์ฅ/์ถ์ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ 0px
threshold: 0, // ํ๋ฉด์ ์ผ๋งํผ ๋
ธ์ถ๋ผ์ผ ์ฝ๋ฐฑ ํจ์๋ฅผ ํธ์ถํ ์ง ๊ฒฐ์ ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ 0(1px ๋ผ๋ ํ๋ฉด์ ๋ณด์ด๋ฉด ์ฝ๋ฐฑ ํธ์ถ)
},
}: IOProps) {
const ioRef = useRef<T>(null);
// Intersection Observer ์ฝ๋ฐฑ ์ ์(useCallback ์ ์ฉ์ ้้กน)
// (์ธ์1) entries: ๊ด์ฐฐ ์ค์ธ ๋ชจ๋ ๋์์ด ๋ด๊ธด ๋ฐฐ์ด, (์ธ์2) observer: ๊ด์ฐฐ์ ๊ฐ์ฒด
const ioHandler: IntersectionObserverCallback = useCallback(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// ํ๋ฉด ์์ ํ๊ฒ ์์๊ฐ ๋ค์ด์๋์ง ์ฒดํฌ
callback(); // ๊ต์ฐจ ์์ ์(๊ด์ฐฐ ๋์์ด ๋ทฐํฌํธ์ ๋
ธ์ถ) ์ธ์๋ก ๋ฐ์ ์ฝ๋ฐฑ ํธ์ถ
if (unObserve) observer.unobserve(entry.target); // (์กฐ๊ฑด ๋ง์กฑ์) ํด๋น ํ๊ฒ์ ๊ด์ฐฐ ์ค์ง
}
});
},
[callback, unObserve],
);
useEffect(() => {
if (!window.IntersectionObserver || !ioRef.current) return undefined;
const observer = new IntersectionObserver(ioHandler, options); // ์ฝ๋ฐฑ&์ต์
๋ฑ๋ก ๋ฐ IO ๊ฐ์ฒด ์์ฑ
observer.observe(ioRef.current); // ์ธ์๋ก ๋๊ธด ์์๋ฅผ ๊ด์ฐฐ ๋์์ผ๋ก ๋ฑ๋ก
return () => observer.disconnect(); // ์ธ๋ง์ดํธ์ ๋ชจ๋ ์์์ ๋ํ ๊ด์ฐฐ ์ค์ง
}, [ioHandler, options]);
return ioRef;
}
(1) Single color Placeloader
LazyImage ์ปดํฌ๋ํธ
Lazy Loading์ ์ ์ฉํ ์ด๋ฏธ์ง๋ LazyImage
์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํ๋ค. data-src
์ ์๋ณธ ์ด๋ฏธ์ง ์ฃผ์๋ฅผ ํ ๋นํ๊ณ ํ๋ฉด์ ๋
ธ์ถ๋์ ๋ src
์์ฑ์ผ๋ก ๋ฐ๊พธ๋ ๋ฐฉ๋ฒ์ด ์๋, ํ๋ฉด ๋
ธ์ถ ์ฌ๋ถ๋ฅผ isInView
์ํ์์ ๊ด๋ฆฌํ๋๋ก ํ๋ค. ํ๋ฉด ๋
ธ์ถ์ ์ ์ด๋ฏธ์ง ํ๊ทธ๋ฅผ ๊ฐ์ธ๋ ํ์ ๋ฐฐ๊ฒฝ์ ๋ถ๋ชจ ์๋ฆฌ๋จผํธ(placeholder)๋ง ๋ณด์ด๊ณ , ํ๋ฉด์ ๋
ธ์ถ ๋์ ๋ isInView
์ํ๋ฅผ ๋ณ๊ฒฝํด์ ์ด๋ฏธ์ง ํ๊ทธ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ฐฉ์์ด๋ค.
์ฒซ ํ๋ฉด์ Lazy Loading ์ ์ฉ ์์ด ์ด๋ฏธ์ง๋ฅผ ๋ฐ๋ก ๋ณด์ฌ์ค์ผ ํ๋ฏ๋ก noLazy
๋ผ๋ props๋ฅผ ๋ฐ์์ ์ง์ฐ ๋ก๋ฉ ์ฌ๋ถ๋ฅผ ํ๋จํ ์ ์๋๋ก ํ๋ค(์ฒ์ 5๊ฐ ์ด๋ฏธ์ง๋ noLazy
๋ก ์ค์ ํ๋ค).
// LazyImage.js
function LazyImage({ src, noLazy, details }) {
const [isInView, setIsInView] = useState(!!noLazy);
const [isLoaded, setIsLoaded] = useState(false);
const imageRef = useIntersectionObserver({ // IO ์ปค์คํ
ํ
callback: () => setIsInView(true), // ๊ด์ฐฐ ๋์์ด ํ๋ฉด์ ๋
ธ์ถ๋๋ฉด isInView ์ํ ๋ณ๊ฒฝ
unObserve: true, // ๊ด์ฐฐ ๋์์ด ํ๋ฉด์ ๋
ธ์ถ(๊ต์ฐจ)๋๋ฉด ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ ์ํ์ด๋ฏ๋ก ๊ด์ฐฐ ํด์
options: { rootMargin: '40%' }, // ์คํฌ๋กค์ด ํ๋จ์ ๋๋ฌํ์์ฆ์ ๋ค์ ์ด๋ฏธ์ง๋ฅผ ๋ฏธ๋ฆฌ ๋ถ๋ฌ์ค๊ธฐ ์ํด rootMargin ์ค์
});
const imgClasses = classnames(
`transition-opacity ease-in-out ${!noLazy && 'duration-1000'}`,
// ์ด๋ฏธ์ง ์์ํ ๋ณด์ด๋ ํจ๊ณผ ('opacity-0': !isLoaded ๋ง ์
๋ ฅํด๋ ์๋ํจ)
{ 'opacity-100': isLoaded, 'opacity-0': !isLoaded },
);
return (
<div
ref={imageRef} // IO ์ปค์คํ
ํ
์ด ๋ฐํํ ref ๊ฐ์ฒด ํ ๋น
className="bg-gray-200 w-4/6 aspect-[5/3] rounded-md overflow-hidden"
// ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ธ๋ ์ปจํ
์ด๋ ์์์ ๋๋น/๋์ด๋ฅผ ์ง์ ํด์ ์ด๋ฏธ์ง ๋ก๋ฉ์ ๋ฐ๋ฆผ ํ์์ ๋ฐฉ์งํ๋ค
>
{isInView && (
<img
className={imgClasses}
src={src}
onLoad={() => setIsLoaded(true)} // ์ด๋ฏธ์ง ๋ก๋๋ฅผ ์๋ฃํ๋ฉด isLoaded ์ํ ๋ณ๊ฒฝ
alt={details.author}
/>
)}
</div>
);
}
์ด๋ฏธ์ง ํ๊ทธ์ opacity
(ํฌ๋ช
๋)๋ ๊ธฐ๋ณธ๊ฐ 0
์์ โํ๋ฉด์ ๋
ธ์ถ๋ ํ โ์ด๋ฏธ์ง ๋ก๋๋ฅผ ์๋ฃํ๋ฉด 1
๋ก ๋ณ๊ฒฝํ๋๋ก ์ค์ ํ๋ค. ์ฌ๊ธฐ์ transition: opacity 1s ease-in-out
CSS ์์ฑ์ ์ฃผ๋ฉด ์ด๋ฏธ์ง๊ฐ ์์ํ ๋ณด์ด๋ ํจ๊ณผ๋ฅผ ์ค ์ ์๋ค. ์ฒซ ํ๋ฉด์์ ์ง์ฐ ๋ก๋ฉ์ด ํ์ ์์ผ๋ฏ๋ก noLazy
ํ๋กญ์ด false
์ผ ๋๋ง transition
ํจ๊ณผ ์ง์ ์๊ฐ์ด 1์ด๊ฐ ๋๋๋ก(duration-1000
) ์์ฑํ๋ค.
const imgClasses = classnames(
`transition-opacity ease-in-out ${!noLazy && 'duration-1000'}`,
// ์ด๋ฏธ์ง ์์ํ ๋ณด์ด๋ ํจ๊ณผ ('opacity-0': !isLoaded ๋ง ์
๋ ฅํด๋ ์๋ํจ)
{ 'opacity-100': isLoaded, 'opacity-0': !isLoaded },
);
LazyLoading ์ปดํฌ๋ํธ
API ํธ์ถ ์ญ์ ์ปค์คํ
ํ
(useFetchData.js
)์ผ๋ก ๋ง๋ค์ด์ ์ฌ์ฉํ๋ค. ๋ ๋๋ง ๋ฆฌ์คํธ์ ์์์ ๋ง๋ IO ์ปค์คํ
ํ
์ ์ด์ฉํด์ ๋ฌดํ ์คํฌ๋กค์ ์ ์ฉํ๋ค. ํ๋ฉด ๊ฐ์ฅ ์๋๋ก ์คํฌ๋กคํ๋ฉด page
์ํ๊ฐ ๋ณ๊ฒฝ๋๊ณ , page
์ํ๊ฐ ๋ณ๊ฒฝ๋๊ฑธ ๊ฐ์งํ useFetchData
์ปค์คํ
ํ
์ด ํด๋น page
์ ๋ํ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ฐ์์จ๋ค.
// LazyLoading.js
export default function LazyLoading() {
const [page, setPage] = useState(1);
const { data, loading } = useFetchData({
url: 'https://picsum.photos/v2/list', // ์ด๋ฏธ์ง ๋ฆฌ์คํธ API ์ฃผ์
params: { page, limit: 20 }, // page ์ํ ๋ณ๊ฒฝ ์ ํด๋น page์ ๋ํ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ฐ์์จ๋ค
});
const loaderRef = useIntersectionObserver({
// ๋ฌดํ ์คํฌ๋กค Intersection Observer
callback: () => setPage((prev) => prev + 1), // ํ๋ฉด ๊ฐ์ฅ ์๋๋ก ์คํฌ๋กค ์ page ์ํ ๋ณ๊ฒฝ
});
return (
<section className="flex flex-col justify-center items-center p-8 gap-8">
{data?.map(({ id, ...details }, i) => (
<LazyImage
key={id}
details={details} // ์ด๋ฏธ์ง ์ธ๋ถ์ ๋ณด
src={`https://picsum.photos/id/${id}/1280/768`}
noLazy={i < 5} // ์ฒซ ํ๋ฉด์์ ์ง์ฐ๋ก๋ฉ์ด ํ์ ์์ผ๋ฏ๋ก ์ฒ์ 5๊ฐ ์ด๋ฏธ์ง๋ ๋ฐ๋ก ๋ณด์ฌ์ค๋ค
/>
))}
<div className={`${loading ? 'visibility' : 'invisible'}`}>
๐๏ธ Fetching items...
</div>
{/* ํ๋ฉด ๊ฐ์ฅ ์๋๊น์ง ์คํฌ๋กค ํ๋์ง ํ์ธํ๊ธฐ ์ํ ๋น์์ */}
<div ref={loaderRef} className={`w-full ${data ? 'block' : 'hidden'}`} />
</section>
);
}
page
, query
์ฟผ๋ฆฌ์คํธ๋ง(key=value
)์ ๊ฒ์์ฐฝ์ด๋ ๋ฌดํ ์คํฌ๋กค ๋ฑ์ ๊ตฌํํ ๋ value๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ฐ์์ผ ๋๋ค. ๋ฐ๋ผ์ useEffect
์ข
์์ฑ ๋ฐฐ์ด์ ์ถ๊ฐํด์ ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ๋ค์ ํธ์ถ ๋๋๋ก ํ๋ค. key
๋ API๋ง๋ค ๋ค๋ฅด๋ฏ๋ก(q=value
, query=value
๋ฑ) ์ํฉ์ ๋ง๊ฒ ๋ณ๊ฒฝํด์ ์ฌ์ฉํ๋ค.
import { useEffect, useState } from 'react';
import axios from 'axios';
const useFetchData = ({
method = 'get',
url,
payload,
params: { page, query, ...restParams },
}) => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true); // API ํธ์ถ ์์ - ๋ก๋ฉ ์ํ ๋ณ๊ฒฝ
axios({
method, // ๊ธฐ๋ณธ๊ฐ get
url,
data: payload, // payload ๊ฐ์ด null ํน์ undefined ์ด๋ฉด ์์ฒญ ๋ฐ๋์ ์ถ๊ฐ๋์ง ์๋๋ค
params: { page, q: query, ...restParams },
})
.then((res) => {
setData((prev) => (prev ? [...prev, ...res.data] : res.data));
})
.catch((err) => {
setError(err);
})
.finally(() => {
setLoading(false); // API ํธ์ถ ์ข
๋ฃ - ๋ก๋ฉ ์ํ ๋ณ๊ฒฝ
});
}, [page, query]); // page ๋ฐ query ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค API ํธ์ถ
return { loading, data, error };
};
export default useFetchData;
(2) Dominant color placeholder
๐ก Dominant color placeholder๋ ๋ฐ๋ก ๊ตฌํํ์ง ์๊ณ ์ค๋ช ๋ง ์์ฑํ๋ค.
Pinterest, Google Image ๋ฑ์ ์ฌ์ดํธ๋ ์ด๋ฏธ์ง๊ฐ ๋ก๋๋๊ธฐ ์ ํด๋น ์ด๋ฏธ์ง์ ๋ฉ์ธ ์ปฌ๋ฌ๋ฅผ ๋จผ์ ๋ณด์ฌ์ค๋ค. ์ด๋ฏธ์ง์ ์ฒ์ 1x1 ํฝ์ ๋ก ์ค์ผ์ผ์ ๊ฐ์์ํจ ํ ํด๋น ํฝ์ ๋ก placeholder๋ฅผ ์ฑ์ฐ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ค๊ณ ํ๋ค. ํด๋ผ์ด์ธํธ์์ ์ด๋ฏธ์ง ๋ฆฌ์์ค๋ฅผ ๊ฐ์ง๊ณ ์์ง ์์ผ๋ฏ๋ก ์๋ฒ์์ Dominant Color๋ฅผ ์ถ์ถํ ํ ํด๋ผ์ด์ธํธ๋ก ๋ณด๋ด์ฃผ๋ ์์ ์ด ํ์ํ ๊ฒ ๊ฐ๋ค.
Imagekit์ ๊ฐ์ข
์ต์ ํ ๊ธฐ๋ฅ์ด ํฌํจ๋ ์ด๋ฏธ์ง CDN์ด๋ค. ์ด๋ฏธ์ง URL์ ์ฟผ๋ฆฌ์คํธ๋ง๋ง ์ถ๊ฐํ๋ฉด Dominant ์์์ด ์ฑ์์ง placeholder ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋ค. Imagekit์ ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋ํ ํ URL ๋์ ?tr=w-1,h-1:w-๋๋น,h-๋์ด
์ฟผ๋ฆฌ ์คํธ๋ง๋ง ์ถ๊ฐํ๋ฉด ๋๋ค. ๋ฌด๋ฃ ๋ฒ์ ์ 20GB ๋์ญํญ๊น์ง ์ฌ์ฉํ ์ ์์ผ๋ฏ๋ก ํ ์ด ํ๋ก์ ํธ ๊ท๋ชจ์ ์ฌ์ฉํ๊ธฐ ์ ๋นํด๋ณด์ธ๋ค.
# ์ถ์ฒ ImageKit ๊ณต์ ๋ฌธ์
URL-endpoint transformation image path
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg
# Imagekit์ ์
๋ก๋ํ 1280x768 ํฌ๊ธฐ์ ์๋ณธ ์ด๋ฏธ์ง URL (130kb)
https://ik.imagekit.io/colorfilter/lazy-loading/image-01.jpg
# Dominant ์ปฌ๋ฌ๋ก ์ฑ์์ง 1280x768 ํฌ๊ธฐ์ ์ด๋ฏธ์ง URL (2kb)
https://ik.imagekit.io/colorfilter/lazy-loading/image-01.jpg?tr=w-1,h-1:w-1280,h-768
Imagekit์์ ์ ๊ณตํ๋ ์ด๋ฏธ์ง transformation ๊ธฐ๋ฅ์ผ๋ก ์ ํ์ง์ ๋ธ๋ฌ ์ฒ๋ฆฌ๋ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ฌ ์๋ ์๋ค. ๋ธ๋ฌ ํจ๊ณผ๋ 1~100
, ํ๋ฆฌํฐ ์กฐ์ ์ 1~100
(๊ธฐ๋ณธ๊ฐ 80
) ์ฌ์ด์ ๊ฐ์ ์
๋ ฅํ๋ฉด ๋๋ค. ์๋ณธ ์ด๋ฏธ์ง ์ฉ๋์ด 130kb ์ ๋์๋๋ฐ ํ๋ฆฌํฐ๋ฅผ ๋ฎ์ถ๊ณ ๋ธ๋ฌ ํจ๊ณผ๋ฅผ ์ ์ฉํ๋ 5kb ์ ๋๋ก ์ค์๋ค. placeholder๋ก ์ฌ์ฉํ๊ธฐ ์ ์ ํ๋ค.
# ๋ธ๋ฌ 30, ํ๋ฆฌํฐ 50์ด ์ ์ฉ๋ 1280x768 ํฌ๊ธฐ์ ์ด๋ฏธ์ง URL (5kb)
https://ik.imagekit.io/colorfilter/lazy-loading/image-01.jpg?tr=w-1280,h-768,bl-30,q-50
๋ค์ํ ์ด๋ฏธ์ง transformation ๊ธฐ๋ฅ ์ธ์๋ React ๊ฐ์ ํ๋ ์์ํฌ์์ ์ฌ์ฉํ ์ ์๋ ํจํค์ง๋ ์ ๊ณตํ๋ค. ๊ธฐ์กด ์ฌ์ฉํ๋ ์ ์ฅ์(Amazon S3, Google Storage ๋ฑ)๊ฐ ์๋ค๋ฉด Imagekit๊ณผ ํตํฉํด์ ์ฌ์ฉํ ์๋ ์๋ค.
(3) Blur image placeholder โญ๏ธ
๐ก ๋ ๋๋ง ๊ณผ์ : โํ๋ฉด์ ๋ ธ์ถ๋๊ธฐ ์ ๊น์ง ํ์ ๋ฐฐ๊ฒฝ์ ๋น ์์ ํ์ → โํ๋ฉด์ ๋ ธ์ถ๋๋ฉด placeholder ์ด๋ฏธ์ง ํ์ํ๊ณ ์๋ณธ ์ด๋ฏธ์ง ๋ก๋ ์์ → โ์๋ณธ ์ด๋ฏธ์ง ๋ก๋๋ฅผ ๋ง์น๋ฉด ํ๋ฉด์ ํ์
Lorem Picsum API์ Imagekit ๊ฐ์ ๋ค์ํ transformation ๊ธฐ๋ฅ์ ์์ง๋ง Blur ํจ๊ณผ๋ฅผ ์ ์ฉํ ์ ์๋ค. ์ด๋ฏธ์ง ์ฌ์ด์ฆ๋ ์กฐ์ ํ ์ ์์ผ๋ฏ๋ก ์์ ์ฌ์ด์ฆ์ ๋ธ๋ฌ ํจ๊ณผ๊ฐ ์ ์ฉ๋ ์ด๋ฏธ์ง๋ฅผ Placeholder๋ก ์ฌ์ฉํ๋ฉด ๋๋ค. ์ฐธ๊ณ ๋ก CSS์ filter
์์ฑ์ผ๋ก๋ ๋ธ๋ฌ ํจ๊ณผ๋ฅผ ์ค ์ ์๋ค. ex) filter: blur(10x)
๋ธ๋ฌ ํจ๊ณผ๋ 1~10
์ฌ์ด์ ๊ฐ๋๋ก ์กฐ์ ํ ์ ์๋ค. 50x30
์ ๋์ ์์ ์ฌ์ด์ฆ ์ด๋ฏธ์ง์ ํฌ๊ธฐ๋ฅผ ๋๋ฆฌ๋ฉด ๊นจ์ง ํ์์ด ๋ํ๋์ง๋ง ๋ธ๋ฌ ํจ๊ณผ๋ฅผ ์ ์ฉํ๊ธฐ ๋๋ฌธ์ ๊นจ์ง ๋ถ๋ถ์ด ๋ธ๋ฌ ํจ๊ณผ์ ์ํด ๊ฐ๋ ค์ง๋ค. ์ฉ๋๋ 1kb
์ํ์ด๋ค.
โ๏ธ Placeholder ์ด๋ฏธ์ง๋ฅผ ๋ฐ์์ค๊ธฐ ์ํด ๋คํธ์ํฌ ์์ฒญ์ 1๋ฒ ๋ ํด์ผ๋๋ ๋จ์ ์ด ์๋ค.
# 1280x768 ์ด๋ฏธ์ง (1024๋ ์ด๋ฏธ์ง ID)
https://picsum.photos/id/1025/1280/768
# blur 3์ด ์ ์ฉ๋ 50x30 ์ด๋ฏธ์ง
https://picsum.photos/id/1025/50/30?blur=3
LazyLoading ์ปดํฌ๋ํธ ์์
LazyImage
์ปดํฌ๋ํธ๋ก ๋๊ธฐ๋ props์ Placeholder๋ก ์ฌ์ฉํ ์ด๋ฏธ์ง ์ฃผ์์ธ thumb
๋ฅผ ์ถ๊ฐํ๋ค.
export default function LazyLoading() {
// ...์๋ต
return (
<section className="flex flex-col justify-center items-center p-8 gap-8">
{data?.map(({ id, ...details }, i) => (
<LazyImage
key={id}
details={details} // ์ด๋ฏธ์ง ์ธ๋ถ์ ๋ณด
src={`https://picsum.photos/id/${id}/1280/768`} // original ์ด๋ฏธ์ง
thumb={`https://picsum.photos/id/${id}/50/30?blur=3`} // placeholder ์ด๋ฏธ์ง
noLazy={i < 5} // ์ฒซ ํ๋ฉด์์ ์ง์ฐ๋ก๋ฉ์ด ํ์ ์์ผ๋ฏ๋ก ์ฒ์ 5๊ฐ ์ด๋ฏธ์ง๋ ๋ฐ๋ก ๋ณด์ฌ์ค๋ค
/>
))}
{/* ์๋ต */}
</section>
);
}
export default function LazyLoading() {
const [page, setPage] = useState(1);
const { data, loading } = useFetchData({
url: 'https://picsum.photos/v2/list', // ์ด๋ฏธ์ง ๋ฆฌ์คํธ API ์ฃผ์
params: {
page, // page ์ํ ๋ณ๊ฒฝ ์ ํด๋น page ์ ๋ํ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ฐ์์จ๋ค
limit: 20, // 1๊ฐ ํ์ด์ง์ ์ต๋ 20๊ฐ ์์ดํ
์ผ๋ก ์ ํ
},
});
// ๋ฌดํ ์คํฌ๋กค Intersection Observer
const loaderRef = useIntersectionObserver({
callback: () => {
setPage((prev) => prev + 1); // ํ๋ฉด ๊ฐ์ฅ ์๋๋ก ์คํฌ๋กค ์ page ์ํ ๋ณ๊ฒฝ
},
});
return (
<section className="flex flex-col justify-center items-center p-8 gap-8">
{data?.map(({ id, ...details }, i) => (
<LazyImage
key={id}
details={details} // ์ด๋ฏธ์ง ์ธ๋ถ์ ๋ณด
src={`https://picsum.photos/id/${id}/1280/768`} // original ์ด๋ฏธ์ง
thumb={`https://picsum.photos/id/${id}/50/30?blur=3`} // placeholder ์ด๋ฏธ์ง
noLazy={i < 5} // ์ฒซ ํ๋ฉด์์ ์ง์ฐ๋ก๋ฉ์ด ํ์ ์์ผ๋ฏ๋ก ์ฒ์ 5๊ฐ ์ด๋ฏธ์ง๋ ๋ฐ๋ก ๋ณด์ฌ์ค๋ค
/>
))}
<div className={`${loading ? 'visibility' : 'invisible'}`}>
๐๏ธ Fetching items...
</div>
{/* ํ๋ฉด ๊ฐ์ฅ ์๋๊น์ง ์คํฌ๋กค ํ๋์ง ํ์ธํ๊ธฐ ์ํ ๋น์์ */}
<div ref={loaderRef} className={`w-full ${data ? 'block' : 'hidden'}`} />
</section>
);
}
LazyImage ์ปดํฌ๋ํธ ์์
โthumb
props๋ฅผ ์ถ๊ฐ๋ก ๋ฐ์ผ๋ฏ๋ก ์ปดํฌ๋ํธ ํ๋ผ๋ฏธํฐ์ ์ถ๊ฐํ๊ณ , โPlaceholder ์ด๋ฏธ์ง๋ฅผ ์ํ <img>
ํ๊ทธ๋ ์ถ๊ฐํ๋ค. Placeholder / ์๋ณธ ์ด๋ฏธ์ง 2๊ฐ๋ฅผ ๋ชจ๋ ํ ์์น์์ ํ์ํ๋ฏ๋ก ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ธ๋ โdiv
์ปจํ
์ด๋์ position
์์ฑ์ relative
, โ์์ ํ๊ทธ์ธ <img>
๋ absolute
๋ก ๋ณ๊ฒฝํ๋ค. โPlaceholder ์ด๋ฏธ์ง ํฌ๊ธฐ๋ 50x30
์ ๋๋ก ์์ผ๋ฏ๋ก ๋ถ๋ชจ ์์(div
)์ ๋๋น/๋์ด ๋งํผ ํฌ๊ธฐ๋ฅผ ํค์ด๋ค.
function LazyImage({ src, noLazy, thumb, details }) { // โด
// ...
return (
<div
ref={imageRef} // IO ์ปค์คํ
ํ
์ด ๋ฐํํ ref ๊ฐ์ฒด ํ ๋น
className="relative bg-gray-200 w-4/6 aspect-[5/3] rounded-md overflow-hidden" // โถ
>
{isInView && (
<>
<img // placeholder ์ด๋ฏธ์ง โต
className="absolute w-full h-full object-covers" // โท โธ
src={thumb}
alt={details.author}
/>
<img // original ์ด๋ฏธ์ง
className={imgClasses}
src={src}
onLoad={() => setIsLoaded(true)} // ์ด๋ฏธ์ง ๋ก๋๋ฅผ ์๋ฃํ๋ฉด isLoaded ์ํ ๋ณ๊ฒฝ
alt={details.author}
/>
</>
)}
</div>
);
}
Placeholder (๋ธ๋ฌ)์ด๋ฏธ์ง → ์๋ณธ ์ด๋ฏธ์ง ์ ํ์ ์์ฐ์ค๋ฝ๊ฒ ํ๊ธฐ ์ํด transition-delay
(์ ํ ํจ๊ณผ๋ฅผ ์์ํ๊ธฐ ์ ์ ๋๊ธฐ์๊ฐ) CSS ์์ฑ๋ 200
์ผ๋ก ์ค์ ํ๋ค. ๊ทธ๋ผ ์๋ณธ ์ด๋ฏธ์ง๊ฐ ํฌ๋ช
(opacity: 0
)์์ ๋ถํฌ๋ช
(opacity: 1
)์ผ๋ก ๋ณํ๊ธฐ ์ 200ms ๋๊ธฐ ์๊ฐ์ ๊ฐ๊ฒ๋๋ค. ์ฆ, ๋ธ๋ฌ ์ด๋ฏธ์ง๊ฐ 200ms ๋์ ๋ ๋ณด์ด๋ ๊ฒ.
const imgClasses = classnames(
`absolute transition-opacity ease-in-out ${!noLazy && 'delay-200 duration-1000'}`,
// ์ด๋ฏธ์ง ์์ํ ๋ณด์ด๋ ํจ๊ณผ ('opacity-0': !isLoaded ๋ง ์
๋ ฅํด๋ ์๋ํจ)
{ 'opacity-100': isLoaded, 'opacity-0': !isLoaded },
);
function LazyImage({ src, noLazy, thumb, details }) {
const [isInView, setIsInView] = useState(!!noLazy);
const [isLoaded, setIsLoaded] = useState(false);
const imageRef = useIntersectionObserver({
callback: () => {
setIsInView(true); // ๊ด์ฐฐ ๋์์ด ํ๋ฉด์ ๋
ธ์ถ๋๋ฉด isInView ์ํ ๋ณ๊ฒฝ
},
unObserve: true, // ๊ด์ฐฐ ๋์์ด ํ๋ฉด์ ๋
ธ์ถ(๊ต์ฐจ)๋๋ฉด ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ ์ํ์ด๋ฏ๋ก ๊ด์ฐฐ ํด์
options: { rootMargin: '40%' }, // ์คํฌ๋กค์ด ํ๋จ์ ๋๋ฌํ์์ฆ์ ๋ค์ ์ด๋ฏธ์ง๋ฅผ ๋ฏธ๋ฆฌ ๋ถ๋ฌ์ค๊ธฐ ์ํด rootMargin ์ค์
});
const imgClasses = classnames(
`absolute transition-opacity ease-in-out
${!noLazy && 'delay-200 duration-1000'}`,
{
'opacity-100': isLoaded,
'opacity-0': !isLoaded,
},
);
return (
<div
ref={imageRef} // IO ์ปค์คํ
ํ
์ด ๋ฐํํ ref ๊ฐ์ฒด ํ ๋น
className="relative bg-gray-200 w-4/6 aspect-[5/3] rounded-md overflow-hidden"
// ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ธ๋ ์ปจํ
์ด๋ ์์์ ๋๋น/๋์ด๋ฅผ ์ง์ ํด์ ์ด๋ฏธ์ง ๋ก๋ฉ์ ๋ฐ๋ฆผ ํ์์ ๋ฐฉ์งํ๋ค
>
{isInView && (
<>
<img // placeholder image
className="absolute w-full h-full object-covers"
src={thumb}
alt={details.author}
/>
<img // ์๋ณธ ์ด๋ฏธ์ง
className={imgClasses}
src={src}
onLoad={() => setIsLoaded(true)} // ์ด๋ฏธ์ง ๋ก๋๋ฅผ ์๋ฃํ๋ฉด isLoaded ์ํ ๋ณ๊ฒฝ
alt={details.author}
/>
</>
)}
</div>
);
}
๋ ํผ๋ฐ์ค
- Lazy Loading Images - The Complete Guide
- ์น ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํ Image Lazy Loading ๊ธฐ๋ฒ
- Lazy Loading Images in React
- Intersection Observer API์ ์ฌ์ฉ๋ฒ๊ณผ ํ์ฉ๋ฐฉ๋ฒ
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[TS] ํ์ ์คํฌ๋ฆฝํธ - ํ์ ํธํ (0) | 2024.05.04 |
---|---|
[DevTools] ์ธ๋ถ์์ ๋ก์ปฌ ์๋ฒ ์ ์ํ๊ธฐ โ ngrok (0) | 2024.05.04 |
[React] ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํด๋ณด๋ ๋ฌดํ ์คํฌ๋กค Infinite Scroll (0) | 2024.05.04 |
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ํํ์ ํ๊ฐ ์์์ ๊ฒฐํฉ์ฑ (1) | 2024.05.03 |
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ์ ๋๋ ์ดํฐ Generator ์ด ์ ๋ฆฌ (0) | 2024.05.03 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[TS] ํ์ ์คํฌ๋ฆฝํธ - ํ์ ํธํ
[TS] ํ์ ์คํฌ๋ฆฝํธ - ํ์ ํธํ
2024.05.04 -
[DevTools] ์ธ๋ถ์์ ๋ก์ปฌ ์๋ฒ ์ ์ํ๊ธฐ — ngrok
[DevTools] ์ธ๋ถ์์ ๋ก์ปฌ ์๋ฒ ์ ์ํ๊ธฐ — ngrok
2024.05.04 -
[React] ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํด๋ณด๋ ๋ฌดํ ์คํฌ๋กค Infinite Scroll
[React] ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํด๋ณด๋ ๋ฌดํ ์คํฌ๋กค Infinite Scroll
2024.05.04 -
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ํํ์ ํ๊ฐ ์์์ ๊ฒฐํฉ์ฑ
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ํํ์ ํ๊ฐ ์์์ ๊ฒฐํฉ์ฑ
2024.05.03