본문 바로가기
SPA/Next

[Next] Next13의 새로운 기능 알아보기 (5) - Metadata

by J4J 2023. 6. 21.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 Next13의 새로운 기능 알아보기 마지막인 Metadata에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

들어가기에 앞서

 

Opengraph 이미지 적용을 위해 아래 이미지를 활용할 예정입니다.

 

테스트가 필요하신 분은 해당 경로를 이용하여 동일하게 사용해 보셔도 무방합니다.

 

 

 

Metadata Image

 

 

반응형

 

 

기본 설정

 

next13 이전에 metadata 설정을 위해 일반적으로 _document 파일을 활용하거나 layout을 이용하여 적용해 볼 수 있었습니다.

 

간단한 예시로 다음과 같이 코드를 작성해볼 수 있습니다.

 

// src/layouts/metadata/layout.tsx
import Head from 'next/head';
import { ReactNode } from 'react';

interface Props {
    children: ReactNode;
}

const MetadataLayout = (props: Props) => {
    return (
        <>
            <Head>
                <title>title 테스트</title>
                <meta name="description" content="description 테스트" />

                <meta property="og:title" content="og title 테스트" />
                <meta property="og:description" content="og description 테스트" />
                <meta property="og:url" content="https://jforj.tistory.com" />
                <meta property="og:site_name" content="og site name 테스트" />
                <meta
                    property="og:image"
                    content="https://blog.kakaocdn.net/dn/kT8fV/btskKl5skJt/XcqUQY5A7DDGcqU4iIY9P0/img.png"
                />
                <meta property="og:image:width" content="800" />
                <meta property="og:image:height" content="600" />
                <meta property="og:type" content="website" />

                <meta name="twitter:card" content="summary" />
                <meta name="twitter:title" content="twitter title 테스트" />
                <meta name="twitter:description" content="twitter description 테스트" />
                <meta
                    name="twitter:image"
                    content="https://blog.kakaocdn.net/dn/kT8fV/btskKl5skJt/XcqUQY5A7DDGcqU4iIY9P0/img.png"
                />
                <meta name="twitter:image:width" content="800" />
                <meta name="twitter:image:height" content="600" />
            </Head>

            {props.children}
        </>
    );
};

export default MetadataLayout;


// src/pages/metadata/index.tsx
import MetadataLayout from '../../layouts/metadata/layout';

const Metadata = () => {
    return (
        <MetadataLayout>
            <div>
                <h2>Metadata Page</h2>
            </div>
        </MetadataLayout>
    );
};

export default Metadata;

 

 

 

위와 동일한 역할을 할 수 있는 metadata 설정을 next 13.2부터 더 간편하게 적용해 볼 수 있습니다.

 

next13 이전처럼 return 값의 head에 담아서 metadata를 설정해줄 필요 없이 metadata 객체를 생성하여 export만 해주면 동일하게 적용이 가능합니다.

 

또한 타입까지 활용하면 기존에 하나하나 작성했을 때 발생될 수 있었던 문제들도 방지할 수 있습니다.

 

// src/app/metadata/layout.tsx
import { Metadata } from 'next';
import { ReactNode } from 'react';

export const metadata: Metadata = {
    title: 'title 테스트',
    description: 'description 테스트',
    openGraph: {
        title: 'og title 테스트',
        description: 'og description 테스트',
        url: 'https://jforj.tistory.com',
        siteName: 'og site name 테스트',
        images: [
            {
                url: 'https://blog.kakaocdn.net/dn/kT8fV/btskKl5skJt/XcqUQY5A7DDGcqU4iIY9P0/img.png',
                width: 800,
                height: 600,
            },
        ],
        type: 'website',
    },
    twitter: {
        card: 'summary',
        title: 'twitter title 테스트',
        description: 'twitter description 테스트',
        images: [
            {
                url: 'https://blog.kakaocdn.net/dn/kT8fV/btskKl5skJt/XcqUQY5A7DDGcqU4iIY9P0/img.png',
                width: 800,
                height: 600,
            },
        ],
    },
};

interface Props {
    children: ReactNode;
}

const MetadataLayout = (props: Props) => {
    return <div>{props.children}</div>;
};

export default MetadataLayout;


// src/app/metadata/page.tsx
const MetadataPage = () => {
    return (
        <div>
            <h2>Metadata Page</h2>
        </div>
    );
};

export default MetadataPage;

 

 

 

 

Children에서 재 설정

 

next13 이전에는 자식 페이지에서 metadata를 재 설정할 경우 다음과 같이 부모가 가지고 있던 값과 함께 중복되어 표기되는 것을 볼 수 있습니다.

 

// src/pages/metadata/children.tsx
import Head from 'next/head';
import MetadataLayout from '../../layouts/metadata/layout';

const MetadataChildren = () => {
    return (
        <MetadataLayout>
            <Head>
                <title>children title 테스트</title>
                <meta name="description" content="children description 테스트" />

                <meta property="og:title" content="og children title 테스트" />
                <meta property="og:description" content="og children description 테스트" />
                <meta property="og:site_name" content="og children site name 테스트" />
            </Head>

            <div>
                <h2>Metadata Children Page</h2>
            </div>
        </MetadataLayout>
    );
};

export default MetadataChildren;

 

next13 이전 children 페이지

 

 

 

next13 이후로는 위처럼 중복되어 표기되지 않고 재 정의하여 사용됩니다.

 

새롭게 설정이 된 값들은 부모에서 사용된 값을 무시하고 자식에서 설정한 값으로 적용되며, 설정하지 않은 값들은 부모에서 사용된 것이 그대로 적용됩니다.

 

// src/app/metadata/children/layout.tsx
import { Metadata } from 'next';
import { ReactNode } from 'react';

// 따로 설정을 하지 않으면 상위 Layout에 설정한 metadata 적용
export const metadata: Metadata = {
    title: 'children title 테스트',
    description: 'children description 테스트',
    openGraph: {
        title: 'og children title 테스트',
        description: 'og children description 테스트',
        siteName: 'og children site name 테스트',
    },
};

interface Props {
    children: ReactNode;
}

const MetadataChildrenLayout = (props: Props) => {
    return <div>{props.children}</div>;
};

export default MetadataChildrenLayout;


// src/app/metadata/children/page.tsx
const MetadataChildrenPage = () => {
    return (
        <div>
            <h2>Metadata Children Page</h2>
        </div>
    );
};

export default MetadataChildrenPage;

 

next13 이후 children 페이지

 

 

 

 

OG Image Generation

 

next13 이전에 opengraph 이미지에 대한 값을 설정할 때 일반적으로 위에서 한 것처럼 이미지 url을 meta 태그에 넣어 사용해 왔습니다.

 

그렇기 때문에 만약 설정해야 되는 이미지를 변경할 때 url에 해당되는 이미지를 변경하거나 또는 새로운 url을 만들어 새롭게 적용해줘야 했습니다.

 

 

 

next13부터는 opengraph 이미지를 보다 편리하게 커스텀하여 활용할 수 있습니다.

 

또한 Next 공식 문서에 의하면 5배 더 빠른 속도를 제공하기 때문에 기존에 종종 발생되었던 이미지가 누락되거나 스킵되는 현상을 방지해 준다고 합니다.

 

적용 방법은 다음과 같이 api routes를 이용하여 opengraph 이미지에 보여야 하는 화면을 그려주고, 해당 경로를 metadata에 추가해주기만 하면 됩니다.

 

// src/pages/api/og.tsx
import { ImageResponse } from 'next/server';

export const config = {
    runtime: 'edge',
};

export default function () {
    return new ImageResponse(
        (
            <div
                style={{
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center',
                    fontSize: 128,
                    background: 'white',
                    width: '100%',
                    height: '100%',
                }}
            >
                OG Image Generation!
            </div>
        ),
    );
}


// src/app/metadata/ogImage/layout.tsx
import { Metadata } from 'next';
import { ReactNode } from 'react';

// API Routes를 활용하여 OG Image Generation 적용
export const metadata: Metadata = {
    openGraph: {
        images: [
            {
                url: '/api/og',
                width: 300,
                height: 150,
            },
        ],
    },
};

interface Props {
    children: ReactNode;
}

const MetadataOgImageLayout = (props: Props) => {
    return <div>{props.children}</div>;
};

export default MetadataOgImageLayout;


// src/app/metadata/ogImage/page.tsx
const MetadataOgImagePage = () => {
    return (
        <div>
            <h2>Metadata OgImage Page</h2>
        </div>
    );
};

export default MetadataOgImagePage;

 

 

 

페이지에 접근해보면 다음과 같이 opengraph metadata 설정이 적용되어 있는 것을 확인할 수 있습니다.

 

OG Image Generation 적용 metadata

 

 

 

설정된 이미지 url을 들어가 보면 api routes에 설정했던 화면대로 이미지가 보이고 있는 것도 확인해 볼 수 있습니다.

 

OG Image Generation 적용 이미지

 

 

 

 

파일 기반 이미지

 

next13 이후부터는 opengraph 이미지를 적용할 때 프로젝트 내부에 있는 정적 파일만을 활용하여 설정할 수도 있게 됩니다.

 

사용 방법은 원하는 페이지의 경로에 다음과 같이 "opengraph-image.png", "twitter-image.png"와 같이 정적 이미지 파일을 넣어주면 그 외 설정은 필요 없이 이미지의 정보에 맞게 자동으로 metadata가 설정되는 것을 확인할 수 있습니다.

 

파일 기반 이미지 폴더 구조

 

// src/app/metadata/fileOg/page.tsx
const MetadatFileOgPage = () => {
    return (
        <div>
            <h2>Metadata FileOg Page</h2>
        </div>
    );
};

export default MetadatFileOgPage;

 

파일 기반 이미지 자동 적용 metadata

 

 

 

이 외에도 위와 같이 이름만 올바르게 매핑을 하면 적용해볼 수 있는 것들이 아래에 더 있으니 필요하신 분들은 활용해 보시면 될 것 같습니다.

 

파일 기반 metadata 적용

 

 

 

 

Dynamic Image Generation

 

OG Image Generation과 파일 기반 이미지 적용하는 것을 섞어 놓은 듯한 방식으로 opengraph 이미지를 추가할 수 있는 방법도 있습니다.

 

Dynamic Image Generation은 파일 기반 이미지 적용하는 것처럼 "opengraph-image.tsx", "twitter-image.tsx"와 같이 파일 명을 구성하고 내부 소스 코드에는 OG Image Generation에서 api routes에 작성하던 것처럼 활용해 볼 수 있습니다.

 

dynamic image generation 폴더 구조

 

// src/app/metadata/dynamicOg/opengraph-image.tsx
import { ImageResponse } from 'next/server';

export const size = { width: 600, height: 300 };
export const alt = 'Dynamic Opengraph';
export const contentType = 'image/png';
export const runtime = 'edge';

export default function () {
    return new ImageResponse(
        (
            <div
                style={{
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center',
                    fontSize: 128,
                    background: 'white',
                    width: '100%',
                    height: '100%',
                }}
            >
                Dynamic OG Image Generation!
            </div>
        ),
    );
}

// src/app/metadata/dynamicOg/page.tsx
const MetadataDynamicOgPage = () => {
    return (
        <div>
            <h2>Metadata DynamicOg Page</h2>
        </div>
    );
};

export default MetadataDynamicOgPage;

 

 

 

그러면 다음과 같이 설정한 값에 맞게 metadata가 자동으로 적용되어 있는 것을 확인할 수 있습니다.

 

dynamic image generation 자동 적용

 

 

 

 

Dynamic Metadata

 

next13 이전에는 metadata의 값이 동적으로 변동되어야 할 경우 SSR 등의 렌더링 방식을 활용하여 적용해볼 수 있었습니다.

 

간단한 예시로 다음과 같은 API가 있다고 가정하고, 해당 API를 조회하여 name과 description을 metadata의 값으로 적용해 보는 코드를 구현해 볼 수 있습니다.

 

조회 API

 

// src/layouts/metadata/dynamicLayout.tsx
import Head from 'next/head';
import { ReactNode } from 'react';

interface Props {
    children: ReactNode;
    title: string;
    description: string;
}

const MetadataDynamicLayout = (props: Props) => {
    return (
        <>
            <Head>
                <title>{props.title}</title>
                <meta name="description" content={props.description} />

                <meta property="og:title" content={props.title} />
                <meta property="og:description" content={props.description} />
                <meta property="og:url" content="https://jforj.tistory.com" />
                <meta property="og:site_name" content="og site name 테스트" />
                <meta
                    property="og:image"
                    content="https://blog.kakaocdn.net/dn/kT8fV/btskKl5skJt/XcqUQY5A7DDGcqU4iIY9P0/img.png"
                />
                <meta property="og:image:width" content="800" />
                <meta property="og:image:height" content="600" />
                <meta property="og:type" content="website" />

                <meta name="twitter:card" content="summary" />
                <meta name="twitter:title" content={props.title} />
                <meta name="twitter:description" content={props.description} />
                <meta
                    name="twitter:image"
                    content="https://blog.kakaocdn.net/dn/kT8fV/btskKl5skJt/XcqUQY5A7DDGcqU4iIY9P0/img.png"
                />
                <meta name="twitter:image:width" content="800" />
                <meta name="twitter:image:height" content="600" />
            </Head>

            {props.children}
        </>
    );
};

export default MetadataDynamicLayout;

 

// src/pages/metadata/dynamicMetadata/index.tsx
import { GetServerSideProps } from 'next';
import MetadataDynamicLayout from '../../../layouts/metadata/dynamicLayout';

interface Product {
    no: number;
    name: string;
    description: string;
    price: number;
}

export const getServerSideProps: GetServerSideProps = async (context) => {
    const res = await fetch(`http://localhost:8080/getProduct?no=${context.query.no}`);
    const product: Product = await res.json();

    return {
        props: {
            product,
        },
    };
};

interface Props {
    product: Product;
}

const MetadataDynamicMetadata = (props: Props) => {
    return (
        <MetadataDynamicLayout title={props.product.name} description={props.product.description}>
            <div>
                <h2>Metadata DynamicMetadata Page</h2>
            </div>
        </MetadataDynamicLayout>
    );
};

export default MetadataDynamicMetadata;

 

// src/pages/metadata/dynamicMetadata/[no].tsx
import { GetServerSideProps } from 'next';
import MetadataDynamicLayout from '../../../layouts/metadata/dynamicLayout';

interface Product {
    no: number;
    name: string;
    description: string;
    price: number;
}

export const getServerSideProps: GetServerSideProps = async (context) => {
    const res = await fetch(`http://localhost:8080/getProduct?no=${context.query.no}`);
    const product: Product = await res.json();

    return {
        props: {
            product,
        },
    };
};

interface Props {
    product: Product;
}

const MetadataDynamicMetadataNo = (props: Props) => {
    return (
        <MetadataDynamicLayout title={props.product.name} description={props.product.description}>
            <div>
                <h2>Metadata DynamicMetadata No Page</h2>
            </div>
        </MetadataDynamicLayout>
    );
};

export default MetadataDynamicMetadataNo;

 

 

 

위와 같이 작성했을 경우 각각 다음의 경로를 통해 metadata가 동적으로 변경된 것을 확인해볼 수 있습니다.

 

  • /metadata/dynamicMetadata?no=1
  • /metadata/dynamicMetadata/1

 

 

next13 이전 metadata 동적 변경

 

 

 

 

next13 이후부터는 일반 metadata 설정이 변경된 것과 유사한 방식을 활용하여 보다 편리하게 동적으로 적용해 볼 수 있습니다.

 

API를 위와 동일하게 활용한다는 가정하에 다음과 같이 코드를 작성해 볼 수 있습니다.

 

// src/app/metadata/dynamicMetadata/layout.tsx
import { ReactNode } from 'react';

interface Props {
    children: ReactNode;
}

const MetadataDynamicMetadataLayout = (props: Props) => {
    return <div>{props.children}</div>;
};

export default MetadataDynamicMetadataLayout;


// src/app/metadata/dynamicMetadata/page.tsx
import { ResolvingMetadata } from 'next';

interface Product {
    no: number;
    name: string;
    description: string;
    price: number;
}

interface Props {
    params: { no: string };
    searchParams: { [key: string]: string | string[] | undefined };
}

// generateMetadata를 사용할 경우 metadata는 적용되지 않음
// searchParams를 활용할 경우 layout말고 page에서만 사용
export const generateMetadata = async (props: Props, parent: ResolvingMetadata) => {
    const res = await fetch(`http://localhost:8080/getProduct?no=${props.searchParams.no}`);
    const product: Product = await res.json();

    return {
        title: product.name,
        description: product.description,
        openGraph: {
            ...(await parent).openGraph,
            title: product.name,
            description: product.description,
        },
        twitter: {
            ...(await parent).twitter,
            title: product.name,
            description: product.description,
        },
    };
};

const MetadataDynamicMetadataPage = () => {
    return (
        <div>
            <h2>Metadata DynamicMetadata Page</h2>
        </div>
    );
};

export default MetadataDynamicMetadataPage;

 

// src/app/metadata/dynamicMetadata/[no]/layout.tsx
import { ResolvingMetadata } from 'next';
import { ReactNode } from 'react';

interface Product {
    no: number;
    name: string;
    description: string;
    price: number;
}

interface Props {
    params: { no: string };
    searchParams: { [key: string]: string | string[] | undefined };
    children: ReactNode;
}

// generateMetadata를 사용할 경우 metadata는 적용되지 않음
// params를 활용할 경우 layout과 page 둘다 사용 가능
export const generateMetadata = async (props: Props, parent: ResolvingMetadata) => {
    const res = await fetch(`http://localhost:8080/getProduct?no=${props.params.no}`);
    const product: Product = await res.json();

    return {
        title: product.name,
        description: product.description,
        openGraph: {
            ...(await parent).openGraph,
            title: product.name,
            description: product.description,
        },
        twitter: {
            ...(await parent).twitter,
            title: product.name,
            description: product.description,
        },
    };
};

const MetadataDynamicMetadataNoLayout = (props: Props) => {
    return <div>{props.children}</div>;
};

export default MetadataDynamicMetadataNoLayout;


// src/app/metadata/dynamicMetadata/[no]/page.tsx
const MetadataDynamicMetadataNoPage = () => {
    return (
        <div>
            <h2>Metadata DynamicMetadata No Page</h2>
        </div>
    );
};

export default MetadataDynamicMetadataNoPage;

 

next13 이후 metadata 동적 변경

 

 

 

 

 

 

 

 

이상으로 Next13의 새로운 기능 알아보기 마지막인 Metadata에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글