SEO(3) - 프로젝트에 오픈그래프 적용하기
📌시작하며
이전 포스팅에서 오픈그래프 만드는 법에 대해 쭉 살펴보고, 프로젝트에 적용했는데 역시나😅 예상치 못한 오류를 여럿 마주했다. 다행히 그 오류들을 해결하고 오픈그래프 적용을 완료해서, 관련 내용을 잊지않게 정리하고자 한다.
✅프로젝트 상황
기본적으로 모든 페이지에 적절한 오픈그래프가 적용될 수 있도록 figma를 이용해 이미지를 만들어 두었고, 각 페이지 폴더에 opengraph-image.jpg
를 추가해 적절히 적용될 수 있도록 했다.
여기까지는 수월하게 적용이 가능했으나, 한가지 더 시도해보고 싶은게 있었는데, 음악 상세페이지에는 각 앨범의 썸네일이 있었기 때문에 이 앨범 썸네일을 활용해 오픈그래프 이미지로 사용하고 싶었다. 이미 dynamic opengraph 만들기는 예제를 통해 한 번 해봤기 때문에 어렵지 않을것이라 예상했지만…😅 (그러나 오류는 늘 발생하는 법이다.)
어쨌든! 적용 완료 된 폴더 구조는 다음과 같다.
1
2
3
4
5
app
└── musicpt
├── opengraph-image.jpg
└── [id]
└── opengraph-image.tsx
✅opengraph-image.jpg
먼저 musicpt 바로 아래에 있는 opengraph-image.jpg는 미리 만들어 둔 기본 오픈그래프이미지다. 해당 이미지는 썸네일이 없는 경우 적용될 수 있도록 넣어두었다.
✅opengraph-image.tsx
[id] 아래에 있는 opengraph-image.tsx는 동적으로 오픈그래프 이미지를 만들기 위해 사용했다. 이미지는 ImageResponse를 이용해 제작했다.
➡️오픈그래프 제작을 위한 기본 세팅
먼저 ImageResponse를 불러오고, edge 환경에서 실행될 수 있도록했다. size는 미리 만들어 둔 오픈그래프 사이즈와 동일하게 1200*1200의 정사각형 사이즈를 선택했고, jpg 이미지로 제작될 수 있도록 했다.
1
2
3
4
5
6
7
8
9
10
import { ImageResponse } from "next/og"
export const runtime = "edge"
// Image metadata
export const size = {
width: 1200,
height: 1200,
}
export const contentType = "image/jpg"
➡️필요한 썸네일 이미지 불러오기
getMusicData라는 함수를 이용해서 썸네일 이미지를 불러온다. kothumbnail과 jpthumbnail을 불러와, thumbnail이라는 변수에 할당한다. 둘 다 없는 경우 null값을 할당해주었다.
참고로, 다른 파일에서 전부 axios를 이용해 통신을 했기 때문에 이번에도 axios를 사용하려고 했으나, edge 환경에서 사용이 불가능하다는 에러가 발생했다.
따라서 fetch를 사용해주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
const getMusicData = async (id: number) => {
try {
const res = await fetch(`https://test.com/api/music/${id}`)
const data = await res.json()
const { kothumbnail, jpthumbnail } = data.post
const thumbnail = kothumbnail
? kothumbnail
: jpthumbnail
? jpthumbnail
: null
return { thumbnail }
} catch (err) {
console.log(err)
return { thumbnail: null }
}
}
➡️이미지 제작하기
이제 ImageResponse를 사용해 오픈그래프 이미지를 제작해준다. 위에서 만들어 둔 getMusicData함수를 사용해 thumbnail을 꺼내준다.
thumbanil값이 없는 경우 null값으로 비워주었기 때문에, thumbnail이 있는 경우에만 이미지를 제작할 수 있도록 조건문을 걸어주었다.
이미지는 style과 tailwind를 적절히 사용해서 기존에 만든 오픈그래프와 톤앤매너를 유지할 수 있도록 조절해주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Image generation
export default async function Image({ params, searchParams }: Props) {
const id = Number(params.id)
const data = await getMusicData(id)
const { thumbnail } = data
const font = await fetch(
new URL("../../../assets/fonts/NotoSans-OG-Black.woff", import.meta.url)
).then((res) => res.arrayBuffer())
if (thumbnail) {
return new ImageResponse(
(
<div tw="h-full w-full flex flex-col">
<img
style=
tw="absolute inset-0 w-full h-full"
src={thumbnail}
alt="썸네일"
/>
<div tw="bg-black absolute inset-0 bg-opacity-60"></div>
<div tw="flex w-full justify-center mt-[464px]">
<svg width="72" height="56" viewBox="0 0 72 56" fill="none">
// 생략
</svg>
</div>
<p
tw="text-center w-full flex justify-center"
style=
>
LYRICS
</p>
</div>
),
// ImageResponse options
{
...size,
fonts: [
{
name: "NotoSans-OG-Black",
data: font,
weight: 900,
},
],
}
)
}
}
➡️폰트 사용하기
해당 이미지에는 내가 다른 오픈그래프를 만들 때 사용하던 폰트를 사용해야 했다. LYRICS
라는 이 단어만 폰트가 필요하므로 LYRICS만을 담은 서브셋을 제작해 적용해주었다.
파일 구조는 다음과 같다.
1
2
3
4
5
src
├── app
└── assets
└── fonts
└── NotoSans-OG-Black.woff
이때 woff2 파일은 사용이 불가하므로, woff 폰트를 사용해주었다.
사용할 때는 위에서 살펴본 것과 마찬가지로 아래와 같이 폰트를 불러와준다음,
1
2
3
const font = await fetch(
new URL("../../../assets/fonts/NotoSans-OG-Black.woff", import.meta.url)
).then((res) => res.arrayBuffer())
ImageResponse의 옵션에 아래와 같이 작성해주면 된다.
1
2
3
4
5
6
7
8
9
10
{
...size,
fonts: [
{
name: "NotoSans-OG-Black",
data: font,
weight: 900,
},
],
}
✅page.tsx에서 적용하기
이제 이렇게 만든 오픈그래프를 적용해준다. 참고로 generateMetadata
는 client component에서는 사용이 불가능 하므로, 만약 client component인 경우 컴포넌트를 분리해주자!
먼저 opengraph-image.tsx에서 작성한 것과 비슷하게 getMusicData함수를 정의해 주었다.
➡️오픈그래프에 필요한 데이터 불러오기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { Metadata, ResolvingMetadata } from "next"
import MusicPage from "@/components/MusicPage"
import { ParamsProps } from "@/types/form"
import axios from "axios"
import ogImage from "../opengraph-image.jpg"
type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
const getMusicData = async (id: number) => {
try {
const { data }: any = await axios.get(`https://test.com/api/music/${id}`)
const { kothumbnail, jpthumbnail, kotitle, jptitle, kosinger, jpsinger } =
data.post
let thumbnail = kothumbnail ? kothumbnail : jpthumbnail ? jpthumbnail : ""
//이미지가 허용되는 형식인지 확인한다.
//만약 허용되지 않는 형식인 경우 thumbnail값을 공백으로 두어, 기본 og를 사용하도록 한다.
if (thumbnail) {
try {
const response = await axios.get(thumbnail, { responseType: "blob" })
const contentType = response.headers["content-type"]
// 형식 체크
if (
!(
contentType === "image/png" ||
contentType === "image/jpeg" ||
contentType === "image/jpg"
)
) {
thumbnail = ""
}
} catch (err) {
console.log(err)
thumbnail = ""
}
}
const title = kotitle ? kotitle : jptitle ? jptitle : ""
const singer = kosinger ? kosinger : jpsinger ? jpsinger : ""
return { thumbnail, title, singer }
} catch (err) {
console.log(err)
return { thumbnail: "", title: "", singer: "" }
}
}
➡️이미지 형식 확인하기
여기서 핵심은 이미지가 허용되는 타입인지를 확인하는 부분이다! thumbnail의 경우 사용자가 업로드하는 이미지 파일로, webp 파일이 사용되기도 했는데, ImageResponse에서는 webp를 인식하지 못해 오류가 발생했다. 따라서, 먼저 해당 주소의 이미지 포맷을 확인하는 작업이 필요했다.
1
2
const response = await axios.get(thumbnail, { responseType: "blob" })
const contentType = response.headers["content-type"]
이와 같이 contentType을 통해 이미지 포맷을 확인하고, png/jpg/jpeg가 아닌 경우에는 thumbnail값을 비워두어, 아래에서 삼항연산자를 통해 미리 만들어 둔 기본 opengraph를 사용할 수 있도록 했다.
➡️generateMetadata를 활용해 오픈그래프 생성하기
마지막으로 이렇게 만든 오픈그래프를 적용해주었다. 핵심은 thumbnail값이 falsy한지 확인하여 thumbnail이 존재하고, 허용되는 파일 포맷일 경우에만 동적 오픈그래프 (ImageResponse를 사용한 오픈그래프)가 적용될 수 있도록 해주고 그 외에는 기본 이미지를 사용할 수 있도록 하는 것이다.
이때 tsx파일로 만든 오픈그래프의 주소는 /musicpt/${id}/opengraph-image
이며, jpg 파일의 경우 import로 불러와서 ogImage.src
로 할당해주면 된다.
1
2
3
4
5
6
7
images: [
{
url: thumbnail ? `/musicpt/${id}/opengraph-image` : ogImage.src,
width: 1200,
height: 630,
},
],
generateMetadata의 전체 내용은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const id = Number(params.id)
const data = await getMusicData(id)
const { thumbnail, title, singer } = data
return {
title: title ? "Music PT_" + title : "Music PT",
description: "Music PT에서 좋아하는 음악의 언어별 가사를 비교해보세요!",
metadataBase: new URL("https://test.com"),
alternates: {
canonical: `/musicpt/${id}`,
},
openGraph: {
title: title + " 가사 탐구" || "Music PT 가사 탐구",
description:
title && singer
? `[${singer}] ` + title + "의 언어별 가사를 비교해보세요!"
: "Music PT에서 언어별 가사를 비교해보세요!",
images: [
{
url: thumbnail ? `/musicpt/${id}/opengraph-image` : ogImage.src,
width: 1200,
height: 630,
},
],
},
twitter: {
title: title + " 가사 탐구" || "Music PT 가사 탐구",
description:
title && singer
? `[${singer}] ` + title + "의 언어별 가사를 비교해보세요!"
: "Music PT에서 언어별 가사를 비교해보세요!",
images: [
{
url: thumbnail ? `/musicpt/${id}/opengraph-image` : ogImage.src,
width: 800,
height: 600,
alt: title ? title + " 앨범 썸네일" : "앨범 기본 이미지",
},
],
},
}
}
이를 통해 각 페이지당 적절한 오픈그래프를 적용하고, 상세페이지의 경우 해당 음악 앨범 이미지를 이용한 오픈그래프를 제작할 수 있었다. 😎
시간은 다소 소요되었지만, 카카오톡이나 노션에 내가 만든 프로젝트 주소를 붙여넣었을 때 미리 보기 이미지가 훨씬 보기 좋고, 어떤 내용인지 바로 짐작할 수 있어서 사용성이나 SEO측면에서 적절한 방식이었다고 생각한다!