미주알고주알

[Next.js] SEO 태그를 사용해 검색 엔진 최적화 적용하기 본문

Next.js

[Next.js] SEO 태그를 사용해 검색 엔진 최적화 적용하기

미주알고주알 2023. 3. 23. 00:03

Next에서 검색 엔진 최적화를 위한 SEO meta 태그의 예시 작성함.

 

사용법? 원하는 페이지 뷰에 맞게 해당 meta 태그를 같이 사용해주면 된다. 이때, 태그에 들어갈 데이터는 `getServerSideProp`라는 SSR 메소드와 함께 사용하여 초기 렌더링 데이터 자체를 서버에서 먼저 제공하여 말 그대로 검색 엔진 최적화를 완성한다.

 

import Head from 'next/head'
import { FC, ReactNode, Fragment } from 'react'
import config from '@config/seo_meta.json'

const siteUrl = process.env.PUBLIC_SITE_URL
const siteBaseUrl = siteUrl ? `https://${siteUrl}` : null

interface Props {
  title?: string
  description?: string
  robots?: string
  openGraph?: {
    title?: string
    type?: string
    locale?: string
    description?: string
    site_name?: string
    url?: string
    images?: OgImage[]
  }
  children: ReactNode
}

interface OgImage {
  url?: string
  width?: string
  height?: string
  alt?: string
}

const ogImage = ({ url, width, height, alt }: OgImage, index: number) => {
  const imgUrl = siteBaseUrl ? new URL(url!, siteBaseUrl).toString() : url

  return (
    <Fragment key={`og:image:${index}`}>
      <meta
        key={`og:image:url:${index}`}
        property="og:image"
        content={imgUrl}
      />
      <meta
        key={`og:image:width:${index}`}
        property="og:image:width"
        content={width}
      />
      <meta
        key={`og:image:height:${index}`}
        property="og:image:height"
        content={height}
      />
      <meta
        key={`og:image:alt:${index}`}
        property="og:image:alt"
        content={alt}
      />
    </Fragment>
  )
}

const SEO: FC<Props> = ({
  title,
  description,
  openGraph,
  robots,
  children,
}) => {
  return (
    <Head>
      <title key="title">{title}</title>
      <meta
        key="description"
        name="description"
        content={description ?? config.description}
      />
      <meta
        key="og:type"
        property="og:type"
        content={openGraph?.type ?? config.openGraph.type}
      />
      <meta
        key="og:title"
        property="og:title"
        content={
          openGraph?.title ?? config.openGraph.title ?? title ?? config.title
        }
      />
      <meta
        key="og:description"
        property="og:description"
        content={
          openGraph?.description ??
          config.openGraph.description ??
          description ??
          config.description
        }
      />
      <meta
        key="og:site_name"
        property="og:site_name"
        content={openGraph?.site_name ?? config.openGraph.site_name}
      />
      <meta
        key="og:url"
        property="og:url"
        content={openGraph?.url ?? config.openGraph.url}
      />
      <meta key="og:locale" property="og:locale" content={openGraph?.locale} />
      {openGraph?.images?.length
        ? openGraph.images?.map((img, i) => ogImage(img, i))
        : ogImage(config.openGraph.images[0], 0)}
      {config.twitter.cardType && (
        <meta
          key="twitter:card"
          name="twitter:card"
          content={config.twitter.cardType}
        />
      )}
      {config.twitter.site && (
        <meta
          key="twitter:site"
          name="twitter:site"
          content={config.twitter.site}
        />
      )}
      {config.twitter.handle && (
        <meta
          key="twitter:creator"
          name="twitter:creator"
          content={config.twitter.handle}
        />
      )}
      <meta key="robots" name="robots" content={robots ?? 'index,follow'} />
      <meta
        key="googlebot"
        name="googlebot"
        content={robots ?? 'index,follow'}
      ></meta>
      {children}
    </Head>
  )
}

export default SEO

 

여기서 `config`파일은 정적 데이터를 따로 분리해 관리함.

json 파일 대신 .ts 파일도 사용해도 되지만 key 에 대한 타입 추론도 추가되어야 한다! (`keyof typeof SeoMeta`)

 

더 나아가  다음을 통해 여러 `ogImage`를 동시에 처리하는 것 뿐만 아니라 `structuredData`(구글-구조화된 데이터) 를 통해 검색 엔진이 해당 페이지의 데이터를 더 잘 이해할 수 있도록 일련의 논리적인 레시피처럼 정리해두는 것도 구현할 수 있다.

function DetailSEO({
   title,
   description,
   openGraph,
   robots,
   children,
}: Props) {

  const { title, type, locale, description, site_name, url, images } = openGraph

  const { asPath: path } = useRouter()

  const imageArr =
    !images || images.length === 0
      ? [config.banner]
      : typeof images === 'string'
      ? [images]
      : images

  const featuredImages = imageArr.map((image) => ({
    '@type': 'ImageObject',
    url: ['https', 'http'].includes(image.split('://')[0]) ? image : config.siteUrl + image,
  }))

  const structuredData = {
    '@context': 'https://schema.org',
    '@type': 'Detail Page',
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': path,
    },
    headline: ogTitle,
    image: featuredImages,
    author: {
      '@type': 'Person',
      name: config.author,
    },
    publisher: {
      logo: {
        '@type': 'ImageObject',
        url: config.siteUrl + config.siteLogo,
      },
    },
    description: description,
    datePublished: config.datePublished,
  }

  return (
    <>
      <SEO
       title={title},
       description={description},
       openGraph={openGraph}
       robots={robots}>
          <Head>
            <script
              type="application/ld+json"
              dangerouslySetInnerHTML={{
                __html: JSON.stringify(structuredData, null, 2),
              }}
            />
          </Head>
      </SEO>
  )
}

'Next.js' 카테고리의 다른 글

Next.js에서 url 바꾸는 방법 3가지  (0) 2023.02.16
Next.js의 서버 사이드 렌더링 기술 3가지  (0) 2023.02.16