
rehypeRewrite 플러그인 함수 개발
블로그 글을 쓰다보면 외부 링크를 작성하는 일이 있다. 예를 들어 프리뷰 네트워크 기반 동적 품질 조절 기능 관련 글에는 시연 영상이 링크되어있다.
티스토리 같은 블로그에서는 유튜브 링크를 걸 경우에 유튜브 영상이 embed 되는 것을 볼 수 있다. 하지만 내 블로그에서는 지원하지 않았기 때문에 유튜브 영상이 링크 되어있는 줄도 모르고 지나칠 가능성이 매우 높다고 생각했다. 그래서 유튜브 임베드 기능을 만들어야겠고 생각했다.
썸네일
유튜브 임베드 도입하기
지금은 유튜브 링크를 걸어도 아래와 같이 링크임을 알 수 있도록 파란색 글씨인 것을 제외하고는 유튜브라는 것을 알 수 없는 상황이다.
임베딩 구현 전
일단 내 블로그는 마크다운 양식으로 어드민 페이지에서 작성해야한다. 그리고 작성된 글은 외부 라이브러리인 UIW 마크다운 라이브러리(@uiw/react-md-editor)를 통해 렌더링된다.
내가 직접 구현한 것이 아닌 외부 라이브러리를 사용해서 글을 렌더링하는 것이었기 때문에 내가 원하는 방식으로 렌더링하기 쉽지 않다. 그래서 어떻게 해야할까 고민을 시작했다. 유튜브 링크가 있으면 → 그 다음 줄에 embed 동작을 하는 것에 대해 좀 생각하는 과정을 거쳤어야 했다.
유튜브 링크가 있는지 확인한다 → 동영상 Id를 가져온다. → 유튜브 embed iframe을 생성한다. 과정을 거쳐야 한다.
다행히 UIW 라이브러리에는 rehypeRewrite 플러그인을 지원한다. 이 플러그인을 통해 마크다운에서 HTML로 변경하는 과정에서 사용자가 원하는 방식으로 렌더링 덮어씌우기가 가능하다.
<MDEditor.Markdown
style={{
backgroundColor: 'var(--background)',
color: 'var(--text-primary)',
}}
className={''}
source={content}
wrapperElement={{
'data-color-mode': theme,
}}
rehypeRewrite={(node, index?, parent?) => {
asideStyleRewrite(node);
renderOpenGraph(node, index || 0, parent as Element | undefined);
renderYoutubeEmbed(
node,
index || 0,
parent as Element | undefined
);
}}
/>
이전에도 Notion의 콜아웃 스타일을 내 블로그에도 적용하기 위해 사용했었기 때문에 이 방법을 사용하기로 했다. 위 컴포넌트는 MDEditor의 마크다운을 HTML 형식으로 렌더링해주는 컴포넌트이다. rehypeRewrite prop에 각각의 덮어씌우기 플러그인들을 함수 형태로 분리해서 가독성을 높였다.
왓
Youtube 링크 찾기
rewrite를 하기 위해서는 youtube 링크가 포함된 a 태그를 발견해야한다.
Rewrite 함수의 시그니쳐
type RehypeRewriteFunction = (node: Element, index: number, parent: Element) => void;
위는 RehypeRewrite 플러그인에 들어가는 함수의 타입 시그니쳐이다. 마크다운에서 HTML의 DOM 트리로 변환하는 과정에서 node를 하나씩 거쳐가며 rehypeRewrite를 적용하는 방식이다.
아래 uiw의 공식 문서에 포함된 예제를 살펴보자.
<MarkdownPreview
source={source}
style={{ padding: 16 }}
rehypeRewrite={(node, index, parent) => {
if (node.tagName === "a" && parent && /^h(1|2|3|4|5|6)/.test(parent.tagName)) {
parent.children = parent.children.slice(1)
}
}}
/>
node의 tagName을 통해 필터링을 하고, 부모 요소의 태그가 헤딩 태그일 경우 부모 요소의 첫번째 자식 요소만 남겨두고 제거하는 코드이다.
a 태그를 찾기 위한 플러그인 함수를 만들어보자. 위 예제처럼 하면 링크 발견은 쉽게 할 수 있다. if (node.tagName === "a") 를 적용하면 되기 때문이다. 하지만..
링크 발견까지는 했는데 어떻게 다음 줄에 embed하지?
링크를 발견할 경우 해당 a 태그의 parent는 p태그가 된다. 그렇다면 p 태그를 중심으로 요소를 추가해야 하는데 이 경우 다음 줄에 embed하기 어렵다고 판단했다.
그래서 a 태그를 갖는 p 태그를 찾기로 결정했다.
const renderYoutubeEmbed = (node: any, index?: number, parent?: Element) => {
if (node.type === 'element' && node.tagName === 'p' && node.children) {
const aTag = node.children.find(
(node: any) => node.type === 'element' && node.tagName === 'a'
);
}
}
이렇게 하면 parent는 문서 전체가 되고 요소의 index를 활용하여 a 태그 다음 줄에 iframe을 삽입할 수 있을 것이다.
Youtube iframe 생성하기
구현한 플러그인의 전체 코드는 아래와 같다. youtube 링크인지 확인하고, createYoutubeIframe 함수를 통해 iframe 요소를 생성한다. 객체를 만드는 함수를 만들어서 분리하여 가독성을 높였다.
유튜브의 링크는 youtu.be 형식과 youtube.com/watch 형식 두 가지가 있어서 두 가지 모두에 대응할 수 있도록 구현했다.
renderYoutubeEmbed 함수의 전체
const renderYoutubeEmbed = (node: any, index?: number, parent?: Element) => {
if (node.type === 'element' && node.tagName === 'p' && node.children) {
const aTag = node.children.find(
(node: any) => node.type === 'element' && node.tagName === 'a'
);
if (!aTag) return;
const href = aTag.properties?.href;
const isYoutubeLink =
href &&
(href.startsWith('https://www.youtube.com/watch') ||
href.startsWith('https://youtu.be/'));
if (isYoutubeLink) {
const urlType = href.startsWith('https://www.youtube.com/watch')
? 'watch'
: 'be';
const videoId =
urlType === 'watch'
? new URL(href).searchParams.get('v')
: href.split('/').pop();
if (videoId) {
const youtubeEmbed = createYoutubeIframe(videoId, 736, 414);
// 부모가 존재하고 children 배열이 있는 경우
if (index && parent && parent.children) {
// 현재 a 태그 다음 위치에 embed 삽입
parent.children.splice(index + 1, 0, youtubeEmbed);
} else return;
}
}
}
};
createYoutubeIframe 함수 코드
const createYoutubeIframe = (
videoId: string,
width: number,
height: number
) => {
return {
tagName: 'iframe',
properties: {
src: `https://www.youtube.com/embed/${videoId}`,
width: width,
height: height,
frameBorder: '0',
allowFullScreen: true,
className: 'youtube-embed',
},
children: [],
};
};
위 코드를 통해서 유튜브 임베드를 렌더링하게 된다. 아래 부분 코드를 통해 부모 요소의 index 위치 다음에 유튜브 임베드 iframe을 삽입하는 방식이다.
if (videoId) {
const youtubeEmbed = createYoutubeIframe(videoId, 736, 414);
// 부모가 존재하고 children 배열이 있는 경우
if (
index &&
parent &&
parent.children &&
Array.isArray(parent.children)
) {
parent.children.splice(index + 1, 0, youtubeEmbed);
} else return;
}
이제 유튜브 링크가 존재하는 블럭 다음 줄에 이렇게 Youtube Embed Iframe이 생성되는 것을 볼 수 있다.
유튜브 링크를 걸면 아래에 자동으로 embed되는 모습이다. 요즘 집중할 때 틀어놓는 앰비언스를 링크해봤다. ㅎㅎ
유튜브에는 시연 영상 위주로 올릴 것 같다.
끝
