๐Ÿ”
๊ฐœ๋ฐœโ€ขํ”„๋ก ํŠธ์—”๋“œ

SEO๋ฅผ ์œ„ํ•œ Next.js ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„ค์ •๋ฐฉ๋ฒ•

2024.08.12

Next.js๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€ํ‘œ์ ์ธ ์ด์œ ์ธ SEO๋ฅผ ๋ธ”๋กœ๊ทธ์— ์ ์šฉํ•˜๋ฉด์„œ ๊ฒช์€ ์‚ฝ์งˆ๊ธฐ๋ฅผ ๊ธฐ๋ก ๋ฐ ๊ณต์œ ํ•˜๊ณ ์ž ํฌ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

What is SEO? SEO stands for Search Engine Optimization. The goal of SEO is to create a strategy that will increase your rankings position in search engine results. The higher the ranking, the more organic traffic to your site, which ultimately leads to more business for you!

๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„ค์ •

next js page router ์—์„œ๋Š” next/head์˜ Head ํƒœ๊ทธ๋‚ด์— ์ •์˜ํ•ด์„œ ์„ค์ •ํ•ด์•ผ ํ–ˆ์ง€๋งŒ app router์—์„œ๋Š” layout ๋˜๋Š” page์—์„œ metadata๋‚˜ generateMetadata๋ฅผ export ํ•˜๋ฉด๋œ๋‹ค.

// ์ •์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์„ฑ

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: '...',
  description: '...',
};

export default function Page() {}
// ๋™์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์„ฑ

import type { Metadata, ResolvingMetadata } from 'next';

type Props = {
  params: { id: string };
  searchParams: { [key: string]: string | string[] | undefined };
};

export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
  // read route params
  const id = params.id;

  // fetch data
  const product = await fetch(`https://.../${id}`).then((res) => res.json());

  // optionally access and extend (rather than replace) parent metadata
  const previousImages = (await parent).openGraph?.images || [];

  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  };
}

export default function Page({ params, searchParams }: Props) {}

๋™์ ์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ์šฐ args๋กœ params, searchParams, parent(๋ถ€๋ชจ์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ)๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

favicon๊ณผ opengraph image ์—ฐ๋™

create next app ๋ช…๋ น์–ด๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด vercel์˜ ๋กœ๊ณ ๊ฐ€ ๊ธฐ๋ณธ ํŒŒ๋น„์ฝ˜์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค. ์šฐ์„  ํŒŒ๋น„์ฝ˜์„ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด ํŒŒ๋น„์ฝ˜์ด ์žˆ์–ด์•ผ ํ•˜๋Š”๋ฐ, ๋‚˜๋Š” favicon.io ์—ฌ๊ธฐ์„œ ํŒŒ๋น„์ฝ˜์„ ์ƒ์„ฑํ–ˆ๋‹ค.

์ ๋‹นํ•œ ํŒŒ๋น„์ฝ˜์„ ๋งŒ๋“ค๊ณ  ๋‹ค์šด๋กœ๋“œ๋ฅผ ํ•˜๋ฉด android, apple, favicon ์ด๋ฆ„๋ถ€ํ„ฐ png, ico ํ™•์žฅ์ž๋ช… ํŒŒ์ผ, site.webmanifest ๋“ฑ ์—ฌ๋Ÿฌ ํŒŒ์ผ์ด ํ•จ๊ป˜์ƒ๊ธด๋‹ค.

์šฐ์„  ํŒŒ๋น„์ฝ˜์„ ๋ณ€๊ฒฝํ•ด๋ณด์ž

๊ธฐ๋ณธ์ ์œผ๋กœ app ๋””๋ ‰ํ† ๋ฆฌ ์•ˆ์— favicon.ico ๋ผ๋Š” ํŒŒ์ผ์ด ์žˆ์„ํ…๋ฐ, ํ•ด๋‹น ํŒŒ์ผ์„ ์ƒ์„ฑํ•œ favicon.ico๋กœ ๊ต์ฒดํ•ด๋„ ๋˜์ง€๋งŒ, metadata๋กœ ์ „๋‹ฌํ•ด๋„ ํŒŒ๋น„์ฝ˜์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‚˜๋Š” /public/favicon ํ•˜์œ„๋กœ ๊ด€๋ จ ํŒŒ์ผ์„ ๋ชจ๋‘ ๋„ฃ์–ด์ฃผ๊ณ  ๋ฐฐ์—ด ํ˜•ํƒœ๋กœ ๋งŒ๋“ค์–ด์„œ layout์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ > icons ๋กœ ๋„ฃ์–ด์คฌ๋‹ค.

// /constant/metadata.ts
export const METADATA_ICONS: Metadata["icons"] = [
    {
        url: "/favicon/favicon-16.png",
        sizes: "16x16",
        type: "image/png",
    },
    {
        ...
    },
]

// /app/layout.tsx
export const metadata: Metadata = {
    ...DEFAULT_META,
    icons: METADATA_ICONS,
};

์—ฌ๊ธฐ๊นŒ์ง€ ์ ์šฉํ•˜๋ฉด ํŒŒ๋น„์ฝ˜ ์•„์ด์ฝ˜์ด ๋ณ€๊ฒฝ๋˜์–ด์žˆ๋‹ค.

open api key ์ €์žฅ ์ด๋ฏธ์ง€

์ด์ œ opengraph๋ฅผ ์ ์šฉํ•ด๋ณด์ž. ๋งŒ์•ฝ opengraph ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์•„๋ž˜์ฒ˜๋Ÿผ url ๊ฒฝ๋กœ๋กœ metadata์—์„œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

// ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ opengraph ์ด๋ฏธ์ง€ ์„ค์ • ๋ฐฉ๋ฒ•

export const metadata: Metadata = {
    openGraph: {
        images: [
            {
                url: "/public/opengraph-image.png",
                alt: "...",
                type: "image/png",
                width: 000,
                height: 000,
            },
        ],
    },
};

๊ทธ๋Ÿฌ๋‚˜ ๋‚˜๋Š” opengraph ์ด๋ฏธ์ง€๊ฐ€ ์—†์–ด์„œ next.js๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋™์ ์œผ๋กœ opengraph, twitter card ๋“ฑ ์†Œ์…œ ๋ฏธ๋””์–ด ์ด๋ฏธ์ง€ ์ƒ์„ฑ์ž์ธ ImageResponse ๋ฅผ ์‚ฌ์šฉํ•ด์„œ opengraph ์ด๋ฏธ์ง€๋ฅผ ๋งŒ๋“ค๊ธฐ๋กœ ํ–ˆ๋‹ค.

14๋ฒ„์ „ ์ด์ „์—๋Š” next/server์—์„œ import ํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ , 14๋ฒ„์ „๋ถ€ํ„ด next/og ์—์„œ import ํ•  ์ˆ˜ ์žˆ๋‹ค.

์˜ˆ์‹œ๋Š” ์—ฌ๊ธฐ ๋‚˜์™€์žˆ๋‹ค. opengraph๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ๋กœ์— opengraph-image.tsx๋กœ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๋‚˜๋Š” root์— opengraph-image.tsx๋ฅผ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ํ•˜์œ„ routes์—๋Š” ์ ์šฉ์ด ์•ˆ๋๋‹ค. ๊ทธ๋ž˜์„œ ์ง์ ‘ ๊ฒฝ๋กœ๋ฅผ ๋„ฃ์–ด์ฃผ๊ธฐ ์œ„ํ•ด vercel ๋ฌธ์„œ์—์„œ ์ œ๊ณตํ•˜๋Š” ํ˜•ํƒœ์ธ api routes๋กœ ๋งŒ๋“ค๊ธฐ๋กœ ํ–ˆ๋‹ค.

๋ฐฉ๋ฒ•์€ opengraph-image.tsx๋กœ ๋งŒ๋“œ๋Š”๊ฑฐ๋ž‘ ๊ฑฐ์˜ ๋™์ผํ•œ๋ฐ, ์ฐจ์ด์ ์€ next.js์—์„œ ์ œ๊ณตํ•˜๋Š” api routes๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋งŒ๋“œ๋Š” ์ ์ด ๋‹ค๋ฅด๋‹ค.

// /api/og/route.tsx

import { ImageResponse } from 'next/og';
// App router includes @vercel/og.
// No need to install it.

export async function GET() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 40,
          color: 'black',
          background: 'white',
          width: '100%',
          height: '100%',
          padding: '50px 200px',
          textAlign: 'center',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        ๐Ÿ‘‹ Hello
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•œ api ๊ฒฝ๋กœ๋ฅผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์˜ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋กœ ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค.

const metadata: Metadata = {
    ...
    openGraph: {
        images: [
            {
                url: "/api/og",
                alt: "Shimyuseob's blog og image",
                type: "image/png",
                width: 1200,
                height: 630,
            },
        ],
        ...

๋‚ด ๋ธ”๋กœ๊ทธ๋ฅผ ์˜ˆ์‹œ๋กœ ๋ณด๋ฉด https://www.shimyuseob.xyz/api/og ๊ฒฝ๋กœ์— opengraph ์ด๋ฏธ์ง€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

og๊ฐ€ ์ž˜ ์„ค์ •๋๋Š”์ง€๋Š” head์˜ og:image ๋ฉ”ํƒ€ํƒœ๊ทธ๋ฅผ ๋ณด๊ฑฐ๋‚˜, https://www.opengraph.xyz/ ์ด๊ณณ์—์„œ๋„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

SEO๋ฅผ ๋‹ค ์„ค์ •ํ•˜๋ฉด ๊ตฌ๊ธ€ ์„œ์น˜์ฝ˜์†”์—์„œ ์ƒ‰์ธ์ƒ์„ฑ์„ ์š”์ฒญํ•˜๊ฑฐ๋‚˜ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

JSON-LD

๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด JSON-LD ์„ค์ •๋„ ๋‚˜์™€์žˆ๋‹ค.

JSON-LD๋Š” ๊ฒ€์ƒ‰ ์—”์ง„์ด ์ฝ˜ํ…์ธ ๋ฅผ ์ดํ•ดํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ ํ˜•์‹

์ธ๋ฐ, ํšŒ์‚ฌ ํ”„๋กœ์ ํŠธ์—” ์„ค์ •ํ•ด๋’€์ง€๋งŒ ๋ธ”๋กœ๊ทธ์—๋Š” JSON-LD๋Š” ์„ค์ •ํ•˜์ง€ ์•Š์•˜๋‹ค.

๋‚˜์ค‘์— ํฌ์ŠคํŠธ๋„ ๋งŽ์•„์ง€๊ณ , ์ƒ‰์ธ์ด ์ž˜ ์•ˆ๋˜๋Š” ๊ฒƒ ๊ฐ™์œผ๋ฉด ์ถ”๊ฐ€ํ•ด๋ด๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.