[Next.js] ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋งํฌ์˜ ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ

2025. 11. 17. 00:33ใ†Trouble Shooting & Issues/Linkiving

๋ฐ˜์‘ํ˜•

๋ชฉ์ฐจ

1. ์ƒํ™ฉ

2. og ํ™œ์šฉํ•˜๊ธฐ

3. ๋” ์ข‹๊ฒŒ ๋งŒ๋“ค๊ธฐ


1. ์ƒํ™ฉ

๊ธฐ์กด ์ œ์ž‘ํ•ด๋‘” BookmarkCard ์ปดํฌ๋„ŒํŠธ

์นด๋“œ ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ถ๋งˆํฌํ•œ ๋งํฌ์˜ url๋กœ๋ถ€ํ„ฐ ์ธ๋„ค์ผ์„ ๊ฐ€์ ธ์™€ ๊ฐ™์ด ๋ณด์—ฌ์ฃผ๋Š” ์—ญํ• ์„ ๊ฐ€์ง„๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๊ธฐ์กด ์ฝ”๋“œ์—์„œ๋Š” ์ผ๋‹จ ์ž„์˜๋กœ ์ด๋ฏธ์ง€ URL์„ ์ง๋นต์œผ๋กœ ๋„ฃ์–ด์„œ ์ด๊ฑธ url๋กœ๋ถ€ํ„ฐ ๊ฐ€์ ธ์˜ค๋„๋ก ๋ฐ”๊ฟ”์ฃผ์–ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด๋‹ค.

์ง€๊ธˆ์€ ๊ทธ๋ƒฅ ๋‹จ์ˆœํ•˜๊ฒŒ props๋กœ imageUrl์„ ๋ฐ›์•„์„œ public์— ์žˆ๋Š” ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ globe.svg๋ฅผ ๋ฐ”๋กœ ๋„ฃ๊ณ  ์žˆ๋‹ค.

 

 


2. cheerio์™€ og ์‚ฌ์šฉํ•˜๊ธฐ

1. API Route์˜ GET ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜ ์ •์˜

// src/app/api.og-thumbnail/route.ts
import * as cheerio from 'cheerio';
import { NextResponse } from 'next/server';

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const targetUrl = searchParams.get('url');

  if (!targetUrl) {
    return NextResponse.json({ error: 'url is required' }, { status: 400 });
  }

  try {
    const html = await fetch(targetUrl).then(res => res.text());
    const $ = cheerio.load(html);

    const ogImage =
      $('meta[property="og:image"]').attr('content') ||
      $('meta[name="twitter:image"]').attr('content');

    return NextResponse.json({ image: ogImage || null });
  } catch (e) {
    return NextResponse.json({ image: null });
  }
}

์™ธ๋ถ€ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด์„œ ์ž‘์„ฑํ•œ ํŒŒ์ผ์ด๋‹ค. ํ•˜๋‚˜ํ•˜๋‚˜ ์‚ดํŽด๋ณด์ž.

 

import * as cheerio from 'cheerio';
import { NextResponse } from 'next/server';
  • cheerio: Next ์„œ๋ฒ„(Node.js)์—์„œ HTML์„ ํŒŒ์‹ฑํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” npm ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ,
    HTML DOM์„ jQuery์ฒ˜๋Ÿผ ํƒ์ƒ‰ํ•˜๊ณ  ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค. (OG ํƒœ๊ทธ ์ถ”์ถœ์šฉ)
  • NextResponse: Next.js App Router์—์„œ API Router์˜ ์‘๋‹ต์„ ์‰ฝ๊ฒŒ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•œ๋‹ค.

 

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const targetUrl = searchParams.get('url');
  • Next.js์˜ App Router์—์„œ๋Š” app/api/ํด๋”๋ช…/route.ts ๋‚ด๋ถ€์— ์œ„์™€ ๊ฐ™์€ ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜๋ฉด api/ํด๋”๋กœ ๋“ค์–ด์˜ค๋Š” GET ์š”์ฒญ์„ ์ž๋™์œผ๋กœ ์ด ํ•จ์ˆ˜๊ฐ€ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋œ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์„œ๋ฒ„์ธก ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ •์˜ํ•œ๋‹ค. api/og-thumbnail๊ฐ™์€ URL์— GET/POST ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์„œ๋ฒ„์—์„œ ์ด ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜์–ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • Node.js์˜ ๋‚ด์žฅ ๊ฐ์ฒด URL(์ƒ์„ฑ์ž)์„ ์‚ฌ์šฉํ•ด์„œ ์š”์ฒญ์œผ๋กœ ๋ฐ›์€ req์—์„œ url์„ ๋ฐ›์•„์˜ค๊ณ , ํ•ด๋‹น url์—์„œ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ(url)์˜ ๊ฐ’์„ ๋ฐ›์•„์˜จ๋‹ค.
    ex) /api/og-thumbnail?url='https://naver.com  →  targetUrl = 'https://naver.com'
    ๊ณผ ๊ฐ™์ด req๋กœ ๋ฐ›์€ url์—์„œ url ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ’์„ ๋ฝ‘์•„๋‚ธ๋‹ค.

 

if (!targetUrl) {
    return NextResponse.json({ error: 'url is required' }, { status: 400 });
  }

 

๋งŒ์•ฝ ์„œ๋ฒ„์—์„œ fetchํ•  URL์ด ์—†์œผ๋ฉด 400 Bad Request๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ฒ˜๋ฆฌ๋ฅผ ์ค‘๋‹จํ•œ๋‹ค.

 

 

try {
    const html = await fetch(targetUrl).then(res => res.text());
    const $ = cheerio.load(html);

    const ogImage =
      $('meta[property="og:image"]').attr('content') ||
      $('meta[name="twitter:image"]').attr('content');

    return NextResponse.json({ image: ogImage || null });
  } catch (e) {
    return NextResponse.json({ image: null });
  }

ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” CORS ๋ฌธ์ œ๋กœ ์™ธ๋ถ€ HTML์„ fetchํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— next ์„œ๋ฒ„์—์„œ ํ”„๋ก์‹œ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

HTML๋ฌธ์„œ(res) ์ „์ฒด๋ฅผ ๋ฌธ์ž์—ด๋กœ ๊ฐ€์ ธ์™€, cheerio๋กœ ํŒŒ์‹ฑํ•œ๋‹ค.

OG ํƒœ๊ทธ, twitter ํƒœ๊ทธ๋ฅผ ๋จผ์ € ํƒ์ƒ‰ํ•˜๊ณ , ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์œผ๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

์˜ˆ์™ธ ์ƒํ™ฉ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์—ญ์‹œ null์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

** ํ”„๋ก์‹œ : ํด๋ผ์ด์–ธํŠธ์™€ ์™ธ๋ถ€ ์„œ๋ฒ„ ์‚ฌ์ด์—์„œ ์ค‘๊ณ„ ์—ญํ• ์„ ํ•˜๋Š” ์„œ๋ฒ„

 

2. useThumbnail ์ปค์Šคํ…€ ํ›… ์ •์˜

string ํƒ€์ž…์˜ link๋ฅผ props๋กœ ๋ฐ›์•„์„œ ์ธ๋„ค์ผ์„ ๋กœ๋“œํ•˜๋Š” ํ›…, useThumbnail์„ ์ •์˜ํ•œ๋‹ค.

'use client';

import { useEffect, useState } from 'react';

const thumbnailCache = new Map<string, string | null>();
const defaultImage = '/file.svg';

interface ThumbnailState {
  thumbnail: string | null;
  loading: boolean;
  error: boolean;
}

export function useThumbnail(link: string) {
  const [state, setState] = useState<ThumbnailState>({
    thumbnail: null,
    loading: true,
    error: false,
  });
  •  thumbnailCache: ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด ์ธ๋„ค์ผ ์บ์‹œ๋ฅผ ์ €์žฅํ•  thumbnailCache๋ฅผ ์ •์˜ํ–ˆ๋‹ค.
  • defaultImage: ์ธ๋„ค์ผ์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์„ ๋•Œ ๋„์šธ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€์ด๋‹ค. ์•„์ง ์ œ์ž‘์ด ์•ˆ๋˜์–ด์„œ ์ž„์˜๋กœ next ๊ธฐ๋ณธ ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ์—ˆ๋‹ค.
  • useState: ํด๋ผ์ด์–ธํŠธ๊ฐ€ API ํ˜ธ์ถœ์„ ํ†ตํ•ด ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์— useState๋กœ ์ธ๋„ค์ผ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์ฃผ๊ณ , loading๊ณผ error ์ƒํƒœ๋„ ๊ฐ™์ด ๊ด€๋ฆฌํ•ด์ค€๋‹ค.
    ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋‹ค๋ณด๋‹ˆ thumbnail, loading, error๋ฅผ ํ•œ๊บผ๋ฒˆ์— ๊ด€๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„ state๋กœ ๋ฌถ์–ด์„œ ์ •์˜ํ–ˆ๋‹ค.

 

useEffect(() => {
    if (!/^https?:\/\//i.test(link)) {
      setState({ thumbnail: defaultImage, loading: false, error: false });
      return;
    }

    // check cache
    if (thumbnailCache.has(link)) {
      setState({
        thumbnail: thumbnailCache.get(link) || defaultImage,
        loading: false,
        error: false,
      });
      return;
    }

    let isMounted = true;
  • if๋ฌธ: ๋งํฌ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, defaultImage๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค.
  • check cache: ๋งํฌ๊ฐ€ ์œ ํšจํ•  ๊ฒฝ์šฐ, ๊ฐ€์žฅ ๋จผ์ € ์บ์‹œ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ•˜์—ฌ ์žˆ์„ ๊ฒฝ์šฐ, ํ•ด๋‹น ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค.
  • isMounted: ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋œ ํ›„์—๋„ setState ํ˜ธ์ถœ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ํ”Œ๋ž˜๊ทธ๋กœ,
    ๋น„๋™๊ธฐ fetch๊ฐ€ ๋Šฆ๊ฒŒ ๋๋‚˜๋”๋ผ๋„ ์ด๋ฏธ ์–ธ๋งˆ์šดํŠธ๋œ ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.

 

async function load() {
      setState(prev => ({ ...prev, loading: true, error: false }));
      try {
        const res = await fetch(`/api/og-thumbnail?url=${encodeURIComponent(link)}`);
        if (!res.ok) throw new Error('Failed to fetch thumbnail');

        const data = await res.json();
        const imageUrl = data.image || defaultImage;

        // save to cache
        thumbnailCache.set(link, imageUrl);

        if (isMounted) {
          setState({ thumbnail: imageUrl, loading: false, error: false });
        }
      } catch {
        if (isMounted) {
          setState({ thumbnail: defaultImage, loading: false, error: true });
        }
      }
    }

๋‹ค์Œ์œผ๋กœ๋Š” ์ด์ „ ๋‹จ๊ณ„์—์„œ og-thumbnail์—์„œ ์ •์˜ํ•œ GETํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธ๋„ค์ผ์„ ๋กœ๋“œํ•œ๋‹ค.

  • encodeURIComponent: URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์•ˆ์ „ํ•˜๊ฒŒ ๋„ฃ๊ธฐ ์œ„ํ•ด ํŠน์ˆ˜๋ฌธ์ž๋ฅผ ์ธ์ฝ”๋”ฉํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค.
  1. res: ์„œ๋ฒ„ API(/api/og-thumbnail) ํ˜ธ์ถœ
  2. data: ์‘๋‹ต ์ƒํƒœ ํ™•์ธ ๋ฐ JSON ํŒŒ์‹ฑ
  3. imageURL: OG ์ด๋ฏธ์ง€ URL ์ถ”์ถœ
  4. ์„ฑ๊ณต ์‹œ, ์บ์‹œ ์ €์žฅ ๋ฐ ์‹คํŒจ์‹œ fallback ์ด๋ฏธ์ง€ ์„ค์ •
    load();

    return () => {
      isMounted = false;
    };
  }, [link]);

  return state;
}

load() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , isMounted๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค.

ํ›…์—์„œ ์ตœ์ข… ๋ฆฌํ„ดํ•˜๋Š” ๊ฒƒ์€ ๊ฐ€์žฅ ์ฒ˜์Œ ์ •์˜ํ–ˆ๋˜ state(thumbnail, loading, error)๊ฐ€ ๋œ๋‹ค.

 

3. next.config.ts ์„ธํŒ… ๋ฐ ํ›… ํ™œ์šฉ

next.js๋Š” ์ด๋ฏธ์ง€ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด์„œ ์™ธ๋ถ€์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ ๊ฒฝ์šฐ, ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฏธ๋ฆฌ ๋งํ•ด๋‘” ๋งํฌ์˜ ์ด๋ฏธ์ง€๋งŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

์šฐ๋ฆฌ ์„œ๋น„์Šค์—์„œ ์–ด๋–ค ๋งํฌ๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ๋ถ๋งˆํฌํ•  ์ง€ ๋ชจ๋ฅด๋ฏ€๋กœ, ํ•˜๋‚˜ํ•˜๋‚˜ ์ถ”๊ฐ€ํ•˜๊ธฐ์—๋Š” ๋„ˆ๋ฌด ๋งŽ์€ ๊ฒƒ ๊ฐ™์•„์„œ ์šฐ์„  http, https๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋งํฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ธํŒ…ํ•ด์ค€๋‹ค.

// next.config.ts

images: {
    remotePatterns: [
      {
        protocol: 'http',
        hostname: '**',
      },
      {
        protocol: 'https',
        hostname: '**',
      },
    ],
  },

 

 

ํ›… ํ™œ์šฉ

const thumbnail = useThumbnail(link); // url์—์„œ ์ธ๋„ค์ผ ๋ฝ‘๋Š” ํ›…

์ด๋ ‡๊ฒŒ thumbnail์„ ํ›…์„ ์‚ฌ์šฉํ•ด์„œ ์ •์˜ํ•˜๊ณ , ์ด๋ฏธ์ง€ ๋งํฌ๋ฅผ ๋„ฃ์–ด์•ผ ํ•˜๋Š” ๊ณณ์— ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

 

 


3. ๋” ์ข‹๊ฒŒ ๋งŒ๋“ค๊ธฐ

1. ์บ์‹œ ์‹œ์  ๋ณ€๊ฒฝ useThumbnail → route.ts

์ƒ๊ฐํ•ด๋ณด๋‹ˆ๊นŒ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋ถ€๋ถ„์€ route.ts์ธ๋ฐ, ์บ์‹œ ์ €์žฅ์„ ํ•  ๊ฑฐ๋ฉด ํ›…์ด ์•„๋‹ˆ๋ผ ๋ผ์šฐํŠธ ๋ถ€๋ถ„์—์„œ ํ•˜๋Š” ๊ฒŒ ๋งž๋‹ค๊ณ  ๋А๊ผˆ๋‹ค.

๊ทธ๋ž˜์„œ useThumbnail์—์„œ ์บ์‹œํ•˜๋Š” ๋ถ€๋ถ„์„ ์ง€์šฐ๊ณ  route.ts์— ์บ์‹œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

 

const cache = new Map<string, { image: string | null; expires: number }>();
const CACHE_TTL = 1000 * 60 * 60 * 24 * 14; // 14์ผ

์ฝ”๋“œ ๊ฐ€์žฅ ์œ—๋ถ€๋ถ„์— ์ด๋ ‡๊ฒŒ ์บ์‹œ๋ฅผ ์ •์˜ํ•ด์คฌ๋‹ค.

๋ถ๋งˆํฌํ•œ ๋ธ”๋กœ๊ทธ ๊ธ€์€ ์ด๋ฏธ์ง€๊ฐ€ ๊ฑฐ์˜ ๋ฐ”๋€Œ์ง€ ์•Š์„ ๊ฒƒ์ด๋ฏ€๋กœ 14์ผ๋กœ ์„ค์ •ํ–ˆ๋Š”๋ฐ ๋” ๊ธธ๊ฒŒ ํ•ด๋„ ๊ดœ์ฐฎ์„ ๊ฒƒ ๊ฐ™๊ธดํ•˜๋‹ค.

์ด๊ฑด ๋” ์•Œ์•„๋ณด๊ณ  ์ž‘์„ฑํ•ด๋ด์•ผ๊ฒ ๋‹ค.

 

const cached = cache.get(targetUrl);
  const now = Date.now();
  if (cached && cached.expires > now) {
    return NextResponse.json(
      { image: cached.image },
      {
        headers: {
          'Cache-Control': 'public, max-age=1209600', // 14์ผ
        },
      }
    );
  }

url์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•œ ์งํ›„์— ์บ์‹œ๋ฅผ ํ™•์ธํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

 

// ์บ์‹œ ์ €์žฅ
    cache.set(targetUrl, { image: ogImage, expires: now + CACHE_TTL });

    return NextResponse.json(
      { image: ogImage },
      {
        headers: {
          'Cache-Control': 'public, max-age=1209600', // ๋ธŒ๋ผ์šฐ์ €/ํ”„๋ก์‹œ ์บ์‹œ
        },
      }
    );

try๋ฌธ ๋‚ด์—์„œ๋„ ์—ญ์‹œ image๋ฅผ ์ƒˆ๋กœ ๊ฐ€์ ธ์™”์„ ๊ฒฝ์šฐ, ์บ์‹œ์— ์ €์žฅํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

 

 

2. ๋ฉ”ํƒ€ํƒœ๊ทธ๊ฐ€ ์„ค์ •์ด ์ž˜ ์•ˆ๋˜์–ด์žˆ๋Š” ๊ฒฝ์šฐ

ํ˜„์žฌ ์ž‘์„ฑํ•œ GET์„ ๋ณด๋ฉด og์™€ twitter์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์–ป์–ด์˜ค๋„๋กํ•˜๊ณ  ์žˆ๋Š”๋ฐ,๋ฉ”ํƒ€ํƒœ๊ทธ์— ์ด๋ฏธ์ง€๊ฐ€ ์„ธํŒ…์ด ์•ˆ๋˜์–ด์žˆ๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์„ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.๊ทธ๋Ÿด ๋• ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ์ฒซ ๋ฒˆ์งธ <img> ํƒœ๊ทธ๋ฅผ ๊ฐ€์ ธ์˜ค๋„๋ก ํ•  ์ˆ˜๋„ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ํ•  ๊ฒฝ์šฐ ๋‹จ์ ์ด ์ž‡๋‹ค.

  • ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์˜ˆ์ธก ๋ถˆ๊ฐ€
    OG ์ด๋ฏธ์ง€๋Š” ์ตœ์ ํ™”๋œ ํฌ๊ธฐ์ด๋‚˜, img๋Š” ์ตœ์ ํ™”๊ฐ€ ๋œ ์ด๋ฏธ์ง€์ž„์„ ํ™•์‹ ํ•  ์ˆ˜ ์—†์–ด ๋„คํŠธ์›Œํฌ ๋ถ€๋‹ด์ด ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.
  • ์ž˜๋ชป๋œ ์ด๋ฏธ์ง€ ์„ ํƒ ๊ฐ€๋Šฅ
    ์ฒซ ๋ฒˆ์งธ img๊ฐ€ ๋กœ๊ณ , ๊ด‘๊ณ  ๋ฐฐ๋„ˆ ๋“ฑ์ด ๋  ์ˆ˜๋„ ์žˆ๋Š”๋ฐ, ์ด๋Š” UX ์ธก๋ฉด์—์„œ ๋ถ€์ ์ ˆ ํ•  ์ˆ˜ ์žˆ๋‹ค. (์ด์ƒํ•œ ๊ด‘๊ณ  ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜ค๋ฉด ์–ด๋–กํ•ด!)
  • ์„œ๋ฒ„ ์ธก ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ์ฆ๊ฐ€
    OG ์ด๋ฏธ์ง€๋Š” meta ํƒœ๊ทธ๋งŒ ์ฝ์œผ๋ฉด ๋˜์ง€๋งŒ, ์ฒซ ๋ฒˆ์งธ img๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ ค๋ฉด HTML ์ „์ฒด๋ฅผ ํŒŒ์‹ฑํ•ด์•ผํ•˜๋ฉฐ, ํ•„์š”์‹œ URL ๋ณด์ •๊นŒ์ง€ ํ•ด์•ผํ•œ๋‹ค.

๋‹จ์ ์ด ์น˜๋ช…์ ์ธ ๊ฒƒ ๊ฐ™์•„์„œ ๋ฉ”ํƒ€ํƒœ๊ทธ์— ์ด๋ฏธ์ง€๊ฐ€ ์—†๋‹ค๋ฉด ๊ธฐ๋ณธ ์ด๋ฏธ์ง€๋ฅผ ๋„์šฐ๊ธฐ๋กœ ํ–ˆ๋‹ค.

์•„๋งˆ AI๊ฐ€ ๋” ํฌ๊ฒŒ ๋ฐœ์ „ํ•˜๋ฉด AI ์ƒ์„ฑํ˜• ์ด๋ฏธ์ง€๋ฅผ ๋„์›Œ๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

 

3. image ํ—ˆ์šฉ url์˜ ๋ณด์•ˆ๊ณผ ์„ฑ๋Šฅ

๋‹ค์–‘ํ•œ ๋ถ๋งˆํฌ๋ฅผ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด์„œ next.config.ts์—์„œ hostname: '**'๋กœ ์ธํ„ฐ๋„ท ์ƒ์˜ ๋ชจ๋“  images๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉํ•˜์˜€๋‹ค.

ํ•˜์ง€๋งŒ ๋ณด์•ˆ๊ณผ ์„ฑ๋Šฅ ์ธก๋ฉด์—์„œ ๊ฑฑ์ •๋˜๋Š” ๋ถ€๋ถ„์ด ์žˆ์—ˆ๋‹ค.

  • ๋ณด์•ˆ
    ์™ธ๋ถ€ URL์„ ๊ทธ๋Œ€๋กœ ๋กœ๋“œ → ์•…์„ฑ ์ด๋ฏธ์ง€, XSS, SSRF ๊ณต๊ฒฉ ๊ฐ€๋Šฅ
    ๊ณต๊ฒฉ์ž๊ฐ€ ์ž„์˜๋กœ ์„œ๋ฒ„ ์š”์ฒญ์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ์Œ
  • ์„ฑ๋Šฅ
    Next.js Image ์ตœ์ ํ™” ๊ณผ์ •์—์„œ ๋ชจ๋“  ์ด๋ฏธ์ง€์— ๋Œ€ํ•ด resizing, WebP ๋ณ€ํ™˜ ๋“ฑ์˜ ์„œ๋ฒ„ ์ฒ˜๋ฆฌ๋ฅผ ์‹œ๋„ํ•˜์—ฌ, ๋„๋ฉ”์ธ์ด ๋งŽ์œผ๋ฉด CDN ์บ์‹ฑ์ด ์–ด๋ ค์›Œ์ง€๊ณ  ์ฒซ ๋กœ๋“œ ์†๋„๊ฐ€ ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์Œ

ํ”„๋ก ํŠธ์—์„œ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹จ์ˆœํ•˜๊ฒŒ ์ƒ๊ฐํ•˜๋ฉด ์„ธ ๊ฐ€์ง€์˜ ํ•ด๊ฒฐ์ฑ…์ด ์žˆ๋‹ค.

  • ์ปค์Šคํ…€ ๋กœ๋”(Custom loader)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ <img>์ฒ˜๋Ÿผ ์ถœ๋ ฅ (Next.js์˜ ์ตœ์ ํ™” ํฌ๊ธฐ)
  • ์‹ ๋ขฐ๋œ ๋„๋ฉ”์ธ๋งŒ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ์ฒ˜๋ฆฌ (๋ชจ๋“  ๋งํฌ๋ฅผ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ํ˜„์‹ค์ ์œผ๋กœ ๋ถˆ๊ฐ€๋Šฅํ•จ + 50๊ฐœ ์ œํ•œ ์žˆ์Œ)
  • ํ”„๋ก์‹œ ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€ ์บ์‹ฑ ํ›„ ํด๋ผ์ด์–ธํŠธ์— ์ œ๊ณต (์„œ๋ฒ„ ๋ถ€๋‹ด์ด ์ปค์ง)

 

์ด ๋ถ€๋ถ„ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ์ด๊ฒƒ์ €๊ฒƒ ํ•ด๋ดค๋Š”๋ฐ ๋™๊ธฐ๋“ค์ด๋ž‘ ์–˜๊ธฐํ•˜๋‹ˆ๊นŒ ๊ฒฐ๊ตญ ์„œ๋ฒ„์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ๋งž๋Š” ๊ฒƒ ๊ฐ™๋‹ค๋Š” ๊ฒฐ๋ก ์ด ๋‚˜์™”๋‹ค..^_^..

์›๋ž˜ ๋‹ค๋“ค ๋ชฐ๋ž์–ด์„œ ํ˜„์žฌ ์„œ๋ฒ„์—์„œ ์ด๋ฏธ์ง€ ์ €์žฅ์„ ์ƒ๊ฐ์„ ์•ˆํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ๊ทธ๋ž˜๋„ ๋‹คํ–‰ํžˆ ์ด๋ฅธ ์‹œ์ ์— ์ด๋ฏธ์ง€ ์ €์žฅ์„ ๊ตฌํ˜„ํ•ด์•ผํ•œ๋‹ค๋Š” ์–˜๊ธฐ๊ฐ€ ๋‚˜์™€์„œ ๋‹คํ–‰์ด๋‹ค.

 

ํ”„๋ก ํŠธ์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด์ฃผ๋ฉด ์„œ๋ฒ„์—์„œ ํฌ๋กค๋ง์„ ํ†ตํ•ด ์ตœ์ ํ™”๋œ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•˜๊ณ , ํ”„๋ก ํŠธ๋กœ ๋ณด๋‚ด์ฃผ๋Š” ๋ฐฉ์‹์ด๋‹ค.

 

 

 

 

 

์ฐธ๊ณ 

๋”๋ณด๊ธฐ

์ฐธ๊ณ ์ž๋ฃŒ๋งํฌ

๋ฐ˜์‘ํ˜•