바쁜 세상, 바로 본론으로 들어갑시다.
잘 되는지 확인부터
Shorts는 블로그 가로폭이 클 경우를 대비해서 안전하게 기본값 75%가 적용된 상태이고, 기본값과 옵션값 수정으로 크기를 변경할 수 있습니다.
제가 원래 유튜브 링크한 방식은 아래와 같습니다.
아직까지 많이 쓰이는 직접 iframe을 사용한 방식
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
<iframe
src="https://www.youtube.com/embed/21mqS5O1kWs"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
allowfullscreen
title="District 9 4K HDR | Ending Scene">
</iframe>
</div>데스크톱에서는 웬만하면 잘 보이는데, 모바일에서 어떤 영상들은 정상적으로 표시되지 않습니다.
제 모바일 기기에서 가운데 영상이 ‘동영상을 재생할 수 없음’ 으로 표시되면서 미리보기가 안됩니다.
정확한 이유는 모르고 유튜브 링크할 일이 얼마나 되겠냐만, 아쉽습니다.
그리고 쇼츠의 비율이 아쉽습니다.
lite-youtube-embed
그래서 찾다보니 lite-youtube-embed를 발견하게 되고, 바로 적용을 해보았습니다.
Astro 사용자는 다음 내용을 건너뛰고 제일 하단의 Astro에서 페이지 최적화 부분으로 바로 이동하세요.
설치 방법은 2가지인데, npm을 사용하거나 직접 .js, .css 파일 2개 다운 받아서 사용하는 방법이 있습니다.
직접 다운 받아서 사용해보겠습니다.
- lite-yt-embed.css, lite-yt-embed.js 받으러 가기
- 전체 페이지 혹은 개별페이지 html의 head 부분에 2가지 링크를 넣어 줍니다.
본인에게 맞게 파일의 위치를 수정하세요.<head> ... <link rel="stylesheet" href="/src/styles/lite-yt-embed.css" /> <script src="/src/utils/lite-yt-embed.js"></script> </head> - 이제 영상의 VIDEO_ID를 확인하고 글에 링크를 넣으세요!
<lite-youtube videoid="VIDEO_ID"></lite-youtube> - 잘 나오나 확인합니다.
어? Shorts가 나오긴 나오는데 좀 아쉬운데요.
파일 수정
lite-youtube-embed 제작자가 바쁘거나 쇼츠에 관심이 없는 것 같습니다.
그래서 AI의 힘을 빌려 수정했습니다.
여기저기 직접 수정하려면 여러분도 귀찮으니까 완성본을 그대로 복붙해서 사용하세요.
클릭해서 코드 확인
class LiteYTEmbed extends HTMLElement {
connectedCallback() {
this.videoId = this.getAttribute('videoid');
let playBtnEl = this.querySelector('.lyt-playbtn,.lty-playbtn');
this.playLabel = (playBtnEl && playBtnEl.textContent.trim()) || this.getAttribute('playlabel') || 'Play';
this.dataset.title = this.getAttribute('title') || "";
this.orientation = this.getAttribute('orientation') || "";
this.customWidth = this.getAttribute('width');
if (this.orientation === 'shorts') {
this.classList.add('shorts-mode');
if (this.customWidth) {
this.dataset.shortsWidth = this.customWidth;
}
const shortsRatio = 9 / 16;
if (this.customWidth) {
this.style.paddingBottom = `calc(${this.customWidth} / ${shortsRatio})`;
this.style.width = this.customWidth;
this.style.margin = '0 auto';
} else {
this.style.paddingBottom = "calc(75% / (9 / 16))";
this.style.width = '75%';
this.style.margin = '0 auto';
}
} else {
if (this.hasAttribute('width')) {
this.style.width = this.customWidth;
this.style.margin = '0 auto';
this.style.paddingBottom = `calc(${this.customWidth} / (16 / 9))`;
} else {
this.style.paddingBottom = "calc(100% / (16 / 9))";
}
}
if (!this.style.backgroundImage) {
this.style.backgroundImage = `url("https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg")`;
this.upgradePosterImage();
}
if (!playBtnEl) {
playBtnEl = document.createElement('button');
playBtnEl.type = 'button';
playBtnEl.classList.add('lyt-playbtn', 'lty-playbtn');
this.append(playBtnEl);
}
if (!playBtnEl.textContent) {
const playBtnLabelEl = document.createElement('span');
playBtnLabelEl.className = 'lyt-visually-hidden';
playBtnLabelEl.textContent = this.playLabel;
playBtnEl.append(playBtnLabelEl);
}
this.addNoscriptIframe();
if (playBtnEl.nodeName === 'A') {
playBtnEl.removeAttribute('href');
playBtnEl.setAttribute('tabindex', '0');
playBtnEl.setAttribute('role', 'button');
playBtnEl.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.activate();
}
});
}
this.addEventListener('pointerover', LiteYTEmbed.warmConnections, { once: true });
this.addEventListener('focusin', LiteYTEmbed.warmConnections, { once: true });
this.addEventListener('click', this.activate);
this.needsYTApi = this.hasAttribute("js-api") || navigator.vendor.includes('Apple') || navigator.userAgent.includes('Mobi');
}
upgradePosterImage() {
setTimeout(() => {
const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`;
const img = new Image();
img.fetchPriority = 'low';
img.referrerpolicy = 'origin';
img.src = webpUrl;
img.onload = e => {
const noAvailablePoster = e.target.naturalHeight == 90 && e.target.naturalWidth == 120;
if (noAvailablePoster) return;
this.style.backgroundImage = `url("${webpUrl}")`;
}
}, 100);
}
static addPrefetch(kind, url, as) {
const linkEl = document.createElement('link');
linkEl.rel = kind;
linkEl.href = url;
if (as) {
linkEl.as = as;
}
document.head.append(linkEl);
}
static warmConnections() {
if (LiteYTEmbed.preconnected) return;
LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com');
LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');
LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');
LiteYTEmbed.preconnected = true;
}
fetchYTPlayerApi() {
if (window.YT || (window.YT && window.YT.Player)) return;
this.ytApiPromise = new Promise((res, rej) => {
var el = document.createElement('script');
el.src = 'https://www.youtube.com/iframe_api';
el.async = true;
el.onload = _ => {
YT.ready(res);
};
el.onerror = rej;
this.append(el);
});
}
async getYTPlayer() {
if (!this.playerPromise) {
await this.activate();
}
return this.playerPromise;
}
async addYTPlayerIframe() {
this.fetchYTPlayerApi();
await this.ytApiPromise;
const videoPlaceholderEl = document.createElement('div')
this.append(videoPlaceholderEl);
const paramsObj = Object.fromEntries(this.getParams().entries());
this.playerPromise = new Promise(resolve => {
let player = new YT.Player(videoPlaceholderEl, {
width: '100%',
videoId: this.videoId,
playerVars: paramsObj,
events: {
'onReady': event => {
event.target.playVideo();
resolve(player);
}
}
});
});
}
addNoscriptIframe() {
const iframeEl = this.createBasicIframe();
const noscriptEl = document.createElement('noscript');
noscriptEl.innerHTML = iframeEl.outerHTML;
this.append(noscriptEl);
}
getParams() {
const params = new URLSearchParams(this.getAttribute('params') || []);
params.append('autoplay', '1');
params.append('playsinline', '1');
return params;
}
async activate() {
if (this.classList.contains('lyt-activated')) return;
this.classList.add('lyt-activated');
if (this.needsYTApi) {
return this.addYTPlayerIframe(this.getParams());
}
const iframeEl = this.createBasicIframe();
this.append(iframeEl);
iframeEl.focus();
}
createBasicIframe() {
const iframeEl = document.createElement('iframe');
iframeEl.width = 560;
iframeEl.height = 315;
iframeEl.title = this.playLabel;
iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
iframeEl.allowFullscreen = true;
iframeEl.referrerPolicy = 'strict-origin-when-cross-origin';
iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${this.getParams().toString()}`;
return iframeEl;
}
}
customElements.define('lite-youtube', LiteYTEmbed);lite-youtube {
background-color: #000;
position: relative;
display: block;
contain: content;
background-position: center center;
background-size: cover;
cursor: pointer;
width: 100%;
height: 0;
}
lite-youtube::before {
content: attr(data-title);
display: block;
position: absolute;
top: 0;
background-image: linear-gradient(180deg, rgb(0 0 0 / 67%) 0%, rgb(0 0 0 / 54%) 14%, rgb(0 0 0 / 15%) 54%, rgb(0 0 0 / 5%) 72%, rgb(0 0 0 / 0%) 94%);
height: 99px;
width: 100%;
font-family: "YouTube Noto",Roboto,Arial,Helvetica,sans-serif;
color: hsl(0deg 0% 93.33%);
text-shadow: 0 0 2px rgba(0,0,0,.5);
font-size: 18px;
padding: 25px 20px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
box-sizing: border-box;
}
lite-youtube:hover::before {
color: white;
}
lite-youtube::after {
content: "";
display: block;
}
lite-youtube > iframe {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border: 0;
}
lite-youtube > .lyt-playbtn {
display: block;
width: 100%;
height: 100%;
background: no-repeat center/68px 48px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 48"><path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="red"/><path d="M45 24 27 14v20" fill="white"/></svg>');
position: absolute;
cursor: pointer;
z-index: 1;
filter: grayscale(100%);
transition: filter .1s cubic-bezier(0, 0, 0.2, 1);
border: 0;
}
lite-youtube:hover > .lyt-playbtn,
lite-youtube .lyt-playbtn:focus {
filter: none;
}
lite-youtube.lyt-activated {
cursor: unset;
}
lite-youtube.lyt-activated::before,
lite-youtube.lyt-activated > .lyt-playbtn {
opacity: 0;
pointer-events: none;
}
.lyt-visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}사용 방법
유튜브 기본 영상
<lite-youtube videoid="VIDEO_ID"></lite-youtube> 미리보기 이미지 변경하기
style=“background-image: url(‘이미지 주소’);” 를 추가합니다.
아래 예제는 미리보기가 오징어 게임이지만 클릭하면 Distirct 9이 재생됩니다.
<lite-youtube videoid="VIDEO_ID" style="background-image: url('이미지 주소');"></lite-youtube> 유튜브 쇼츠
orientation=“shorts” 를 추가합니다.
추가하지 않으면 세로로 길게 표시되지 않습니다.
<lite-youtube videoid="VIDEO_ID" orientation="shorts"></lite-youtube> 유튜브 쇼츠의 가로 너비
width=“WIDTH” 를 추가합니다.
지정하지 않으면 기본값 75%가 적용됩니다.
<lite-youtube videoid="VIDEO_ID" orientation="shorts" width="WIDTH"></lite-youtube> 기본값 자체를 변경하고 싶다면 lite-yt-embed.js 에서 2군데 75%를 원하는 크기로 수정하면 됩니다.
Astro에서 페이지 최적화
위 과정대로 스크립트와 css를 삽입할 경우 해당 페이지에 유튜브 영상이 없더라도 모든 페이지에 코드가 삽입됩니다.
코드가 크지 않으므로 무시하고 그냥 사용해도 됩니다만, 조금이라도 페이지의 크기를 줄이기 위해 lite-youtube가 사용된 페이지와 일반 페이지를 구분하고 그에 맞게 스크립트와 css가 포함되지 않도록 해봅시다.
title: 블로그에 유튜브 영상과 쇼츠를 반응형 사이즈로 넣는 방법
pubDate: 2025-12-22
slug: youtube-embed 대략 위와 같은 프론트매터를 사용하고 있을겁니다.
앞으로 영상을 삽입할 글에는 youtube: true 프론트매터를 추가해서 구분합니다.
그러기 위해서 content.config.ts 혹은 schema가 설정된 파일을 수정해야 하는데 예를 들어 아래와 같습니다.
const schema = z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
kind: z.enum(["article", "note"]),
tags: z.array(z.string()).optional(),
ogImage: z.string().optional(),
youtube: z.boolean().optional(),
});본인의 상황에 맞게 수정하면 됩니다.
어려우면 AI에게 아래의 lite-yt-embed.astro 파일과 함께 물어보시면 잘 수정해줍니다.
lite-yt-embed.css 와 lite-yt-embed.js 파일을 lite-yt-embed.astro 파일 하나로 통합합니다.
---
export interface Props {
enabled?: boolean;
}
const { enabled = false } = Astro.props;
---
{enabled && (
<script is:inline>
const LITE_YT_CSS = `
lite-youtube {
background-color: #000;
position: relative;
display: block;
contain: content;
background-position: center center;
background-size: cover;
cursor: pointer;
width: 100%;
height: 0;
}
lite-youtube::before {
content: attr(data-title);
display: block;
position: absolute;
top: 0;
background-image: linear-gradient(180deg, rgb(0 0 0 / 67%) 0%, rgb(0 0 0 / 54%) 14%, rgb(0 0 0 / 15%) 54%, rgb(0 0 0 / 5%) 72%, rgb(0 0 0 / 0%) 94%);
height: 99px;
width: 100%;
font-family: "YouTube Noto", Roboto, Arial, Helvetica, sans-serif;
color: hsl(0deg 0% 93.33%);
text-shadow: 0 0 2px rgba(0, 0, 0, .5);
font-size: 18px;
padding: 25px 20px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
box-sizing: border-box;
}
lite-youtube:hover::before {
color: white;
}
lite-youtube::after {
content: "";
display: block;
}
lite-youtube > iframe {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border: 0;
}
lite-youtube > .lyt-playbtn {
display: block;
width: 100%;
height: 100%;
background: no-repeat center/68px 48px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 48"><path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="red"/><path d="M45 24 27 14v20" fill="white"/></svg>');
position: absolute;
cursor: pointer;
z-index: 1;
filter: grayscale(100%);
transition: filter .1s cubic-bezier(0, 0, 0.2, 1);
border: 0;
}
lite-youtube:hover > .lyt-playbtn,
lite-youtube .lyt-playbtn:focus {
filter: none;
}
lite-youtube.lyt-activated {
cursor: unset;
}
lite-youtube.lyt-activated::before,
lite-youtube.lyt-activated > .lyt-playbtn {
opacity: 0;
pointer-events: none;
}
.lyt-visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
`;
function injectLiteYTStyles() {
if (document.querySelector('#lite-yt-styles')) return;
const style = document.createElement('style');
style.id = 'lite-yt-styles';
style.textContent = LITE_YT_CSS;
document.head.appendChild(style);
}
class LiteYTEmbed extends HTMLElement {
connectedCallback() {
injectLiteYTStyles();
this.videoId = this.getAttribute('videoid');
let playBtnEl = this.querySelector('.lyt-playbtn,.lty-playbtn');
this.playLabel = (playBtnEl && playBtnEl.textContent.trim()) || this.getAttribute('playlabel') || 'Play';
this.dataset.title = this.getAttribute('title') || "";
this.orientation = this.getAttribute('orientation') || "";
this.customWidth = this.getAttribute('width');
if (this.orientation === 'shorts') {
this.classList.add('shorts-mode');
const shortsRatio = 9 / 16;
if (this.customWidth) {
this.style.paddingBottom = `calc(${this.customWidth} / ${shortsRatio})`;
this.style.width = this.customWidth;
this.style.margin = '0 auto';
} else {
this.style.paddingBottom = "calc(75% / (9 / 16))";
this.style.width = '75%';
this.style.margin = '0 auto';
}
} else {
if (this.hasAttribute('width')) {
this.style.width = this.customWidth;
this.style.margin = '0 auto';
this.style.paddingBottom = `calc(${this.customWidth} / (16 / 9))`;
} else {
this.style.paddingBottom = "calc(100% / (16 / 9))";
}
}
if (!this.style.backgroundImage) {
this.style.backgroundImage = `url("https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg")`;
this.upgradePosterImage();
}
if (!playBtnEl) {
playBtnEl = document.createElement('button');
playBtnEl.type = 'button';
playBtnEl.classList.add('lyt-playbtn', 'lty-playbtn');
this.append(playBtnEl);
}
if (!playBtnEl.textContent) {
const playBtnLabelEl = document.createElement('span');
playBtnLabelEl.className = 'lyt-visually-hidden';
playBtnLabelEl.textContent = this.playLabel;
playBtnEl.append(playBtnLabelEl);
}
this.addNoscriptIframe();
if(playBtnEl.nodeName === 'A'){
playBtnEl.removeAttribute('href');
playBtnEl.setAttribute('tabindex', '0');
playBtnEl.setAttribute('role', 'button');
playBtnEl.addEventListener('keydown', e => {
if( e.key === 'Enter' || e.key === ' ' ){
e.preventDefault();
this.activate();
}
});
}
this.addEventListener('pointerover', this.constructor.warmConnections, {once: true});
this.addEventListener('focusin', this.constructor.warmConnections, {once: true});
this.addEventListener('click', this.activate);
this.needsYTApi = this.hasAttribute("js-api") || navigator.vendor.includes('Apple') || navigator.userAgent.includes('Mobi');
}
upgradePosterImage() {
setTimeout(() => {
const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`;
const img = new Image();
img.fetchPriority = 'low';
img.referrerpolicy = 'origin';
img.src = webpUrl;
img.onload = e => {
const noAvailablePoster = e.target.naturalHeight == 90 && e.target.naturalWidth == 120;
if (noAvailablePoster) return;
this.style.backgroundImage = `url("${webpUrl}")`;
};
}, 100);
}
static addPrefetch(kind, url, as) {
const linkEl = document.createElement('link');
linkEl.rel = kind;
linkEl.href = url;
if (as) {
linkEl.as = as;
}
document.head.append(linkEl);
}
static warmConnections() {
if (this.preconnected) return;
this.addPrefetch('preconnect', 'https://www.youtube-nocookie.com');
this.addPrefetch('preconnect', 'https://www.google.com');
this.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
this.addPrefetch('preconnect', 'https://static.doubleclick.net');
this.preconnected = true;
}
addNoscriptIframe() {
const iframeEl = this.createBasicIframe();
const noscriptEl = document.createElement('noscript');
noscriptEl.innerHTML = iframeEl.outerHTML;
this.append(noscriptEl);
}
getParams() {
const params = new URLSearchParams(this.getAttribute('params') || []);
params.append('autoplay', '1');
params.append('playsinline', '1');
return params;
}
async activate(){
if (this.classList.contains('lyt-activated')) return;
this.classList.add('lyt-activated');
if (this.needsYTApi) {
return this.addYTPlayerIframe(this.getParams());
}
const iframeEl = this.createBasicIframe();
this.append(iframeEl);
iframeEl.focus();
}
createBasicIframe(){
const iframeEl = document.createElement('iframe');
iframeEl.width = 560;
iframeEl.height = 315;
iframeEl.title = this.playLabel;
iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
iframeEl.allowFullscreen = true;
iframeEl.referrerPolicy = 'strict-origin-when-cross-origin';
iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${this.getParams().toString()}`;
return iframeEl;
}
fetchYTPlayerApi() {
if (window.YT || (window.YT && window.YT.Player)) return;
this.ytApiPromise = new Promise((res, rej) => {
var el = document.createElement('script');
el.src = 'https://www.youtube.com/iframe_api';
el.async = true;
el.onload = _ => {
YT.ready(res);
};
el.onerror = rej;
this.append(el);
});
}
async addYTPlayerIframe() {
this.fetchYTPlayerApi();
await this.ytApiPromise;
const videoPlaceholderEl = document.createElement('div')
this.append(videoPlaceholderEl);
const paramsObj = Object.fromEntries(this.getParams().entries());
this.playerPromise = new Promise(resolve => {
let player = new YT.Player(videoPlaceholderEl, {
width: '100%',
videoId: this.videoId,
playerVars: paramsObj,
events: {
'onReady': event => {
event.target.playVideo();
resolve(player);
}
}
});
});
}
}
customElements.define('lite-youtube', LiteYTEmbed);
</script>
)}마지막으로 개별페이지에 추가합니다.
항상 파일의 위치를 제대로 학인해야 합니다.
import LiteYTScript from "utils/lite-yt-embed.astro";
...
...
<Html>
...
{hasYoutube && <LiteYTScript enabled={hasYoutube} />}
...
</Html>잘 적용했다면 프론트매터에 youtube: true가 있는 글에만 lite-yt-embed 관련 스크립트와 css가 보여야 합니다.