OpenGraphPreview(링크미리보기) 만들기 (feat. NextJS)
2023년 03월 07일
1. OpenGraphPreview(링크미리보기)는 무엇인가
OpenGraphPreview(링크미리보기)는 URL의 제목, 설명, 사진 등이 포함된 컴포넌트라고 보시면 됩니다.
평소 인터넷 서핑을 하다 보면 티스토리, 벨로그로 블로그를 포스팅하시는 분들을 보면 URL을 공유 했을 때 URL 카드가 예쁘게 만들어져서 렌더링 되어 있더라고요 저는 제가 직접 만든 블로그를 운영하다 보니 OpenGraphPreview를 직접 만들 수 밖에 없었습니다.
2. 구현
1. CORS에러 처리하기
우선 첫 번째로 URL의 og정보를 가져와야 합니다. og에는 title, image, description 3가지를 가져와서 Card를 만든 건데요. og 정보는 axios의 get을 사용하면 html 정보를 가져올 수가 있습니다.
하지만 axios.get
을 사용하게 되면 cors에러가 발생하게 됩니다. cors에러를 제거하기 위해서는 /api
폴더를 사용하면 되는데요 어떻게 처리되는 것일까요?
기본적으로 Next·JS는 /api 폴더의 API 경로에 대해 CORS(출처 간 리소스 공유)를 자동으로 처리합니다. 즉, 명시적으로 달리 구성하지 않는 한 기본적으로 다른 오리진(도메인)에서의 요청이 허용됩니다.
이는 Next. JS 서버가 클라이언트와 API 사이에서 프록시 역할을 하여 요청과 응답의 헤더를 가로채서 수정할 수 있기 때문입니다. 서버는 응답에 Access-Control-Allow-Origin: * 헤더를 응답에 추가하여 모든 오리진이 API에 액세스할 수 있도록 합니다.
import axios from "axios";
export default async (req, res) => {
try {
const url = req.query.url;
const response = await axios.get(url);
res.send(response.data);
} catch (error) {
console.log(error);
res.status(500).send("링크의 정보를 가져오는데 실패했습니다.");
}
};
/api/proxy.js 미들웨어 서버를 만들어서 response.data 리턴하겠습니다.
2. HTML을 얻고 OG정보를 뽑기
이제 a 태그들을 모아 순회를 돌면서 해당 elements의 href 값을 받아오겠습니다.
const $linkList = document.querySelectorAll("#link");
$linkList.forEach(async (el, index) => {
const urlPath = el.href;
const response = await axios.get(
`/api/proxy?url=${encodeURIComponent(urlPath)}`
);
const html = response.data;
...
저의 경우에는 id 값이 link인 값을을 링크 미리보기 카드로 만들 예정입니다. encodeURIComponent
는 무엇일까요?
encodeURIComponent는 특정 문자를 해당 퍼센트로 인코딩된 값으로 대체하여 문자열을 인코딩하는 데 사용되는 JavaScript 함수입니다. 이렇게 인코딩된 문자열은 모든 특수 문자가 올바르게 인코딩되고 URL 구조를 방해하거나 오류를 일으키지 않도록 보장하므로 URL에 안전하게 포함될 수 있습니다.
이렇게 뽑아온 HTML 파일을 파싱해야 합니다. 파싱을 더 쉽게 해주는 패키지를 하나 소개하겠습니다.
https://www.npmjs.com/package/cheerio
Cheerio
는 빠르고 가볍고 유연한 라이브러리로, Node.js 환경에서 HTML 및 XML 문서를 파싱하고 조작할 수 있습니다. 문서에서 요소를 선택하고 조작하기 위한 jQuery와 유사한 구문을 제공하여 HTML 및 XML 페이지에서 정보를 추출하고 문서에서 다양한 변환 및 조작을 쉽게 수행할 수 있습니다.
저는 title, image, description 3가지를 뽑아오겠습니다.
import cheerio from "cheerio";
const $ = cheerio.load(html);
const ogTitle =
$('meta[property="og:title"]').attr("content") || $("title").text();
const ogDescription = $('meta[property="og:description"]').attr("content");
const ogImage = $('meta[property="og:image"]').attr("content");
이런식으로 특정 meta 태그를 가져올수도 있고 title태그의 text값도 가져올수가 있습니다.
3. Element를 변경하기
저는 OpenGraphPreview
컴포넌트를 생성하고 문자열로 렌더링할수있게 변형한 다음에 el.outerHTML
값에 렌더링된 문자열을 넣어서 OpenGraphPreview 렌더링 할 예정입니다.
import OpenGraphPreview from "../UIElements/OpenGraphPreview";
const OpenGraphPreview = ({ urlPath, ogTitle, ogDescription, ogImage }) => {
return (
<a href={urlPath}>
<title>{ogTitle}</title>
<description>{ogDescription}</ escription>
<img src={ogImage} />
</a>
)
}
const $linkList = document.querySelectorAll("#link");
$linkList.forEach(async (el, index) => {
...
...
const aHtml = ReactDOMServer.renderToString(
<OpenGraphPreview
urlPath={urlPath}
ogTitle={ogTitle}
ogDescription={ogDescription}
ogImage={ogImage}
/>
);
el.outerHTML = aHtml
})
대략적인 형태는 이렇습니다. OpenGraphPreview 컴포넌트를 생성하고 해당 컴포넌트를 ReactDOMServer.renderToString
를 사용해서 문자열로 렌더링할 수 있게 변형 후 el를 변경합니다.
완성된 OpenGraphPreview
3. 여담
이번에 OpenGraphPreview를 구현하면서 처음에는 금방 할 수 있을 거라고 생각했었습니다. 하지만 예상치 못한 난관이 많더군요. 첫 번째는 예상치 못한 cors에러가 있고 두 번쨰는 next/image의 Image 컴포넌트였습니다. 보통 next에서 img태그대신 Image를 사용하는데 저도 그렇게 적용해 보려고 했습니다만 Invalid 에러가 나타나더라고요. 그래서 next-config.js에 image를 와일드카드 형식으로 어떤 url이든지 Image로 렌더링하려고 했습니다. 하지만 되지 않더라고요. 그래서 저는 그냥 img 태그를 사용했습니다. 그 밖에도 og태그가 없을때는 title정보만 가지고 렌더링한다던지 여러가지 생각할만한 부분이 많아서 꽤 어려운 도전이였습니다. 대부분 blog article에 a태그를 OpenGraphPreview로 변경했지만 아직 덜된 부분이 있을수 있으니 발견하시면 알려주세요 ^.^
4. 예상치 못한 트러블 슈팅
1. 에러의 원인
이 포스터를 작성한 그다음 날 블로그 글을 둘러 보던 중 뒤로가기를 클릭하면 에러가 발생한다는 사실을 알게 되었습니다. 에러의 내용은 NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
처음에는 제가 el을 변경하여서 이전 페이지 DOM을 복구하려고 할 때 에러가 발생한다고 생각했습니다. 하지만 어떨떄는 에러가 발생하고 어떨 때는 제대로 작동이 되었습니다. 계속 시도해본 결과 알아낸 사실은 블로그의 목차를 클릭하였을 때 해당 목차로 이동하는데 URL 뒤에 해당 목차의 ID 값이 붙은 후 뒤로가기가 수행되면 에러가 발생한다는 사실이었습니다.
해당 문제를 해결하는 방법은 총 3가지
-
a 태그 대신 Link 컴포넌트를 사용하는 방법
-
a 태그를 div 태그로 감싸는 방법
-
a 태그 event를 중지하고 scrollIntoView를 사용하는 방법
2. 문제 해결
3가지의 방법이 존재하지만 제 블로그들을 작성할 때 mdx에서 작성하기 때문에 next의 Link 태그를 사용할 수 없었습니다. 그리고 div 태그를 감싸는 방법 대신 3번쨰 방법을 사용하겠습니다.
3번째 방법 step by step
-
목차 역할을 하는 a 태그에 click event를 붙여줍니다.
-
해당 event 발생 시 event.preventDefault를 수행해 event를 중지합니다.
-
a 태그의 href 값을 찾은 다음 해당 값의 id를 가진 목차로 이동합니다.
문제를 해결하는 함수 구현 👇
const handlerChapterScroll = () => {
// a 태그의 href 값 앞에 #이 붙은 태그를 가져옵니다.
const links = document.querySelectorAll('a[href^="#"]');
links.forEach((link) => {
link.addEventListener("click", function (event) {
event.preventDefault();
// #을 제거한 href를 가져옵니다.
const linkId = link.getAttribute("href")?.slice(1);
if (linkId) {
// id값과 일치하는 목차를 가져옵니다.
const target = document.getElementById(linkId);
if (target) {
// 해당 목차로 scroll 됩니다.
target.scrollIntoView({
behavior: "smooth",
});
}
}
});
});
};