SEO에 강한 면모를 보이는 Next.js를 입문하는 김에, 동적 SEO 생성에 도전해보고 싶어졌다.
컨텐츠의 검색 노출이 매우 중요한 뉴스, 블로그, EC 사이트 등의 생명줄이라고 할 수 있는 동적 SEO, 어떻게 생성해볼 수 있을까.
지금까지 연구하고 적용에 성공한 내용들을 기록한다.
관련 상식들을 하나씩 클리어해보자.
The title tag and meta description (otherwise known as the “SEO title” and “SEO description”) inform Google and other search engines about what the topic of your website is about. This information also shows up in the search results for users to see, and optimizing it can help them click through to your site. - https://revenuezen.com/seo-titles-meta-descriptions/
title과 description는 검색시 수집되는 정보로써 매우 중요도가 높으며, 검색 결과를 표시하는데에도 활용된다. 내용을 가장 잘 설명하는 문구로 작성되야만 고객의 클릭을 성공적으로 유도할 수 있다.
title 데이터는 이렇게 탭 이름도 동적으로 바꿔줌으로써 편리한 웹서핑 경험을 제공하는데 일조한다. Amazon에서 5가지 책 중 구매를 고민하는데 탭의 이름이 모두 Amazon, Amazon, Amazon, Amazon, Amazon 이라면 무척 곤란할 것이다.
공식 문서 에서 안내하는 메타데이터 작성법은 다음과 같다.
import { Metadata } from 'next'
// either Static metadata 정적 메타데이터
export const metadata: Metadata = {
title: '...',
}
// or Dynamic metadata 동적 메타데이터
export async function generateMetadata({ params }) {
return {
title: '...',
}
}
export const metadata ... 를 통해서 정적 메타데이터 생성이 가능하다. "홈, 회사 소개, 인삿말, 오시는 길"과 같은 정적 페이지들에 활용 가능할 것이다.
export async function generateMetadata ... 를 통해서 동적 메타데이터 생성이 가능하다. "상품 상세, 블로그 글, 뉴스 기사" 등의 동적 페이지에 활용 가능할 것이다.
metadata는 server component에서만 생성된다.
또한 나중에 생성되는 metadate객체에 의해 내부 값이 갱신된다. 예를들어 유저가 홈 => 회사소개로 이동한다고 할 때, 홈 메타데이터에서 회사소개 메타데이터로 표시정보가 변화한다.
metadata와 generateMetadata가 같은 라우트의 컴포넌트에서 동시에 사용될 수 없다. 상식적으로 가능해선 안될 것이다.
이제 실제 작성에 돌입해보도록 하자.
정적 메타데이터 설정은 매우 심플하다.
create next app으로 앱 작성시에 보일러플레이트 상에 이미 export const metadata가 layout.tsx에 작성되어있을 것이다.
initial commit시 layout.tsx의 내부
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
각각을 웹사이트를 대표하는 타이틀과 설명으로 수정해준다.
export const metadata: Metadata = {
title: "IT 북스 파인더",
description: "키워드 검색을 통해 찾고 계신 IT도서 정보를 검색해보세요.",
};
다음으로 이 글의 주인공인 동적 메타데이터를 생성해보자.
대상이 될 url은 http://localhost:3000/detail/9781098111878
과 같은 디테일페이지이다. 참고로 /detail/:isbn
같은 동적 라우팅을 위해 작성된 page.tsx의 위치는 다음과 같다.
/app/detail/[isbn]/page.tsx
동적 라우팅을 위해서는 대괄호가 필수이며, 또한 컴포넌트 내부에서 url의 params를 이용하고 싶을 경우 대괄호 내부의 이름과 일치하는 값을 이용해 접근 가능한 점을 알아두자.
const isbn13 = params.isbn;
기존 page.tsx의 내부 모습은 다음과 같다.
import fetchBookDetail from "@/apis/fetchBookDetail";
import Loading from "@/shared/Loading";
import Detail from "@/components/pages/Detail";
interface Props {
params: { isbn: string };
}
async function DetailPage({ params }: Props) {
const isbn13 = params.isbn;
// url의 동적 params부분인 "9781098111878"에 접근.
const bookDetail = await fetchBookDetail(isbn13);
// "9781098111878"를 인자로 넘겨주어 데이터 fetch.
return (
<>
<Detail isbn13={isbn13} bookDetail={bookDetail} />
// UI를 그려주는 서버컴포넌트에 데이터를 전달
</>
);
}
export default DetailPage;
fetchBookDetail함수가 많은 곳에서 중복되지 않도록 api폴더 내부에 추출해놓았다. 내부는 아래와 같다.
async function fetchBookDetail(isbn13: string) {
try {
const response = await fetch(
`https://api.itbook.store/1.0/books/${isbn13}`,
);
return await response.json();
} catch (error) {
console.error("Error fetching book data:", error);
throw error;
}
}
export default fetchBookDetail;
다음으로 공식 홈페이지의 작성법을 참고하여 기존 서버 컴포넌트의 위 편에 generateMetadata를 추가로 작성해주었다.
import { Metadata } from "next";
// next가 제공하는 Metadata interface를 임포트.
import fetchBookDetail from "@/apis/fetchBookDetail";
import Loading from "@/shared/Loading";
import Detail from "@/components/pages/Detail";
interface Props {
params: { isbn: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const isbn13 = params.isbn;
try {
const data = await fetchBookDetail(isbn13);
return {
title: data.title,
description: data.desc,
};
} catch (error) {
console.error("error generating MetaData:", error);
return {
title: "Error",
description: "Failed to fetch book detail.",
};
}
}
async function DetailPage({ params }: Props) {
// ... 기존 로직
generateMetadata는 미리 한번의 fetch를 통해 사전에 metaData를 얻고 html을 렌더하는 식으로 동작한다.
여기서 같은 url에 대한 fetch가 두번 요청되는 것에 찜찜한 기분이 들었으나 Next.js는 다 계획이 있었다.
fetch requests are automatically memoized for the same data across generateMetadata, generateStaticParams, Layouts, Pages, and Server Components. React cache can be used if fetch is unavailable. - 공식문서
따라서 첫번째 fetch시 외부 api url에 요청한 것이 cache되어 두번째 fetch에서 같은 외부 api url에 요청한것이 감지될 경우 메모리에 저장된 결과 값을 이용하게 된다.
캐시가 잘 되고 있는지는 .next/cache/fetch-cache에서 확인할 수 있다!
도서 클릭과 동시에 요청에 대한 캐시값이 생성되는 것을 볼 수 있다. 속이 시원하다.
추가로, axios의 경우는 수동으로 cache(react 메소드)를 통해 감싸주어야만 한다. Next는 fetch에 cache를 포함한 다양한 확장 기능을 마련해놓았으므로 Next.js로 프로젝트 구현시에는 axios보다 fetch를 되도록 사용하는 것이 좋을 것 같다.
여기까지 완료하고나면 이제 title과 description이 동적으로 변하는것을 개발자도구를 통해 확인할 수 있을 것이다!
위와 같은 예쁜 카드를 만들기 위한 open graph, 일명 og.를 generateMetadata를 이용해 간단히 생성해보자.
먼저 아래 extention을 설치하기를 강력 추천한다.
https://socialsharepreview.com/browser-extensions
배포가 아닌 local 상태에서도 프리뷰를 보여주기에 개발시 매우 유용하다.
🔺 일부러 generateMetadata 내부 로직을 주석화해주면 볼 수 있는 결과.
동적메타데이터 작성이 안되는 바람에 전역으로 설정된 정적 메타데이터가 노출되고 있다.
If metadata doesn't depend on runtime information, it should be defined using the static metadata object rather than generateMetadata.
또한 정의된 사진이 없다고 경고가 나오는 모습이다.
Oh no - there's no Share Image defined!
api 요청을 통해 얻는 데이터 꾸러미에는 이미지도 존재하므로 그대로 오픈그래프에 활용해주면 간단히 카드의 퀄리티를 높일 수 있다.
기존의 단순했던 generateMetadata의 리턴값을 아래처럼 바꾸어보자.
return {
metadataBase: new URL("http://localhost:3000"),
title: data.title,
description: data.desc,
openGraph: {
images: [
{
url: data.image,
width: 800,
height: 600,
},
],
},
};
metadataBase의 경우 설정해주지 않으면 아래와 같은 에러가 서버 콘솔에 등장한다.
metadata.metadataBase is not set for resolving social open graph or twitter images, using "http://localhost:3000". See https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadatabase
metadataBase를 설정해줘야만 Next.js는 해당 기본 URL을 사용하여 상대 경로 이미지의 절대 경로를 생성해준다. 실제 프로덕션 환경에서는 metadataBase를 실제 도메인 주소로 설정해야하는 것을 참고하자. 현재는 local이므로 http://localhost:3000을 넣어주었다.
이미지 추가를 통해 그럴듯한 카드가 완성되었다. 👍
탭에도 메타데이터의 title이 들어가 있어서 SEO를 만져준 것만으로도 완성도가 확 높아지는 것을 확인할 수 있다.