๐ ํ์ฌ ํ๋ก์ ํธ ํ๊ฒฝ
ํ์ฌ OG ํ๊ทธ๋ฅผ ์ ์ฉํ ํ๋ก์ ํธ๋ Preact๋ฅผ ์ฌ์ฉํ ์ ํ์ ์ธ **CSR ๊ธฐ๋ฐ์ SPA(Single Page Application)**์ด๋ค. ์๋ฒ๋ API์ ์ ์ ํ์ผ ์๋น๋ง ๋ด๋นํ๊ณ , ์ค์ UI ๋ ๋๋ง์ ๋ธ๋ผ์ฐ์ ์์ JavaScript๋ก ์ฒ๋ฆฌํ๋ค.
๐กOG ํ๊ทธ๋?
OG(Open Graph) ํ๊ทธ๋ ์น ํ์ด์ง๋ฅผ ์์ ๋ฏธ๋์ด(์: ์นด์นด์คํก, ํ์ด์ค๋ถ, ํธ์ํฐ ๋ฑ)์์ ๊ณต์ ํ ๋ ๋งํฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์ ์ ๋ชฉ, ์ค๋ช , ์ด๋ฏธ์ง๋ฑ์ ์ ์ดํ๋ ๋ฉํํ๊ทธ์ด๋ค.
OG ํ๊ทธ๊ฐ ์ ์ฉ๋๋ ํ๋ฆ
- ์ฌ์ฉ์๊ฐ ์น ๋งํฌ๋ฅผ ๊ณต์
- ํ๋ซํผ(์นด์นด์ค, ํ๋ถ ๋ฑ)์ด ํด๋น URL์ ์์ฒญ
- ์๋ฒ๋ HTML ๋ฌธ์ ์๋ต
- <head>์ OG ํ๊ทธ๋ฅผ ํ์ฑ
- ๊ทธ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋งํฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์์ฑ
๐ ํด๋ผ์ด์ธํธ์์ ๋์ ์ผ๋ก ์ฝ์ ํ OG๋ ๋ฌด์๋จ
→ ์ด์ ๋ ํฌ๋กค๋ฌ๋ JS ์คํ์ ํ์ง ์๊ธฐ ๋๋ฌธ
โ๏ธ CSR์์ ๋์ OGํ๊ทธ ์ด์ฉํ๊ธฐ
CSR ๋ฐฉ์(Preact, React, Vue ๋ฑ)์์๋ ํด๋ผ์ด์ธํธ๊ฐ ๋ง์ดํธ๋๊ณ ๋์์ผ JavaScript์ ์ํด DOM์ด ๋ณ๊ฒฝ๋๋ฏ๋ก OG ํ๊ทธ๊ฐ ๋ฆ๊ฒ ์ฝ์ ๋๋ค.
๐ ์นด์นด์ค/ํ๋ถ/ํธ์ํฐ ๊ฐ์ ํฌ๋กค๋ฌ(๋ด)๋ OG ํ๊ทธ๋ฅผ ์ธ์ํ์ง ๋ชปํจ.
ํด๊ฒฐ๋ฐฉ๋ฒ
- SSR๋ก OG ํ๊ทธ๊น์ง ํฌํจ๋ HTML์ ๋ฐ๋ก ๋ ๋๋งํ๊ธฐ
- ์๋ฒ์์ <head>๋ฅผ ์ง์ ์กฐ์ํด OG ํ๊ทธ๋ฅผ ์ฝ์ ํด์ ๋ฐํ
ํ๋ก์ ํธ๋ฅผ CSR์์ SSR๋ก ๋ณ๊ฒฝํ๊ธฐ์๋ ๊ณต์๊ฐ ํฌ๊ธฐ ๋๋ฌธ์ 2๋ฒ ๋ฐฉ๋ฒ์ ์ด์ฉํด ํด๊ฒฐ์ ์๋ํด๋ณด์๋ค.
๐ ๏ธ์๋ฒ์์ <head>๋ฅผ ์ง์ ์กฐ์ํด ๋์ OG ํ๊ทธ ์์ฑํ๊ธฐ
ํด๊ฒฐ ๋ฐฉ๋ฒ ์์ฝ
- OG ํ๊ทธ์ ๋ค์ด๊ฐ ์ ๋ณด๋ฅผ API(๋น๋๊ธฐ)๋ก ์กฐํ
- ์ ๋ฌ ๋ฐ์ ๋ฐ์ดํฐ๋ก OG ํ๊ทธ ๋ฌธ์์ด์ ์์ฑ
- ์ ์ ์ธ HTML ํ ํ๋ฆฟ ํ์ผ(index.html)์ ์ฝ์
- <head> ํ๊ทธ ์์ ๋์ ์ผ๋ก ์์ฑํ OG ํ๊ทธ ๋ฌธ์์ด์ ์ฝ์
- ์ฝ์ ๋ HTML์ ctx.body๋ก ์๋ตํ์ฌ ์๋ฒ์์ OG ๋ฉํ ์ ๋ณด๋ฅผ ํฌํจํ HTML์ ์ ๊ณต
์ฝ๋๋ก ์ค๋ช
1. OG ์ ๋ณด ๋ก๋ฉ
- ์ธ๋ถ API ๋๋ ๋ด๋ถ DB์์ OG ์ ๋ณด๋ฅผ ๋น๋๊ธฐ๋ก ๋ถ๋ฌ์ด
- ์คํจ ์ ๊ธฐ๋ณธ OG ์ ๋ณด(defaultTitle, defaultImage) ๋ฐํ
loadOgInformation: async code => {
const {title, imgUrl, description} = await getOgTagTittleAndImage(code);
return {title, imgUrl, description};
}
2. OG ํ๊ทธ ๋ฌธ์์ด ์์ฑ
- ์ ๋ฌ๋ฐ์ ๋ฐ์ดํฐ๋ก OG ํ๊ทธ ๋ฌธ์์ด์ ์์ฑ
createOgMetaTags: ({title, imgUrl, description}) => `
<meta property="og:title" content="${title}" />
<meta property="og:image" content="${imgUrl}" />
<meta property="og:description" content="${description}" />
`,
3. HTML์ OG ํ๊ทธ ์ฝ์
- <head> ํ๊ทธ๋ฅผ ์ฐพ์ OG ํ๊ทธ๋ฅผ ์ฝ์
injectOgTags: (html, ogTags) => html.replace(/<head[^>]*>/i, match => `${match}\\n${ogTags.trim()}`)
์ ์ฒด์ฝ๋
- index.html์ ๋ฏธ๋ฆฌ ๋ ๋๋ ํ ํ๋ฆฟ HTML ํ์ผ๋ก, OG ํ๊ทธ๊ฐ ์ฝ์ ๋ ๋ฒ ์ด์ค ํ์ผ
- ํ์ํ OG ์ ๋ณด(title, image)๋ฅผ ๋ถ๋ฌ์ <head>์ ์ฝ์ ํ ํด๋ผ์ด์ธํธ์ ์ ๋ฌ
// OG Tag Util
const ogTagUtils = {
loadOgInformation: async code => {
try {
const {title, imgUrl, description} = await getOgTagTittleAndImage(code);
return {title, imgUrl, description};
} catch (error) {
console.error('[ERROR]', error.message);
return {
title: config.og.defaultTitle,
imgUrl: config.og.defaultImage,
description: config.og.defaultDescription
};
}
},
createOgMetaTags: ({title, imgUrl, description}) => `
<meta property="og:title" content="${title}" />
<meta property="og:image" content="${imgUrl}" />
<meta property="og:description" content="${description}" />
`,
injectOgTags: (html, ogTags) => html.replace(/<head[^>]*>/i, match => `${match}\\n${ogTags.trim()}`),
};
// Server Code
try {
const { title: ogTitle, imgUrl: ogImageUrl, description: ogDescription } = await ogTagUtils.loadOgInformation(code);
const ogMetaTags = ogTagUtils.createOgMetaTags({
title: ogTitle,
imgUrl: ogImageUrl,
description: ogDescription
});
const bridgeHtmlPath = `${config.static.distDir}/bridge.html`;
const bridgeHtml = await readFilePromise(bridgeHtmlPath, 'utf8');
const finalHtml = ogTagUtils.injectOgTags(bridgeHtml, ogMetaTags);
ctx.body = finalHtml;
} catch (error) {
console.error('[ERROR] OG ์ ๋ณด ์กฐํ ์คํจ:', error);
ctx.status = 500;
}
์ ์ํ ์
- OG ํ๊ทธ๋ ๋ฐ๋์ SSR ํน์ HTML ์กฐ์์ผ๋ก ์ ๊ณต๋์ด์ผ ํฌ๋กค๋ฌ๊ฐ ์ ์์ ์ผ๋ก ์ธ์ ๊ฐ๋ฅ
- index.html์ ์ต์ํ์ HTML ๊ตฌ์กฐ (<!DOCTYPE html>, <html>, <head>, <body>)๋ฅผ ํฌํจํด์ผ ํจ
- <head> ํ๊ทธ๋ ๋ฐ๋์ ์กด์ฌํด์ผ ํ๋ฉฐ, ์ ๊ทํํ์์ผ๋ก ์นํ๋๋ฏ๋ก ์๋์น ์์ HTML ๊ตฌ์กฐ์ ์ฃผ์
- OG ์ ๋ณด ๋ก๋ฉ ์ ์บ์ฑ ์ ๋ต ๊ณ ๋ ค ํ์ (์: ๋์ผ ์ฝ๋ ์์ฒญ์ ๋ํด ๋ฐ๋ณต ์กฐํ ๋ฐฉ์ง)
ํ์ฅ ์์ด๋์ด
- url, type ๋ฑ์ ํ๊ทธ๋ ํจ๊ป ๋์ ์ผ๋ก ์ถ๊ฐ
- user-agent ๊ฒ์ฌ๋ก ๋ด ์์ฒญ ์์๋ง OG ์ฝ์ ์ฒ๋ฆฌ
- Redis ๋ฑ์ผ๋ก OG ๋ฐ์ดํฐ ์บ์ฑ
๋๊ธ