# SEO를 위한 나의 몸부림

> Next의 SEO를 향상 시키는 방법을 다룹니다.

- Published: 2023-02-28
- Source: https://yujiseok.blog/ko/posts/how-to-improve-seo

<Image
  src="/images/seo/img.png"
  width={1199}
  height={1087}
  alt="SEO 없는 내 포스트"
/>

## 개요

블로그에 SEO를 적용하기 위해 공부한 내용과 제가 블로그에 적용한 내용을 정리합니다.

## SEO란 뭘까?

**SEO**는 Search Engine Optimization의 약자로 검색 엔진 최적화라는 뜻을 갖습니다.
검색 유저의 의도를 이해하고, 의도에 맞게 웹 페이지의 콘텐츠를 제작하는 과정이라고 할 수 있습니다.

또 페이지가 검색 결과 페이지 상단에 잘 노출될 수 있게, 시맨틱 태그와 구조를 개선해 트래픽을 늘리는 방법론입니다.

## SEO 과정

SEO의 과정을 알기 위해선 검색 엔진이 웹사이트 품질을 결정하는 과정을 알아야 합니다.

[검색 엔진](https://developers.google.com/search/docs/fundamentals/how-search-works?hl=ko#introducing-the-three-stages-of-google-search)은 3 단계로 작동합니다. (구글 기준)

**1. 크롤링**

- 크롤러라는 자동화된 프로그램을 사용해, 인터넷에서 찾은 페이지로부터 텍스트, 이미지, 동영상을 다운로드합니다.

**2. 색인 생성**

- 페이지의 텍스트, 이미지, 동영상 파일을 분석하고 색인에 정보를 저장합니다.

**3. 결과 게재**

- 검색어와 관련된 정보를 반환해 게재합니다.

**크롤링**과 **색인 생성**을 좀 더 자세히 살펴보겠습니다.

**크롤링**

검색엔진은 계속해서 새 페이지와 업데이트된 페이지를 검색해 파악된 페이지 목록에 웹페이지를 등록합니다. 이런 과정을 **URL 검색**이라고 합니다.
기존에 방문한 적이 있는 페이지는 파악된 페이지 목록에 추가되어 있습니다. 다른 페이지의 발견은 새 페이지로 연결되는 링크를 따라갈 때 발견됩니다.
또 사이트 소유자가 **사이트맵**을 제출해 발견되는 페이지도 있습니다.

**색인 생성**

페이지가 크롤링 되면 검색 엔진은 페이지의 내용을 파악하려고 합니다.
이 단계를 색인 생성이라고 하며 `title`, `alt` 속성 등 핵심 콘텐츠·태그와 속성을 처리하고 분석하는 작업입니다.

## SEO가 중요한 이유

우리는 궁금한 것이 생기면 구글 혹은 네이버 등 검색 엔진에 궁금한 키워드를 검색합니다.

<Image src="/images/seo/img2.png" width={907} height={740} alt="아 뭐더라?" />

이때, 대부분의 사람들은 검색 결과 상위에 노출된 페이지를 누르고 열람합니다.
물론 광고를 사용해 트래픽을 올릴 수 있지만 (**paid search**) 대부분의 사람들은 자연적으로 트래픽이 높은 결과 (**organic search**)를 선호합니다.
SEO를 잘하면 비용을 들이지 않고도 페이지를 검색 결과 상위에 지속적으로 노출시킬 수 있습니다.

즉 아무리 좋은 콘텐츠를 갖는 사이트라도 SEO가 좋지 않다면, 검색 결과에서 노출되지 않을 확률이 높습니다.

## 메타 태그

메타 태그란 검색 엔진에 페이지에 대한 추가 정보를 제공하는 데 사용되는 HTML 태그입니다. `meta`태그는 HTML의 `<head>` 태그 안에 추가됩니다.
HTML 문서가 어떤 내용이며, 키워드, 만든 사람 등의 메타데이터를 제공합니다.

Next는 `13.2` 버전 이후부터 [빌트인](https://beta.nextjs.org/docs/api-reference/metadata)으로 SEO를 위한 메타데이터를 만들어주는 기능을 지원합니다.
메타데이터는 어떤 `page.tsx` 나 `layout.tsx`에서 사용 가능합니다.

> ✨ `charset` 과 `viewport`은 설정해 주지 않아도 기본적으로 적용됩니다!

```html
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
```

Next를 이용하면, 두 가지의 방법으로 메타데이터를 생성할 수 있습니다.

1. 정적 메타데이터 (Static)

정적으로 메타데이터를 생성하는 방법으로, 페이지 내부에서 `metadata`를 `export` 하여 메타데이터 객체를 생성합니다.
이때 template 속성을 이용하면 방문하는 페이지 별로 타이틀이 바뀌게 됩니다.

```ts title="layout.tsx"
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: {
    default: "Yujiseok",
    template: "%s | Yujiseok", // 방문하는 페이지 별 타이틀 변경
  },
  description: "공부하는 것을 기록하고 공유하는 유지석의 개인 기술 블로그",
  icons: {
    shortcut: "/favicon.ico",
  },
  openGraph: {
    title: {
      absolute: "유지석",
    },
    description: "공부하는 것을 기록하고 공유하는 유지석의 개인 기술 블로그",
    url: "https://www.yujiseok.blog/",
    locale: "ko_KR",
    type: "website",
  },
};
```

2. 동적 메타데이터 (Dynamic)

`[slug]`와 같이 동적 라우팅을 할 경우에 동적인 값을 받아오기 위해 사용하는 방법입니다.
메타데이터를 `generateMetadata` 함수를 사용하여 `fetch` 해 동적으로 생성합니다.

제 블로그에 동적으로 메타데이터를 생성해야 할 페이지는 각각의 포스트 페이지입니다.
메타데이터를 생성하기 위해선 우선적으로 slug가 필요합니다.

slug의 경우 `generateStaticParams` 함수를 사용해 개별적으로 생성합니다.
`find` 메서드를 사용해 slug와 맞는 포스트를 찾습니다.
찾아진 포스트에서 데이터를 추출하여 메타데이터에 맞는 형식으로 반환해 줍니다.

```ts title="[slug]/page.tsx"
export async function generateStaticParams() {
  return allPosts.map((post) => ({
    slug: post.slug,
  }));
}

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata | undefined> {
  const post = allPosts.find((post) => post.slug === params.slug);
  if (!post) {
    return;
  }
  const {
    title,
    publishedAt: publishedTime,
    summary: description,
    slug,
  } = post;

  return {
    title,
    description,
    openGraph: {
      title: { absolute: title },
      description,
      type: "article",
      publishedTime,
      url: `https://www.yujiseok.blog/post/${slug}`,
    },
  };
}
```

> ✨ openGraph는 sns나 메신저에 링크를 공유할 때, 컨텐츠의 미리 보기를 지원해 주는 태그입니다.

위와 같은 과정을 거치게 되면, Next가 메타데이터를 생성해 줍니다.

<Image
  src="/images/seo/meta.png"
  width={787}
  height={261}
  alt="동적으로 생성된 메타데이터"
/>

## 사이트맵

**사이트맵**은 사이트에 있는 페이지, 동영상 및 기타 파일과 그 관계에 관한 정보를 제공하는 파일입니다.
구글과 같은 검색 엔진은 사이트맵을 통해 더 효율적으로 크롤링을 할 수 있게 됩니다.

사이트의 페이지가 제대로 링크 되어있다면 검색 엔진은 대부분의 사이트를 제대로 찾을 수 있습니다.
하지만 사이트맵을 사용하면, 규모가 큰 사이트나 전문화된 파일의 크롤링 효율을 상승시킬 수 있습니다.

사이트맵은 규모가 큰 사이트에 필요하지만, 사이트맵을 동적으로 생성하는 것을 구현해 보기 위해 직접 사이트맵을 생성해 보기로 하였습니다.

아래의 코드는 가장 기본적인 xml 사이트맵입니다.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://www.example.com</loc>
    <lastmod>2023-02-28</lastmod>
  </url>
</urlset>
```

각 태그의 설명은 아래와 같습니다.

- urlset: 파일을 캡슐화하고 현재 프로토콜 표준을 참조합니다.
- url: 각 url 항목의 상위 태그입니다.
- loc: 페이지의 url
- lastmod: 파일을 마지막으로 수정한 날짜입니다.

위 태그 외에도 `changefreq`, `priorty` 등의 선택 옵션 태그가 있습니다.

> 🚨 아래의 생성 방식은 레가시한 방식입니다. 넥스트는 현재 [사이트맵](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap)과 [robots.txt](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots)를 생성하는 빌트인 API를 제공합니다.
> 빌트인 API를 사용해 구현하는 방식은 이 [포스트](https://yujiseok.blog/post/next-sitemap-and-robots)에서 확인하실 수 있습니다.

사이트맵을 생성하기 위해 `node`의 `fs` 모듈을 가져옵니다.
현재 작업 디렉토리의 경로를 사용해 mdx 파일들이 있는 content 폴더를 읽어들입니다.
모든 파일들을 `map` 메서드를 사용해 순회하면서, `.mdx` 확장자를 `replace` 메서드를 사용해 제거합니다.

이렇게 반환된 slug들을 `postUrls`에 템플릿 리터럴을 사용해 담아줍니다.
xml 태그 코드에 `postUrls`을 역시 템플릿 리터럴을 사용해 추가한 후 `public` 경로에 파일을 생성합니다.

```ts title="lib/sitemap.ts"
import fs from "fs";

const getDate = new Date().toISOString();

function createSiteMap() {
  const path = process.cwd();

  const allPosts = fs.readdirSync(`${path}/content`);
  const slugs = allPosts.map((post) => post.replace(".mdx", ""));

  const postUrls = slugs
    .map(
      (slug) => `<url>
        <loc>https://www.yujiseok.blog/post/${slug}</loc>
        <lastmod>${getDate}</lastmod>
      </url>`
    )
    .join("");

  const generatedSitemap = `<?xml version="1.0" encoding="UTF-8"?>
    <urlset
      xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 
      http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
    >
      <url>
        <loc>https://www.yujiseok.blog/</loc>
      </url>
      <url>
        <loc>https://www.yujiseok.blog/about</loc>
      </url>
      <url>
        <loc>https://www.yujiseok.blog/post</loc>
      </url>
      ${postUrls}
    </urlset> `;

  fs.writeFileSync("public/sitemap.xml", generatedSitemap, "utf-8");
}

createSiteMap();
```

빌드 직전에 스크립트를 이용해 사이트맵을 `public` 디렉토리에 생성해 줍니다.

```json title="package.json"
  "scripts": {
    // ...
    "sitemap": "ts-node --project tsconfig.node.json ./lib/sitemap.ts",
    "build": "pnpm sitemap && next build",
  }
```

생성된 사이트맵은 [구글서치콘솔](https://search.google.com/search-console/about?hl=ko)에 등록해 줍니다.

<Image
  src="/images/seo/console.png"
  width={1220}
  height={637}
  alt="동적으로 생성된 메타데이터"
/>

## robots.txt

**robots.txt**는 크롤러에게 사이트 및 웹페이지를 수집할 수 있도록 허용하거나 제한하는 .txt 파일입니다.
robots.txt는 사이트맵과 함께 `public` 디렉토리에 추가해 줍니다.

```txt
User-agent: * ➡️ 모든 크롤러 허용
Allow: / ➡️ 모든 디렉토리 허용
Sitemap: https://yujiseok.blog/sitemap.xml
```

## 끝으로

평소 한 번도 고민해 보지 않은 분야였는데, 블로그를 만들며 SEO가 정말 중요하며 간과하지 말아야 할 것이라는 것을 알게 되었습니다.
언젠가 제 블로그도 구글에 검색하였을 때 검색 결과 최상단에 나올 그날을 위해 열심히 SEO를 향상시켜야겠습니다.

---

#### 출처

- https://developers.google.com/search/docs/fundamentals/how-search-works?hl=ko
- https://beta.nextjs.org/docs/api-reference/metadata
- https://www.sitemaps.org/ko/protocol.html
- https://searchadvisor.naver.com/guide/seo-basic-robots
