๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป dev

Nextjs blurDataUrl dynamicํ•˜๊ฒŒ ์ œ๊ณตํ•˜๊ธฐ

blur์ด๋ฏธ์ง€๋กœ placeholder์ œ๊ณต

์˜ค๋žœ๋งŒ์— ํฌ์ŠคํŒ…!
์˜ค๋Š˜์€ nextjs ์ด๋ฏธ์ง€ ์ปดํฌ๋„ŒํŠธ์—์„œ blurDataUrl์„ ํ™œ์šฉํ•ด์„œ ์ด๋ฏธ์ง€๊ฐ€ ์™„์ „ํžˆ ๋กœ๋“œ๋˜๊ธฐ ์ „,
ํ๋ฆฌ๊ฒŒ ๋ณด์ด๋„๋ก ํ•˜๋Š” ๋กœ๋”๋ฅผ ํ™œ์šฉํ•ด ๋ณด๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃจ๊ณ ์ž ํ•œ๋‹ค.
์ง€๊ธˆ๊นŒ์ง€๋Š” ์ผ๋ฐ˜ img ํƒœ๊ทธ์— ์Šค์ผˆ๋ ˆํ†ค์„ ํ™œ์šฉํ•ด์„œ placeholder๋ฅผ ๊ตฌํ˜„ํ–ˆ์—ˆ๋Š”๋ฐ,
tripie๋Š” ์—ฌํ–‰ ๊ด€๋ จ ์•ฑ์ด๋ฏ€๋กœ ์‚ฌ์ง„ ์ฝ˜ํ…์ธ ๊ฐ€ ์—„์ฒญ ๋งŽ์•„ ๋ชจ๋‘ ์Šค์ผˆ๋ ˆํ†ค์œผ๋กœ ๋กœ๋“œํ•˜๊ธฐ์—๋Š” ๋ณด๊ธฐ์— ์ข‹์ง€ ์•Š์•˜๋‹ค.
๋”ฐ๋ผ์„œ ์•ž์œผ๋กœ ๋กœ๋“œ๋  ์ด๋ฏธ์ง€์˜ blur ์ฒ˜๋ฆฌ๋œ ๋ฒ„์ „์œผ๋กœ UX๋ฅผ ๋†’์ด๊ณ ์ž ํ•œ๋ฒˆ ๋‹ค๋ค„๋ณด๊ณ ์ž ํ•œ๋‹ค.

 

blur data url์„ ํ™œ์šฉํ•œ Nextjs ์ปดํฌ๋„ŒํŠธ placeholder

Blur Data Url ์ด๋ƒ ์Šค์ผˆ๋ ˆํ†ค UI๋ƒ

๋จผ์ € blur data url์ด ์Šค์ผˆ๋ ˆํ†ค ui ๋ณด๋‹ค ๋” ์ ํ•ฉํ•œ๊ฐ€ ์ฑ—์ง€ํ”ผํ‹ฐํ•œํ…Œ ๋ฌผ์–ด๋ณด์•˜๋‹ค.

โœ… Pros of Blur Data URLs

  1. Better User Experience (UX) ๐Ÿ‘€
    • Blur data URLs create a smooth loading effect, making the transition to the full image feel natural.
    • Skeleton loaders can feel jarring or disconnected, whereas blurred images give a preview of the actual content.
  2. Less Layout Shift (CLS) ๐Ÿ“
    • Using a blurred placeholder prevents Cumulative Layout Shift (CLS), improving Core Web Vitals.
    • Skeleton loaders often take up space but can still cause minor shifts when the actual image loads.
  3. Lower Bandwidth Usage ๐Ÿ“ก
    • Blur data URLs are usually very small base64-encoded images (~100-200 bytes).
    • Skeleton images require a separate request, increasing network requests and data usage.
  4. Prevents Flash of Empty Content ๐Ÿšซโฌœ
    • Instead of a gray box, users see a blurry preview of the real image, maintaining context.
  5. Faster Rendering โšก
    • Since blur data URLs are inlined in the HTML/CSS, the browser does not need a separate request.
    • Skeleton images require fetching an external file before being displayed.
  6. Improves Perceived Performance ๐Ÿš€
    • Even if the full image takes longer to load, users feel like the page is responsive because they see something instantly.

โŒ When Skeleton Images Might Be Better

  • For non-image elements (e.g., text, buttons, cards, etc.).
  • When content size is unknown, and a blurry preview wouldn’t make sense.
  • For very large images where base64 blur placeholders can slightly impact initial page load.

๐ŸŽฏ When to Use Each?

ScenarioUse Blur Data URL? Use Skeleton Loader?

Image-heavy pages (news, travel, e-commerce) โœ… Yes โŒ No
Content with user-generated images โœ… Yes โŒ No
Large text-based content (e.g., blog posts) โŒ No โœ… Yes
UI components (buttons, cards, forms) โŒ No โœ… Yes
Placeholder for API-fetched images โœ… Yes โŒ No

 

์ฆ‰, ์œ„์˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๊ณผ๋ฐ˜ ์ด์ƒ์€ ํ•ด๋‹น๋˜์—ˆ๋‹ค.

 

โ˜‘๏ธ ์ด๋ฏธ์ง€๊ฐ€ ๋งŽ์ด ํ•„์š”ํ•œ ํŽ˜์ด์ง€

โ˜‘๏ธ ์œ ์ €๊ฐ€ ์ œ๊ณตํ•˜๋Š” ์ด๋ฏธ์ง€ (static import๊ฐ€ ์•„๋‹Œ ์ด๋ฏธ์ง€๋“ค)

โ˜‘๏ธ ํ…์ŠคํŠธ๊ฐ€ ๋งŽ์ง€ ์•Š๋Š” ํŽ˜์ด์ง€

โ˜‘๏ธ  API fetch๋œ ์ด๋ฏธ์ง€์˜ placeholder๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด

 

๊ฑธ๋ฆฌ๋Š” ์ ์€ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์™€์„œ db์— ์ธ์ฝ”๋”ฉํ•œ ๊ฐ’์„ ์ €์žฅํ•˜๋ฉด ์ถ”๊ฐ€์ ์ธ ์š”์ฒญ์ด ๋ถˆํ•„์š”ํ•˜์ง€ ์•Š์„๊นŒ?


Blur Data Url ์ ์šฉํ•ด ๋ณด๊ธฐ

๋จผ์ € ๊ณต์‹๋ฌธ์„œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด Next ์ด๋ฏธ์ง€ ์ปดํฌ๋„ŒํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.

import Image from 'next/image'
 
export default function Page() {
  return (
    <Image
      src="/profile.png"
      width={500}
      height={500}
      alt="Picture of the author"
      placeholder={'empty'}
    />
  )
}

 

placeholder ์—๋Š”'empty' // "empty" | "blur" | "data:image/..." ๊ฐ’ ๋“ฑ์ด ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ๋Š”๋ฐ,

blur๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” blurDataURL ์†์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•˜๋Š”๋ฐ,  data:image/...์™€ ๊ฐ™์ด base64๋กœ ์ธ์ฝ”๋”ฉ ๋œ ๊ฐ’์ด ํ•„์ˆ˜๋‹ค.

 

๋งŒ์•ฝ src๊ฐ€ static Import์ธ. jpg,. png,. webp ๋˜๋Š” .avif๋กœ ๋œ ๊ฐ์ฒด๋ผ๋ฉด ์ž๋™์ ์œผ๋กœ ๊ฐ€์ ธ์™€์ง€์ง€๋งŒ, 

dynamic ์ด๋ฏธ์ง€์ธ ๊ฒฝ์šฐ, base24 ์ธ์ฝ”๋”ฉ์ด ํ•„์ˆ˜๋‹ค.

 

ํ•ด๋‹น ํฌ์ŠคํŒ…์€ nextjs(^15.0.3) app router์„ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ๋‹ค.

 

src/api/blur-image.ts์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ธ์ฝ”๋”ฉ ์ž‘์—…์„ ๋‹ค๋ฃฐ ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ด ์ฃผ์—ˆ๋‹ค.

import API from 'constants/api-routes';
import { NextResponse } from 'next/server';

// https://medium.com/@kavindumadushanka972/learn-how-to-create-dynamic-blur-data-urls-for-images-in-next-js-bc4eb5d04ec6 ์ฐธ๊ณ 
export async function GET(request: NextResponse) {
  const { searchParams } = new URL(request.url);
  const url = searchParams.get('url') as string;

  const base64str = await fetch(`${API.BASE_URL}/_next/image?url=${url}&w=16&q=75`).then(async res =>
    Buffer.from(await res.arrayBuffer()).toString('base64')
  );

  const blurSvg = `
  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 5'>
    <filter id='b' color-interpolation-filters='sRGB'>
      <feGaussianBlur stdDeviation='1' />
    </filter>
    <image preserveAspectRatio='none' filter='url(#b)' x='0' y='0' height='100%' width='100%' 
    href='data:image/avif;base64,${base64str}' />
  </svg>
`;

  const toBase64 = (str: string) =>
    typeof window === 'undefined' ? Buffer.from(str).toString('base64') : window.btoa(str);

  return NextResponse.json({ data: `data:image/svg+xml;base64,${toBase64(blurSvg)}` }, { status: 200 });
}

 

API.BASE_URL ์€. env์—  ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์„ค์ •ํ•ด ์ค€ ์›น์‚ฌ์ดํŠธ URL ์ƒ์ˆ˜๊ฐ’ (dev ํ™˜๊ฒฝ์ผ ๋•Œ๋Š” localhost//...)์ด๋‹ค.

w์€ width๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์ฆ‰, ์ด๋ฏธ์ง€๊ฐ€ ํ”ฝ์…€ ๋‹จ์œ„๋กœ ๋„“์ด๊ฐ€ ์–ผ๋งˆ์ธ์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด, 16w์ธ ๊ฒฝ์šฐ 16 ํ”ฝ์…€์˜ ์ด๋ฏธ์ง€๋กœ ์„ค์ •ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

q๋Š” quality๋ฅผ  ๋‚˜ํƒ€๋‚ด๋Š”๋ฐ, ์ด๋ฏธ์ง€๊ฐ€ ์–ผ๋งˆ๋‚˜ ์••์ถ•๋˜์–ด ํ™”์งˆ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. q๊ฐ’์ด ํด์ˆ˜๋ก ๋” ํ™”์งˆ์ด ์ข‹์•„์ง€์ง€๋งŒ ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์‚ฌ์ด์ฆˆ๊ฐ€ ์ปค์ง„๋‹ค. ๋ฐ˜๋Œ€๋กœ, ๋‚ฎ์€ ๊ฐ’์ผ์ˆ˜๋ก ์ด๋ฏธ์ง€ ํŒŒ์ผ ์‚ฌ์ด์ฆˆ๋Š” ์ž‘์•„์ง€์ง€๋งŒ ์ด๋ฏธ์ง€์˜ ์งˆ ๋˜ํ•œ ๋‚ฎ์•„์ง„๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด q=75์ธ ๊ฒฝ์šฐ ์ด๋ฏธ์ง€์˜ ํ€„๋ฆฌํ‹ฐ๋Š” 75%๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

w์™€ q ๊ฐ’์„ ์ตœ์†Œ๋กœ ์„ค์ •ํ•  ๊ฒฝ์šฐ ๋กœ๋“œ๊ฐ€ ๊ฐ€์žฅ ๋นจ๋ผ์ง„๋‹ค. ๋‹ค๋งŒ ์ด๋•Œ, blur ์ฒ˜๋ฆฌ๋œ ์ด๋ฏธ์ง€๋ฅผ ๋ณด์—ฌ์ค„ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ํ™”์งˆ์ด ๋‚˜๋น ์ง€๋Š” ๊ฑด ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š๋Š”๋‹ค.

 

ํŠธ๋ฆฌํ”ผ ์•ฑ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ์–ด์™”๋Š”๋ฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด img url 'https://media.triple.guide/triple-cms/c_fill,f_auto,h_256,w_256/54e3594d-1087-43ff-b760-7231d4103edf.jpeg'๋ฅผ ํ•ด๋‹น ํ•จ์ˆ˜๋กœ ๋Œ๋ฆฌ๋ฉด 'data:image/svg+xml;base64,CiAg....' ๊ฐ’์ด ๋˜์–ด ์˜์ƒ๊ณผ ๊ฐ™์ด ๋กœ๋”ฉ ์ค‘ blur ์ฒ˜๋ฆฌ๋˜์–ด ์ œ๊ณต๋œ๋‹ค๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

์•ฑ์—์„œ ํ™œ์šฉ๋˜๋Š” ๋ชจ์Šต์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

https://tripie-mauve.vercel.app/regions/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD/location/759174cc-0814-4400-a420-5668a0517edd

 

Tripie โœˆ๏ธ

ํ†ต์˜·๊ฑฐ์ œ·๋‚จํ•ดํ†ต์˜·๊ฑฐ์ œ·๋‚จํ•ด

tripie-mauve.vercel.app

 

๐Ÿค” ์—ฌ๊ธฐ๊นŒ์ง€๋Š” ์ˆ˜์›”ํ–ˆ๋Š”๋ฐ, ๋กœ๋“œ๋˜๋Š” ์‹œ๊ฐ„์ด ๋„ˆ๋ฌด ๊ธธ์–ด์ง„ ๊ฑฐ ๊ฐ™์•„์„œ ์•„์‰ฌ์› ๋‹ค. 

ํ˜„์žฌ๋Š” ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ url๋งŒ ์ €์žฅํ–ˆ๋Š”๋ฐ, ์ธ์ฝ”๋”ฉํ•œ ๊ฐ’๋„ ๊ฐ™์ด ์ €์žฅํ•ด ์ฃผ๋ฉด ์ด ์š”์ฒญ์„ ํ•˜๋Š” ์‹œ๊ฐ„์„ ๋‹จ์ถ•ํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ ์‹ถ์–ด์„œ ์ €์žฅํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. firebase์— ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ์‹ถ์—ˆ์œผ๋‚˜, ๊ฐ์ฒด๋ฅผ ์ค‘์ฒฉ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์ ์—์„œ ๋ชจ๋“  blurDataURL์„ ์ €์žฅํ•  ์ˆ˜ ์—†์—ˆ๋‹ค. (ํ˜„์žฌ๋Š” JSON์„ ๋ฌธ์ž๋กœ parseํ•œ ๊ฐ’์„ ํ†ต์œผ๋กœ ์ €์žฅํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ๋ฌธ์ž์—ด์˜ ํฌ๊ธฐ ๋˜ํ•œ ๋„ˆ๋ฌด ์ปค์ ธ์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฅธ ํƒ€์ž…์œผ๋กœ ์ €์žฅํ•ด์•ผ ํ•  ๊ฑฐ ๊ฐ™๋‹ค...).

 

๐Ÿ˜€ ๊ทธ๋ž˜๋„ ์Šค์ผˆ๋ ˆํ†ค๋งŒ ๋ฉ๊ทธ๋Ÿฌ๋‹ˆ ๋ณด์ด๋Š” ํŽ˜์ด์ง€๋ณด๋‹ค๋Š” ๋ธ”๋Ÿฌ๋œ ์ด๋ฏธ์ง€๋ผ๋„ ๋ณด์—ฌ์„œ ๋ญ”๊ฐ€ ์—†์–ด ๋ณด์ด๋Š” ๋А๋‚Œ์ด ๋œํ•˜๊ธด ํ•˜๋‹ค.

 

 


Reference

https://medium.com/@kavindumadushanka972/learn-how-to-create-dynamic-blur-data-urls-for-images-in-next-js-bc4eb5d04ec6

 

Learn how to create dynamic blur data URLs for images in Next.js.

A guide to creating a dynamic blurDataURL property encoded in base64 for Next.js Image.

medium.com