학습 자료

카카오톡에 링크를 붙이면 무엇이 뜨는가


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()로 감쌌다면 요청은 한 번만 나간다.

두 번 부르지만 한 번만 나간다
  1. generateMetadatagetPost(1) — 실제 요청
  2. 메모에 적어둔다같은 렌더링 동안 유효
  3. Post 컴포넌트getPost(1) — 적어둔 것을 받는다

generateMetadatametadata같은 파일에서 둘 다 내보낼 수는 없다. 하나만 고른다.

링크 미리보기를 만든다

카카오톡, 슬랙, 트위터가 읽는 것은 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 안에 viewportthemeColor를 넣었다. 지금은 분리됐다.

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으로 제목 틀을 정하고, defaultabsolute로 예외를 둔다.
  • 데이터에 따라 달라지면 **generateMetadata**를 쓴다. metadata와 함께 쓸 수 없다.
  • 컴포넌트와 중복 호출돼도 요청 메모이제이션이 요청을 하나로 합친다.
  • metadataBase를 선언한다. 없으면 OG 이미지가 상대경로로 나가 미리보기가 깨진다.
  • opengraph-image.jsx로 이미지를 코드로 굽는다. flex만 되고 grid는 안 된다.
  • viewportthemeColormetadata가 아니라 별도 export다.
  • sitemap.jsrobots.js는 객체를 반환하면 XML과 텍스트가 된다.
  • JSON-LD는 메타데이터 API에 없다. 서버 컴포넌트 안에 <script>로 직접 넣는다.

검색봇이 우리 페이지를 잘 읽게 됐다. 그런데 사용자가 느리다고 느낀다면 소용없다.

다음 강의에서는 페이지에서 가장 무거운 두 가지를 다룬다. 이미지와 폰트다. Next 16에서 둘 다 기본값이 바뀌었고, 그중 하나는 우리가 늘 쓰던 prop이 사라졌다.

카카오톡에 링크를 붙이면 무엇이 뜨는가 — 디코드랩(DCODELAB)