카카오톡에 링크를 붙이면 무엇이 뜨는가
Article
15강까지 페이지를 여러 개 만들었다. 브라우저 탭을 보면 전부 같은 제목이다. Ctrl+U로 소스를 열어보면 <title>이 create-next-app이 넣어둔 그대로다.
친구에게 링크를 보내본다. 카카오톡이 미리보기를 못 만든다. 회색 상자에 주소만 뜬다.
HTML의 <head>를 채운 적이 없다.
객체를 내보내면 head 가 채워진다
// app/about/page.js
export const metadata = {
title: "소개",
description: "웹 퍼블리셔를 위한 Next.js 교육",
};
export default function About() {
return <h1>소개</h1>;
}
metadata라는 이름으로 내보내면 Next.js가 알아본다. 4강에서 배운 파일 이름이 곧 약속이라는 규칙이, 여기서는 export 이름이 곧 약속이 된다.
Next.js가 이걸 태그로 바꿔 <head>에 넣는다.
- <title>소개</title>
- <meta name="description" content="웹 퍼블리셔를 위한 Next.js 교육"/>
<head>를 직접 쓰지 않는다. react-helmet 같은 라이브러리도 필요 없다.
제목에 규칙을 준다
페이지마다 "소개 | DCODELAB"이라고 쓰기는 번거롭다. 레이아웃에서 틀을 정한다.
// app/layout.js
export const metadata = {
title: {
template: "%s | DCODELAB",
default: "DCODELAB — Next.js 교육",
},
description: "웹 퍼블리셔를 위한 실무 기술 블로그",
};
| 페이지가 정한 title | 실제 <title> |
|---|---|
| "소개" | 소개 | DCODELAB |
| 없음 | DCODELAB — Next.js 교육 |
| { absolute: "단독 제목" } | 단독 제목 |
default는 자식이 제목을 안 정했을 때 쓰인다. absolute는 틀을 무시한다.
메타데이터는 레이아웃에서 페이지로 내려오며 합쳐진다. 레이아웃에 description을 쓰고 페이지에서 덮어쓰지 않으면 그대로 상속된다.
글마다 다른 제목
5강에서 잠깐 본 generateMetadata다. 값이 데이터에 따라 달라질 때 쓴다.
export async function generateMetadata({ params }) {
const { id } = await params;
const post = await getPost(id);
if (!post) return { title: "찾을 수 없음" };
return {
title: post.title,
description: post.summary,
};
}
export default async function Post({ params }) {
const { id } = await params;
const post = await getPost(id);
return <article>{post.body}</article>;
}
await params가 여기서도 필요하다. 그리고 getPost(id)가 두 번 불린다.
5강에서 미뤄뒀던 그 질문이다. 답은 10강에서 나왔다. 요청 메모이제이션이 둘을 하나로 합친다. fetch를 쓰거나 cache()로 감쌌다면 요청은 한 번만 나간다.
- generateMetadatagetPost(1) — 실제 요청
- 메모에 적어둔다같은 렌더링 동안 유효
- Post 컴포넌트getPost(1) — 적어둔 것을 받는다
generateMetadata와 metadata를 같은 파일에서 둘 다 내보낼 수는 없다. 하나만 고른다.
링크 미리보기를 만든다
카카오톡, 슬랙, 트위터가 읽는 것은 Open Graph 태그다.
export const metadata = {
metadataBase: new URL("https://www.dcodelab.kr"),
title: "소개",
description: "웹 퍼블리셔를 위한 Next.js 교육",
openGraph: {
title: "소개 | DCODELAB",
description: "웹 퍼블리셔를 위한 Next.js 교육",
url: "/about",
siteName: "DCODELAB",
images: [{ url: "/og/about.png", width: 1200, height: 630 }],
locale: "ko_KR",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "소개 | DCODELAB",
images: ["/og/about.png"],
},
};
metadataBase가 핵심이다. 이게 없으면 /og/about.png가 그대로 나간다.
이미지 크기는 1200 × 630이 사실상 표준이다. 그리고 로컬에서는 확인할 수 없다. 크롤러가 localhost에 접근할 수 없기 때문이다. 배포한 뒤 각 서비스의 디버거로 확인한다.
OG 이미지를 코드로 만든다
글마다 이미지를 만들 수는 없다. 제목을 그려 넣은 이미지를 코드로 생성한다.
// app/posts/[id]/opengraph-image.jsx
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function Image({ params }) {
const { id } = await params;
const post = await getPost(id);
return new ImageResponse(
(
<div style={{
width: "100%", height: "100%",
display: "flex", alignItems: "center", justifyContent: "center",
background: "#0a0a0a", color: "#fff", fontSize: 64,
}}>
{post.title}
</div>
),
size
);
}
opengraph-image.jsx도 약속된 이름이다. 그 폴더의 페이지에 자동으로 OG 이미지가 붙는다.
JSX로 그리지만 브라우저가 아니다. Next.js가 이걸 렌더링해 PNG로 굽는다. 그래서 쓸 수 있는 CSS가 제한된다.
viewport 는 따로 내보낸다
예전에는 metadata 안에 viewport와 themeColor를 넣었다. 지금은 분리됐다.
export const viewport = {
width: "device-width",
initialScale: 1,
themeColor: "#0a0a0a",
};
metadata 안에 넣으면 경고가 뜨고 무시된다. 값이 데이터에 따라 달라진다면 generateViewport를 쓴다.
사이트맵과 robots
검색엔진에게 "우리 사이트에 이런 주소들이 있다"고 알려주는 파일이다. 이것도 코드로 만든다.
// app/sitemap.js
export default async function sitemap() {
const posts = await getPosts();
const postUrls = posts.map(post => ({
url: `https://www.dcodelab.kr/posts/${post.id}`,
lastModified: post.updatedAt,
}));
return [
{ url: "https://www.dcodelab.kr", lastModified: new Date() },
{ url: "https://www.dcodelab.kr/about" },
...postUrls,
];
}
// app/robots.js
export default function robots() {
return {
rules: { userAgent: "*", allow: "/", disallow: "/admin/" },
sitemap: "https://www.dcodelab.kr/sitemap.xml",
};
}
객체를 반환하면 Next.js가 XML과 텍스트로 바꿔 /sitemap.xml, /robots.txt에 띄운다.
| 파일 | 만들어지는 주소 |
|---|---|
| app/sitemap.js | /sitemap.xml |
| app/robots.js | /robots.txt |
| app/opengraph-image.jsx | /opengraph-image |
| app/icon.png | <link rel="icon"> |
| app/apple-icon.png | <link rel="apple-touch-icon"> |
구조화 데이터
검색 결과에 별점이나 작성일이 함께 뜨는 것을 본 적이 있을 것이다. JSON-LD로 알려준다.
메타데이터 API에는 이 항목이 없다. 컴포넌트 안에 <script>로 직접 넣는다.
export default async function Post({ params }) {
const { id } = await params;
const post = await getPost(id);
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
datePublished: post.createdAt,
author: { "@type": "Person", name: "김개발" },
};
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<h1>{post.title}</h1>
</article>
);
}
서버 컴포넌트라서 이 <script>는 HTML에 그대로 박힌다. 크롤러가 자바스크립트를 실행하지 않아도 읽는다.
3강에서 "페이지 소스에 내용이 들어 있다"고 확인했던 것이 여기서 값을 한다.
정리하면
export const metadata객체를 내보내면 Next.js가<head>를 채운다. 라이브러리가 필요 없다.- 클라이언트 컴포넌트에서는 내보낼 수 없다. 메타데이터는 서버에서 결정된다.
title.template으로 제목 틀을 정하고,default와absolute로 예외를 둔다.- 데이터에 따라 달라지면 **
generateMetadata**를 쓴다.metadata와 함께 쓸 수 없다. - 컴포넌트와 중복 호출돼도 요청 메모이제이션이 요청을 하나로 합친다.
metadataBase를 선언한다. 없으면 OG 이미지가 상대경로로 나가 미리보기가 깨진다.opengraph-image.jsx로 이미지를 코드로 굽는다.flex만 되고grid는 안 된다.viewport와themeColor는metadata가 아니라 별도 export다.sitemap.js와robots.js는 객체를 반환하면 XML과 텍스트가 된다.- JSON-LD는 메타데이터 API에 없다. 서버 컴포넌트 안에
<script>로 직접 넣는다.
검색봇이 우리 페이지를 잘 읽게 됐다. 그런데 사용자가 느리다고 느낀다면 소용없다.
다음 강의에서는 페이지에서 가장 무거운 두 가지를 다룬다. 이미지와 폰트다. Next 16에서 둘 다 기본값이 바뀌었고, 그중 하나는 우리가 늘 쓰던 prop이 사라졌다.