
블로그 글을 쓰다보면 외부 링크를 작성하는 일이 있다. 예를 들어 프리뷰 네트워크 기반 동적 품질 조절 기능 관련 글에는 시연 영상이 링크되어있다.
티스토리 같은 블로그에서는 유튜브 링크를 걸 경우에 유튜브 영상이 embed 되는 것을 볼 수 있다. 하지만 내 블로그에서는 지원하지 않았기 때문에 유튜브 영상이 링크 되어있는 줄도 모르고 지나칠 가능성이 매우 높다고 생각했다. 그래서 유튜브 임베드 기능을 만들어야겠고 생각했다.
Youtube
지금은 유튜브 링크를 걸어도 아래와 같이 링크임을 알 수 있도록 파란색 글씨인 것을 제외하고는 유튜브라는 것을 알 수 없는 상황이다.
임베딩 구현 전
일단 내 블로그는 마크다운 양식으로 어드민 페이지에서 작성해야한다. 그리고 작성된 글은 외부 라이브러리인 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에 각각의 덮어씌우기 플러그인들을 함수 형태로 분리해서 가독성을 높였다.
왓
rewrite를 하기 위해서는 youtube 링크가 포함된 a 태그를 발견해야한다.
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") 를 적용하면 되기 때문이다. 하지만..
링크를 발견할 경우 해당 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 링크인지 확인하고, 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되는 모습이다. 요즘 집중할 때 틀어놓는 앰비언스를 링크해봤다. ㅎㅎ
유튜브에는 시연 영상 위주로 올릴 것 같다.
끝