오랜만에 블로그를 새단장하며 이전에 Next.js로 마이그레이션하며 미처 추가하지 못했던 SEO 관련해서 설정해보고자 합니다.
SEO?
서재응 아님 주의
SEO (Search Engine Optimization)는 검색엔진 최적화의 줄임말로, 웹사이트가 검색 결과에 더 잘 보이도록 최적화하는 과정입니다.
흔히 SEO라고 하면 시맨틱 태그 사용, 사이트맵 설정 등 여러 가지 방식이 따라오곤 하는데, SEO는 검색 순위를 개선하는 과정 자체이기 때문에 각각의 방식은 SEO를 잘 적용하기 위해 사용하는 하나의 예시일 뿐입니다.
보통 검색 엔진은 사이트를 크롤링하며 찾은 콘텐츠의 색인을 생성하는데, 우리가 검색해서 보는 목록은 그 색인에서 뽑아낸 콘텐츠입니다. 이 크롤러는 일정한 규칙을 따라가므로 해당 규칙을 잘 지킨다면 사이트가 검색 결과의 최상위에 노출될 가능성이 높아지는 것입니다.1
저는 구글 서치 콘솔에만 블로그를 등록해둘 예정이라 Googlebot 관련하여 SEO 관련 가이드를 참고했습니다.
Google이 내 콘텐츠를 찾을 수 있도록 하기
구글은 콘텐츠를 잘 찾을 수 있는 방식으로 세 가지를 제안합니다.
- 이미 색인이 등록된 콘텐츠에 내 콘텐츠를 링크로 걸어두기
- 사이트맵 제출하기
- Google이 사용자와 같은 방식으로 내 페이지를 인식할 수 있는지 확인하기
1번은 제가 혼자서 할 수 있는 부분이 아니고, 3번은 구글서치콘솔에 등록해야 알 수 있는 부분이기에 2번의 방식을 설정을 통해 적용해보기로 합니다.
사이트맵 제출하기
Next.js에서는 기본적으로 사이트맵을 만들기 위한 방법으로 직접 1. sitemap 파일을 추가하는 방식
과 2. sitemap을 만드는 코드를 작성하는 방식
두 가지를 소개합니다.
제 블로그는 간단한 형태만 띄고 있기에 추가해야 할 링크가 많지는 않으나, 매번 글을 작성할 때마다 sitemap을 건드리는 건 개발자로서 용납할 수 없는 부분이겠죠. 자고로 참된 개발자는 자동화를 해야합니다. (단호)
저는 그래서 정적 사이트맵 파일을 넣기보다는 자동화하여 알아서 배포할 때마다 새로운 사이트맵을 만드는 방식을 사용했습니다.
Next.js에서는 SSR 페이지에 한정하여 Node.js의 모듈을 사용할 수 있습니다. (기본적으로 CSR을 적용하지 않았다면 SSR이니까 대부분은 사용할 수 있겠죠?)
때문에 Next.js로 구현한 블로그에서도 fs 모듈을 사용하여 편하게 마크다운 형식으로 작성한 파일의 내용을 읽어오고 있었는데, 비슷하게 fs 모듈을 사용해서 모든 파일의 메타데이터를 읽어와 그 메타데이터를 사용하여 사이트맵을 동적으로 생성할 수 있도록 만들어보려고 합니다.
블로그 글의 메타데이터 목록 읽어오기
저는 블로그 글 파일을 아래와 같은 구조로 관리하고 있는데요.
📦public
┣ 📂posts
┃ ┣ 📂FE
┃ ┃ ┣ 📜2022-10-15-ts-loader-vs-babel-loader-vs-esbuild-loader.md
┃ ┃ ┣ 📜2023-06-01-졸업프로젝트.md
┃ ┃ ┣ 📜2023-06-22-Vite가-뭘까.md
┃ ┃ ┗ 📜2025-02-03-블로그에-SEO-적용하기.md
┃ ┗ 📂회고
┗ ┃ ┗ 📜2025-01-23-2024년-회고.md
여기에서 FE
, 회고
에 해당하는 부분은 카테고리인데, 먼저 이 카테고리 목록을 가져오겠습니다.
import { readdirSync } from 'fs';
import path from 'path';
const categories = readdirSync(path.resolve(process.cwd(), 'public', 'posts'));
그럼 이렇게 가져온 카테고리 폴더를 돌면서 파일을 읽어오면 모든 글에 대한 메타데이터 목록을 만들어낼 수 있습니다.
import { readdirSync } from 'fs';
import path from 'path';
const metadata = categories.reduce((accMetadata, category) => {
const categoryMetadata = readdirSync(
path.resolve(process.cwd(), 'public', 'posts', category),
).map(fileName => readMetadata(category, fileName));
return [...accMetadata, ...categoryMetadata];
}, []);
마크다운 파일의 메타데이터를 읽어오는 부분은 gray-matter라는 라이브러리를 사용하고 있는데, 이건 별로 중요하지 않은 부분이라 생략하겠습니다. 만약 마크다운 파일에서 메타데이터를 읽어오는 코드가 궁금하시면 제 깃허브 코드를 참고해주세요!
이렇게 하면 모든 블로그 글 파일의 메타데이터를 읽어오는 코드가 아래와 같이 만들어집니다.
import { readdirSync } from 'fs';
import path from 'path';
const readAllPostMetadata = () => {
const categories = readdirSync(path.resolve(process.cwd(), 'public', 'posts'));
return categories.reduce((accMetadata, category) => {
const categoryMetadata = readdirSync(
path.resolve(process.cwd(), 'public', 'posts', category),
).map(fileName => readMetadata(category, fileName));
return [...accMetadata, ...categoryMetadata];
}, []);
};
동적 사이트맵 생성하기
사이트맵 자동화를 위해서는 정적 사이트맵 파일을 자동화하는 것보다 코드를 사용하여 매 빌드마다 알아서 생성하도록 하는 편이 좀 더 깔끔할 것 같아서 사이트맵을 생성하는 코드를 작성할 겁니다.
공식에서 소개하고 있다시피 코드를 사용하여 사이트맵을 생성할 때는 app/sitemap.ts
파일 내에 사이트맵을 생성하는 코드를 작성합니다. (app router 기준)
제가 사이트맵에 등록해야 할 경로는 블로그 메인과 블로그 글 이렇게 두 가지입니다. 이 두 페이지의 사이트맵을 생성하는 코드는 아래와 같이 작성할 수 있습니다.
import type { MetadataRoute } from 'next';
const baseUrl = 'https://lah1203.vercel.app';
export default function sitemap(): MetadataRoute.Sitemap {
const posts = readAllPostsMetadata();
return [
{
url: baseUrl,
lastModified: new Date().toISOString().split('T')[0],
changeFrequency: 'daily',
},
...posts.map(post => ({
url: `${baseUrl}/post/${post.category}/${post.fileName.split('.')[0]}`,
lastModified: post.date.toISOString().split('T')[0],
changeFrequency: 'daily',
priority: 0.7,
images: [post.thumbnail],
})),
];
}
저는 블로그 링크를 굳이 숨길 필요는 없어서 상수화시켜 적용했는데요. 필요하다면 환경 변수로 관리하여 숨겨도 좋을 것 같습니다.
저렇게 작성한 후 빌드하면 블로그 링크/sitemap.xml
로 접속하여 생성된 사이트맵을 볼 수 있습니다. 예를 들면, 제 블로그의 사이트맵은 아래와 같이 생성됩니다. (일부 잘라냄)
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
<script />
<url>
<loc>https://lah1203.vercel.app</loc>
<lastmod>2025-02-03</lastmod>
<changefreq>daily</changefreq>
</url>
<url>
<loc>https://lah1203.vercel.app/post/FE/2025-02-03-블로그에-SEO-적용하기</loc>
<image:image>
<image:loc>https://github.com/user-attachments/assets/b6067f3b-f4f3-4516-9429-0b3997c3550d</image:loc>
</image:image>
<lastmod>2025-02-03</lastmod>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://lah1203.vercel.app/post/회고/2025-01-23-2024년-회고</loc>
<image:image>
<image:loc>https://github.com/user-attachments/assets/e89a20c2-fc49-4b86-a0b8-e1426d9cc8fa</image:loc>
</image:image>
<lastmod>2025-01-23</lastmod>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
</urlset>
이렇게 하고 구글서치콘솔에 블로그를 등록하고 만든 sitemap url을 등록해두면 됩니다.
사이트 구성하기
마찬가지로 사이트를 논리적으로 구성하면 검색엔진과 사용자가 페이지의 내용을 유추하거나 이해하는 데 도움이 되기 때문에 초기에 미리 논리적으로 구성해두는 게 좋습니다.
구글은 사이트를 구성할 때 다음과 같은 방식을 추천합니다.
- 설명 URL 사용하기
- 사이트를 재미있고 유용하게 만들기
- Google 검색에 사이트가 표시되는 방식에 영향 미치기
- 사이트에 이미지 추가 및 최적화하기
- 동영상 최적화하기
- 웹사이트 홍보하기
여기에서는 설정을 통해 바로 적용해볼 수 있는 1, 3번을 다뤄보려고 합니다.
설명 URL 사용하기
사용자는 URL을 보고 자신에게 도움이 될 지 여부를 스스로 판단할 수 있습니다. 그렇기 때문에 사용자에게 도움이 될 만한 단어를 URL에 포함시키는 게 좋습니다. 임의의 식별자만 포함된 URL은 사용자에게 도움이 되지 않습니다.
예를 들면 다음과 같습니다.
good : https://www.example.com/pets/cats.html
bad : https://www.example.com/2/6772756D707920636174
또한 디렉터리에서 주제별로 유사한 페이지를 그룹화하는 것이 좋습니다.
사이트에 URL이 수천 개 이상 포함되어 있다면 콘텐츠를 구성하는 방식이 크롤링 및 색인 생성에 영향을 미칠 수 있는데, 특히 폴더를 사용하여 비슷한 주제를 그룹화하면 개별 디렉터리의 URL이 변경되는 빈도를 파악하는 데 도움이 됩니다.
예를 들어 자주 변경되지 않는 콘텐츠를 A, 자주 변경되는 콘텐츠를 B에 두었다면 구글은 이 정보를 학습하여 각 디렉터리를 서로 다른 빈도로 크롤링할 수 있습니다. 이때 sitemap의 changeFrequency를 떠올릴 수도 있겠으나, 이는 어디까지나 콘텐츠가 변경되는 빈도이지 크롤링해가는 빈도가 아니기 때문에 이 부분을 간과해서는 안됩니다.
위 두 가지 예시를 참고하여 저는 블로그 링크의 구조를 잡을 때 아래의 기준을 세웠습니다.
- 모든 글은
/post
하위에 둘 것 - 각 글의 URL에 카테고리와 제목을 포함할 것
그래서 제 블로그의 모든 글은 ${BASE_URL}/post/category/title
의 구조로 링크가 만들어집니다.
Google 검색에 사이트가 표시되는 방식에 영향 미치기
여기에서는 title과 description 같은 메타데이터의 중요성을 언급합니다.
일반적인 Google 검색 결과 페이지는 몇 가지 시각적 요소로 구성되어 있으며 사용자가 검색 결과를 통해 사이트를 방문할지 결정하는 데 도움을 주도록 영향을 미칠 수 있습니다. 이때 메타데이터를 적절하게 설정하면 검색엔진에서 이를 읽어 표시하므로, 사용자에게 하여금 콘텐츠에 대한 이해를 도울 수 있겠죠.
기본적으로 title, description만 수정한 상태에서 배포했을 때는 아래 이미지처럼 title, description 외에는 모두 default로 나옵니다.
여기에서는 1. 블로그 로고가 보이지 않는다는 점
, 2. Vercel로 설정된 기본 타이틀이 나온다는 점
이 가장 먼저 보이네요.
메타데이터로 이미지와 타이틀을 설정해보겠습니다.
Next.js에서 기본 메타데이터 설정하기
Next.js에서는 정적 메타데이터를 설정할 때 metadata 객체를 만들어 export해도 인식할 수 있도록 제공하고 있습니다.
사이트의 기본 메타데이터를 동적으로 설정할 필요성은 못 느꼈기에 정적 메타데이터 설정 방식인 app/layout.tsx
에서 metadata 객체를 만들어 export하는 방식을 사용했습니다.
export const metadata = {
title: '거북이로그',
description: '프론트엔드 개발자 이아현의 블로그',
openGraph: {
type: 'website',
title: '거북이로그',
description: '프론트엔드 개발자 이아현의 블로그',
locale: 'ko',
url: 'https://lah1203.vercel.app',
images: [
{
url: 'https://github.com/user-attachments/assets/21af14de-59cb-4848-a058-5476e4bcb2b5',
},
],
},
authors: {
url: 'https://github.com/LAH1203',
name: '이아현',
},
};
간단하게 기본 메타데이터만 설정했습니다. 가볍게만 설명해볼게요.
title, description
title과 description은 크롤러 봇이 사이트 정보를 수집해갈 때 공통적으로 수집해가는 가장 기본이 되는 정보입니다. title의 경우에는 실제 사이트의 상단에서 볼 수 있는 정보이기도 하죠.
openGraph
openGraph는 흔히 og 메타데이터라고 부르는 항목입니다. 다양한 매체를 통해 사이트의 링크를 공유할 때 아래 이미지처럼 카드 형식으로 사이트에 대한 정보가 뜨곤 하는데, 이때 많이들 사용하는 게 바로 이 og 메타데이터입니다.
Next.js에서는 openGraph를 설정하면 동일한 메타데이터 태그를 og:
, twitter:
로 각각 생성하여 넣어줍니다.
author
author은 그냥 괜찮아보여서 넣어봤습니다. 이 사이트의 게시자가 누구인지를 명시하는 부분이에요.
Next.js에서 동적 메타데이터 설정하기
추가로 각 글에서는 동적으로 메타데이터가 설정되면 좋을 것 같다고 생각했습니다. 이를 위해 Next.js에서 제공하는 generateMetadata를 활용했습니다.
export const generateMetadata = async ({ params }) => {
const metadata = await params;
const category = decodeURIComponent(metadata.category);
const fileName = `${decodeURIComponent(metadata.fileName)}.md`;
const post = await readMetadata(category, fileName);
if (!category || !fileName || !post) return null;
const { title, description, thumbnail } = post;
return {
title: `${title} : 🐢`,
description,
openGraph: {
type: 'article',
title: `${title} : 🐢`,
description,
locale: 'ko',
url: `https://lah1203.vercel.app/${metadata.category}/${metadata.fileName}`,
images: [
{
url: thumbnail,
},
],
},
};
};
위에서도 설명했다시피 각 블로그 글 페이지에서는 링크의 파라미터에서 카테고리, 파일명을 가지고 오고 있습니다. 그리고 이 정보를 활용하면 글의 메타데이터를 읽어올 수 있습니다.
그래서 generateMetadata에서는 읽어온 글의 메타데이터에서 필요한 정보만 활용하여 가공 후 반환합니다.
End?
이렇게 기본적인 설정을 마치고 현재 구글서치콘솔에 사이트를 등록해두었습니다.
이전에는 직접 만든 블로그의 사이트맵을 봇이 제대로 찾지 못하는 문제를 겪고 있었는데, 이번에도 그럴지는 좀 더 지켜봐야 할 것 같습니다. 제발 구글이 정신차려서 잘 읽어와줬으면 좋겠네요 🙈