[Next.js] Next/Image base64 placeholder ๋ง๋ค๊ธฐ (๋ธ๋ฌ ์ฒ๋ฆฌ๋ ํ๋ ์ด์คํ๋)
Next/Image๋ ํฌ๊ฒ ๋ก์ปฌ ์ด๋ฏธ์ง(์ ์ ์ด๋ฏธ์ง)์ ๋ฆฌ๋ชจํธ ์ด๋ฏธ์ง(๋ค์ด๋๋ฏน ์ด๋ฏธ์ง)๋ก ๋๋๋ค. /public
ํด๋์ ์ ์ฅํ ๋ก์ปฌ ์ด๋ฏธ์ง๋ ๋น๋ ํ์์ importํ ์ด๋ฏธ์ง ํ์ผ์ width
, height
๋ฅผ ์๋์ผ๋ก ์ง์ ํ๊ณ base64๋ก ์ธ์ฝ๋ฉํ ์ด๋ฏธ์ง๊ฐ ์์ฑ๋๋ค. ๋ฐ๋ผ์ ์ถ๊ฐ ์์
์์ด ๋ธ๋ฌ ์ฒ๋ฆฌ๋ Placeholder๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
// public ํด๋์ ์๋ me.png ํ์ผ์ ์ฌ์ฉํ๊ณ ์๋ค
<Image
src="/me.png"
alt="Picture of the author"
placeholder="blur"
// width={500} automatically provided
// height={500} automatically provided
// blurDataURL="data:..." automatically provided
/>;
๊ทธ ์ธ ์ํฉ์ ๋ฆฌ๋ชจํธ ์ด๋ฏธ์ง๋ก ๊ตฌ๋ถํ๋ค. ์ด๋ ๋ธ๋ฌ ์ฒ๋ฆฌ๋ Placeholder๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด plaiceholder ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ ์บ๋ฒ์ค API๋ฅผ ์ด์ฉํด์ 4×4 ์ ๋์ ์ฌ์ด์ฆ(๋ณดํต 300๋ฐ์ดํธ ๋ฏธ๋ง)๋ก ์ค์ธ ํ base64๋ก ๋ณํํ๋ ์์ ์ด ํ์ํ๋ค. NextJS ๊ณต์ ๋ฌธ์์์ 10 ํฝ์ ๋ฏธ๋ง์ ์ฌ์ด์ฆ๋ฅผ ๊ถ์ฅํ๊ณ ์๋ค.
๋ฐฉ๋ฒ 1. Canvas ํ์ฉ
์ฌ์ฉ์ ์ปดํจํฐ์์ ์ด๋ฏธ์ง๋ฅผ ์ ํํ ํ ์ ๋ก๋ํ ๊ฒฝ์ฐ์ ์ ํธ ํจ์๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ ์ ์๋ค. ์ด๋ฏธ์ง ํฌ๋กญ ๋ฑ์ ์ํฉ์์ ์ฌ์ฉํ๋ฉด ์ ์ฉํ๋ค. โถ๋ก์ปฌ ์ปดํจํฐ์์ ์ด๋ฏธ์ง ์ ํ โ์ด๋ฏธ์ง ํฌ๋กญ โPlaceholder๋ก ์ฌ์ฉํ base64 ๋ฌธ์์ด์ ์์ฑํ๊ณ , ํฌ๋กญ ์ด๋ฏธ์ง ์๋ณธ์ ์๋ฒ๋ก ์ ์ก โนํฌ๋กญ ์ด๋ฏธ์ง ๋ ๋.
s
๋ source(์ด๋ฏธ์ง), d
๋ destination(์บ๋ฒ์ค)์ ์๋ฏธํ๋ค. drawImage
๋ฉ์๋์ ๋๊ธด ํ๋ผ๋ฏธํฐ ๊ฐ์์ ๋ฐ๋ผ ์ฌ์ฉ๋ฒ์ด ๋ฌ๋ผ์ง๋ฏ๋ก ์ฃผ์ํ ๊ฒ. ์ฐธ๊ณ ๋ก source ์๋ฆฌ๋จผํธ๋ฅผ ๋๊ธฐ๋ ์ฒซ๋ฒ์งธ ์ธ์์ SVG ๊ฐ์ ์ด๋ฏธ์ง๋ ๋ฌผ๋ก ๋น๋์ค๋ ์บ๋ฒ์ค ์๋ฆฌ๋จผํธ๋ ๊ฐ๋ฅํ๋ค. — MDN
drawImage(image, dx, dy)
drawImage(image, dx, dy, dw, dh)
drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
// utils.js
export const toDataURL = (
img: HTMLImageElement,
width: number,
height: number,
) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('No 2d context');
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// 1๋ฒ์งธ ์ธ์ : HTMLImageElement, SVGImageElement ๋ฑ ์ด๋ฏธ์ง ์์ค ์๋ฆฌ๋จผํธ
// 2๋ฒ์งธ ์ธ์(dx) : ์บ๋ฒ์ค์ ๊ทธ๋ฆด x์ถ ์ขํ
// 3๋ฒ์งธ ์ธ์(dy) : ์บ๋ฒ์ค์ ๊ทธ๋ฆด y์ถ ์ขํ
// 4๋ฒ์งธ ์ธ์(dw) : ์บ๋ฒ์ค์ ๊ทธ๋ฆด width
// 5๋ฒ์ฌ ์ธ์(dy) : ์บ๋ฒ์ค์ ๊ทธ๋ฆด height
return canvas.toDataURL(); // ๋ฆฌ์ฌ์ด์ฆํ ์ด๋ฏธ์ง๋ฅผ base64(data URL) ๋ฌธ์์ด๋ก ๋ณํ
};
// ์ด๋ฏธ์ง ์๋ฆฌ๋จผํธ๋ฅผ ๋ฐ์ 4×4๋ก ๋ฆฌ์ฌ์ด์ฆํ base64 ๋ฌธ์์ด์ ๋ฐํํ๋ ํจ์
export const getBlurDataURL = (img: HTMLImageElement) => {
return toDataURL(img, 4, 4);
};
๐ก URL.createObjectURL
๋์ FileReader API๋ฅผ ์ฌ์ฉํ ์๋ ์๋ค(์ฐธ๊ณ ํฌ์คํ
)
// <input type="file" ... /> ์๋ฆฌ๋จผํธ์ onChange ํธ๋ค๋ฌ
const image = e.target.files?.[0];
if (image) {
const blobUrl = URL.createObjectURL(image);
const img = new Image();
img.src = blobUrl;
img.onload = () => {
const base64 = getBlurDataURL(img); // "data:image/png;base64,iVBw...
URL.revokeObjectURL(blobUrl); // ์ด๋ฏธ์ง ๋ก๋๋ฅผ ์๋ฃํ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง๋ฅผ ์ํด ํ๊ธฐ
// ...์ํ๋ ์์
์ํ
// base64 ๋ฌธ์์ด์ Next/Image์ blurDataURL ์์ฑ์ ์ฌ์ฉํ๋ค
};
}
๋ฐฉ๋ฒ2. Plaiceholder ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ฉ
๐ก TailwindCSS ์ ํธ๋ฆฌํฐ ํด๋์ค ํํ๋ก ์ฌ์ฉํ ์ ์๋ @plaiceholder/tailwindcss ํ๋ฌ๊ทธ์ธ๋ ์๋ค
plaiceholder ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ LQIP(์ ํ์ง ์ด๋ฏธ์ง) ์์ฑ์ ๋์์ฃผ๋ NodeJS ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค. Base64, SVG ๋ฑ ํฌ๋งท์ผ๋ก ์์ฑํ ์ ์๋ค. Next/Image์ blurDataURL
์์ฑ์ ์ฌ์ฉํ๋ ค๋ฉด Base64๋ก ์์ฑํ๋ฉด ๋๋ค. ๊ณต์ ๋ฌธ์์ ๋ฐ๋ฅด๋ฉด Base64 ํฌ๋งท์ ์ผ๋ฐ์ ์ผ๋ก ~300 Bytes ๋ฏธ๋ง ์ฌ์ด์ฆ๋ก ์์ฑ๋๋ค๊ณ ํ๋ค.
ํจํค์ง ์ค์น
npm install plaiceholder @plaiceholder/next sharp
์ด๊ธฐ ์ธํ
// next.config.js (withPlugins ํ๋ฌ๊ทธ์ธ์ ์ฌ์ฉํ์ง ์๋ ๊ฒฝ์ฐ)
const { withPlaiceholder } = require('@plaiceholder/next');
module.exports = withPlaiceholder({
images: { domains: ['images.unsplash.com'] },
// ...NextJS configs
});
// next.config.js (withPlugins ํ๋ฌ๊ทธ์ธ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ)
const withPlugins = require('next-compose-plugins');
const { withPlaiceholder } = require('@plaiceholder/next');
const nextConfig = {
images: { domains: ['images.unsplash.com'] },
// ...NextJS configs
};
module.exports = withPlugins([withPlaiceholder, ...], {
...nextConfig,
// ...
});
๊ธฐ๋ณธ ์ฌ์ฉ ๋ฐฉ๋ฒ
๐ก plaiceholder๋ NodeJS ํ๊ฒฝ์์๋ง ์๋ํ๋ค.
plaiceholder ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ NodeJS ํ๊ฒฝ์์๋ง ์๋ํ๋ฏ๋ก getStaticProps
, getServerSideProps
๋ฑ์์ ์ฌ์ฉํด์ผ ํ๋ค. getPlaiceholder
์ฒซ๋ฒ์งธ ์ธ์์ ์ ์ ์ด๋ฏธ์ง ๊ฒฝ๋ก(public
ํด๋ ๊ธฐ์ค) ํน์ ๋ฆฌ๋ชจํธ ์ด๋ฏธ์ง ๊ฒฝ๋ก๋ฅผ ์
๋ ฅํ๋ค. ๋๋ฒ์งธ ์ต์
์ธ์์ dir
(์ ์ ์์
ํด๋ ์ง์ , ๊ธฐ๋ณธ๊ฐ ./public
), size
(๊ธฐ๋ณธ๊ฐ 4
) brightness
๋ฑ์ ์ต์
์ ์ง์ ํ ์ ์๋ค.
getPlaiceholder(src, options?)
- ์ ์ ์ด๋ฏธ์ง ๊ฒฝ๋ก ์์ :
getPlaiceholder("/images/me.png")
→ public/images/me.png (dir
์ต์ ์ ๋ณ๊ฒฝํ์ง ์์๋ค๋ฉด ์ฒ์/
๋public
ํด๋๋ฅผ ๊ฐ๋ฆฌํจ๋ค) - ๋ฆฌ๋ชจํธ ์ด๋ฏธ์ง ๊ฒฝ๋ก ์์ :
getPlaiceholder("images.unsplash.com/...")
getPlaiceholder
ํจ์๋ css
, svg
, base64
, blurhash
, img
๋ค์ฏ ์ ํ์ ๊ฐ์ ๋ฐํํ๋ค. img
๋ ์ด๋ฏธ์ง ํ๊ทธ์ ํ์ํ ์ดํธ๋ฆฌ๋ทฐํธ๋ฅผ ํฌํจ(src
, width
, height
, type
)ํ๊ณ ์๋ค. base64
๋ฐํ๊ฐ์ Image ์ปดํฌ๋ํธ์ blurDataURL
์์ฑ์ ์ฌ์ฉํ๋ฉด ๋๋ค.
// ๊ณต์ ๋ฌธ์ ์ฝ๋ ์ฐธ๊ณ
import type { InferGetStaticPropsType } from 'next';
import { getPlaiceholder } from 'plaiceholder';
export const getStaticProps = async () => {
const { base64, img } = await getPlaiceholder('/path-to-your-image.jpg');
// img -> { src: '...', width: 382, height: 382, type: 'png' }
// base64 -> "data:image/png;base64,iVBw...
return {
props: {
imageProps: { ...img, blurDataURL: base64 },
},
};
};
export default function ImageList({
imageProps,
}: InferGetStaticPropsType<typeof getStaticProps>) {
// ...
return (
<div>
<Image {...imageProps} placeholder="blur" />
</div>
);
}
๋ธ๋ผ์ฐ์ ํ๊ฒฝ์์ ์ฌ์ฉํ๊ธฐ
plaiceholder๋ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์์ ์ฌ์ฉํ ์ ์๋ค. ๋์ ์๋์ฒ๋ผ Base64 ๋ณํ ์์ ์ ์ํํ๋ NextJS API Route๋ฅผ ๋ง๋ค์ด๋๊ณ ๋ธ๋ผ์ฐ์ ์์ NextJS API๋ฅผ ํธ์ถํ๋ ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ ์ ์๋ค. (์ฐธ๊ณ ๊ธ)
// pages/api/get-base64.ts
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<string>,
) {
const { body } = req;
const { url } = body;
try {
const { base64 } = await getPlaiceholder(url);
res.status(200).send(base64);
} catch (e) {
if (e instanceof Error) res.status(500).send(e.message);
// ...
}
}
// utils.ts
export const getBase64 = async (url: string) => {
return await axios.post<string>('/api/get-base64', { url });
};
๋ ํผ๋ฐ์ค
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[Next.js] ๋ผ์ฐํธ ๋ณ๊ฒฝ / ์๋ก๊ณ ์นจ ์ทจ์ํ๊ธฐ (๋ค๋น๊ฒ์ด์ ๊ฐ๋)
[Next.js] ๋ผ์ฐํธ ๋ณ๊ฒฝ / ์๋ก๊ณ ์นจ ์ทจ์ํ๊ธฐ (๋ค๋น๊ฒ์ด์ ๊ฐ๋)
2024.05.14 -
[JS] API ์์ฒญ / ๋น๋๊ธฐ ์์ ์ทจ์ํ๊ธฐ - Abort Controller
[JS] API ์์ฒญ / ๋น๋๊ธฐ ์์ ์ทจ์ํ๊ธฐ - Abort Controller
2024.05.14 -
[DevTools] Tailwind CSS ์ ํธ๋ฆฌํฐ ํด๋์ค ์๋ ์ ๋ ฌ ํ๋ฌ๊ทธ์ธ
[DevTools] Tailwind CSS ์ ํธ๋ฆฌํฐ ํด๋์ค ์๋ ์ ๋ ฌ ํ๋ฌ๊ทธ์ธ
2024.05.13 -
[JS] ์ ๋ก๋ ์งํ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ onUploadProgress
[JS] ์ ๋ก๋ ์งํ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ onUploadProgress
2024.05.13