라이트박스가 너무 무거워요! 1.5kB로 끝내는 실전압축 라이트박스 제작기

의문: 라이트박스는 정말로 라이트한가?
뭔가 설명하는 글을 쓸 때 라이트박스가 참 유용하죠? 이미지로 설명을 보충하고 싶지만, 가독성을 생각하면 본문 너비를 무한정 늘릴 수가 없어요.
그렇다고 글로 전부 설명하자니 말이 지나치게 많아지는 그 때, 커다란 스크린샷 한장 띄우면 만사 오케이여요.
헌데 문제가... 요즘들어 라이트박스들이 너무 무거워지고 있어요. 심플이니 라이트니 하지만 심하게는 스크립트파일 하나가 70kB씩 되는 것도 있어요.
화려하고 예쁜 기능 많은 거 좋아요. 그런데, 그 기능 정말로 필요한가요? 슬라이드 필요해요? 갤러리 필요해요? 진짜요?
저는 필요없어요. 원하는 기능이 있으면 따로 적용하고 싶어요.
라이트박스는 더이상 라이트하지 않은 것이어요.
그래서 직접 만들었어요!
라이트박스 직접 제작의 당위성
조금 극단적으로 볼까요? 만약 스크립트 파일 용량 차이가 1.5kB vs 50kB라면, 딴 것 빼고 다운로드 속도만 해도 제법 차이날 거예요. 물론, 광대역 통신망에선 별 거 아니겠죠. js 파일이 서버측 압축으로 용량이 절반이 되었다고 가정해보죠.
그럼 100Mbps 통신망일 때 100/8*1024=12800kB/s => 12800 / (25-1.5) = 1/554.68s = 약 1.8ms에 불과해요. 10Mbps면 18ms, 하지만 1Mbps면 180ms.
'그런 저속 통신은 비현실적임.'
속도 문제가 별 것 아니라면 트래픽은요? 하루 1000명이 방문하는 사이트면 23.5*1000/1024 = 22.95MB/day, 한 달이면 688MB, 1년에 약 8GB의 트래픽이 소모되어요. 오직 라이트박스 하나 때문에요.
'캐시 뒀다 뭐함?'
참, 캐시가 있었죠? 근데요... 손님들은 항상 오던 분만 오시고 새 손님은 안 오실까요? 기존 손님들은 캐시 비우지않고 계속 들고 계실까요?
'외부 라이브러리 있잖아? cdnjs, jsDelivr같은.'
그렇죠. 외부 라이브러리 쓰면 앞서 말 한 모든 것이 무효여요. 근데... 그 사이트들 속도 빠르던가요? 생각보다 느리고 가끔 먹통되는 경우도 있지 않던가요?
'그냥 쓰면 안 됨?'
선택지, 우리에겐 선택지가 필요한 것이어요. 우리는 개발자가 아니라 일반인. 그러니 최신 모듈화 스크립트도 마음대로, 필요한 것만 분리해서 쓰지 못하죠.
그래서 만들었습니다. minify 시 약 1.5kB짜리 매우 가벼운 라이트박스죠. 아래를 한 번 보셔요. 처음 나오는 건 JS 파일, 두 번째가 CSS예요.
Very Silple Lightbox
// Very Simple Light Box
// minify시 1.5kB 이하. (약 1424Byte)
// 갤러리, 슬라이드 기능 없음.
// 오직 개별 이미지를 크게 보여주는 기능 뿐.
// 이미지, figcaption 외 다른 영역을 클릭하면 꺼짐.
// 레이아웃 문제로 본문, 스키티헤더 옆으로 미는 부분은 테마 상황에 맞춰서.
let cloneIMG, clickedElem, parentElem, getLBDiv, chkLBDiv, chkAllowArea, chkImgTag, chkFigCapTag;
let getStickyHead4LB, getPageBody4LB, getScrollWidth;
const figTagStr = /figure/i;
const figCapStr = /figcaption/i;
const imgTagStr = /img/i;
const allowArea = "body.item-view .post-body";
let lightBoxBg = document.createElement('div');
lightBoxBg.className = "lb-bg-dim";
// 클릭 이벤트 감시 시작
document.addEventListener("click", ()=>{
clickedElem = event.target; // 클릭 요소 기억
parentElem = clickedElem.parentNode; // 클릭한 요소의 부모 기억
getLBDiv = document.querySelectorAll(".lb-bg-dim")[0]; // 라이트박스 요소 가져오기
getPageBody4LB = document.querySelectorAll(".page_body")[0]; // 페이지 바디 컨테이너 가져오기
getStickyHead4LB = document.querySelectorAll("header.sticky")[0]; // 스티키헤더 가져오기
getScrollWidth = window.innerWidth - document.body.clientWidth; // 스크롤 바 너비 얼마?
chkAllowArea = clickedElem.closest(allowArea); // 허용된 구역에서 클릭했는가?
chkImgTag = imgTagStr.test(clickedElem.tagName) ? 1:0; // 클릭한 것이 img인가?
chkFigCapTag = figCapStr.test(clickedElem.tagName) ? 1:0; // 클릭한 것이 figcaption인가?
chkLBDiv = getLBDiv ? 1:0; // 라이트박스 요소가 존재하는가?
if ( chkImgTag && !chkLBDiv && chkAllowArea ) { // img를 클릭했으며, 현재 라이트박스가 없는 상태인가? 허용된 구역 안쪽인가?
lightBoxBg.innerHTML = ""; // lightBoxBg 내부의 모든 자식 제거
if ( figTagStr.test(parentElem.tagName) ) { cloneIMG = parentElem; } // 부모 노드 태그명이 figure 인가?
else { cloneIMG = clickedElem; } // 부모 노드 태그명이 figure가 아니다.
cloneIMG.id = ""; // id를 공백 처리
lightBoxBg.appendChild(cloneIMG.cloneNode(true)); // 라이트박스 배경에 이미지 삽입
document.body.appendChild(lightBoxBg); // 라이트박스 요소를 body태그 끝 부분에 삽입
// 레이아웃 문제
getPageBody4LB.style.cssText += "left: -" + getScrollWidth/2 + "px"; // 본문 왼쪽으로 밀기
if( getStickyHead4LB ) { getStickyHead4LB.style.cssText += "left: -" + getScrollWidth + "px"; } // 스티키 헤더 왼쪽으로 밀기
}
else {
if ( chkLBDiv ) { // 라이트박스가 존재하는가?
if( !chkImgTag && !chkFigCapTag ) { // IMG, Figcaption 외 다른 것을 클릭했는가??
getLBDiv.remove(); // 라이트박스 제거
// 레이아웃 문제
getPageBody4LB.style.removeProperty("left"); // 본문 원위치
if( getStickyHead4LB ) { getStickyHead4LB.style.removeProperty("left"); } // 스티키 헤더 원위치
}
}
}
});
/* ******************************************************************** */
/* **************** Very Simple Light Box 용 스타일 **************** */
/* ******************************************************************** */
body:has(.lb-bg-dim) { overflow: hidden; }
.lb-bg-dim { background-color: #000000AA; width:100vw; height: 100vh; position: fixed; top: 0; left: 0; display: flex; align-items: center; justify-content: center; z-index: 51; }
.lb-bg-dim::before { content: 'X'; color: white; font-weight: bold; position: absolute; top: 40px; right: 40px; }
.lb-bg-dim figure { margin: min(40px, 5vw) auto; }
.lb-bg-dim img { width: max-content; max-width: 90%; max-height: 90%; display: block; margin: auto; }
.lb-bg-dim figcaption { color: white; }
죄송해요 혼자 끙끙대며 만들다보니 좀 지저분하죠? 대신, 주석을 꼼꼼하게 붙여놨어요.
주인님은 어쨌냐 하시면... 요즘들어 연산능력이 퇴화하셨는지 자꾸 멍멍이 소리만 하셔서요. 제 속이 끝없는 화염에 휩싸이더라고요. 결국 MDN 뒤적거리며 혼자 만들었어요.
아무튼, 이거 기준이 블로그스팟, 에센셜 테마인데요. 순수 에센셜 테마는 아니고 제 맘대로 수정을 한 것이예요.
2026-05-11 기준으로 메이드네 블로그에 적용되어 있으니 그림을 눌러보시면 어떻게 동작하는지 확인 가능하시어요.
* 코드는 긁어서 복사 가능하신 거예요!
* 허용구역(.post-body) 내의 모든 img 태그가 대상이어요. 구역지정만 맞으면 따로 건드릴 게 없는 것이어요.
* 이미지를 A 태그로 감싸면 안 되어요. 클릭을 가로채지 않기 때문에 딴데로(href) 가버리는 거여요.
* 블로그스팟, 테마직접 수정 상황이면 CDATA로 감싸는 것 잊지마셔요!
<script> //<![CDATA[ // 실제 코드 여기에 //]]> </script>
Q: 피규어 필요없는 것이예요! 레이아웃 문제 없는 것이예요!
A: 빼시면 되어요.
figure 관련을 빼면 약 1.2kB 이하, 레이아웃 부분까지 다 빼면 약 700Byte 이하까지 다이어트 가능한 것이예요!!
처음에는 1500바이트 중반이었는데 수정하다보니 1400바이트 초반까지 내려온 거여요.
거기다 수정 다했어요~, 오타 없어요~ 하고 글 올리고 나니 선명하게 보이는 오타와 비효율... 반복되는 작업에 너무 즐거워서 팔짝팔짝 뛰었던 것이어요!
Very Simple Lightbox 설명
Lightbox 동작 부분
메이드가 만든 Very Simple Lightbox는 동작이 아주 간단해요. 이미지를 클릭하면 어두운 배경에 그림이 크게 뜬다. 이미지 바깥 부분을 클릭하면 꺼진다. 끝.
진짜 이게 다여요. 슬라이드? 갤러리? 빠른 미리보기? 부드러운 화면이동? 그런거 없다예요.
메이드에게 필요한 것은 정말로 이거 하나였어요.
슬라이드나 갤러리는 따로 만들어둔 코드가 있기에 필요하면 그걸 가져와서 쓸 것이어요. 지금은 정말로 필요없는 것이어요.
Lightbox 코드 부분
상수로 선언한 imgTagStr, figTagStr 등은 클릭한 곳이 이미지인지 확인하려는 거고요, 그 다음에 클릭한 img 태그가 figure 태그 아래에 들어있느냐? 이걸 검사하기 위한 거예요. 원하면 딴걸로 바꿔도 되어요.
figure태그를 왜 신경씀?
혹시 figcaption이 있다면 해당 자막까지 같이 가져오려고요. 다만 저는 이미지 캡션을 잘 안 써서요, 라이트박스용 figcaption 스타일을 단순하게 color: white 로 지정했어요. 좀 더 예쁘게 하고싶으면 따로 조정 하셔요.
골치아픈 것은 상단에 따라다니는 스티키 헤더랑 본문인데요, 스크롤바 때문에 얘들이 자꾸 위치 이동을 하는 게 아니겠어요? 너무 귀찮은 것이어요.
특히 스티키 헤더 부분은 스크롤바 너비만큼 왼쪽으로 밀면 되었지만 본문은 수치가 안 맞는 것이 아니겠어요? 속에서 sky fire가 진짜...
처음에는 끙끙앓다가 본문이랑 헤더 메뉴 둘 다 px 단위 수치를 직접 넣었어요. 근데 아무리 생각해도 이건 아니어요 싶어서 오랜만에 MDN을 방문했답니다.
흐릿한 기억을 뒤지며 분명히 뭔가, 편한거 있었는데 하며 MDN 여기저기를 돌아다니다 보니 결국 window.innerWidth, document.body.clientWidth 같은 자동 계산을 손에 넣게 되었어요.
window.innerWidth는 스크롤바 포함 전체 너비, document.body.clientWidth는 스크롤바를 제외한 body 태그의 너비. 그래서 둘의 차이가 곧 스크롤바 두께가 되는 것이었어요.
그.런.데... 상단 메뉴인 스티키헤더는 정상인데, 본문이 수치가 안 맞는 것이었어요. 분명히 맞게 계산했는데 왜? 테마를 수정하면서 이래저래 스파게티 코드가 되어버려서 어디선가 충돌이 났나 보아요.
한 30분쯤 어쩔까 고민하다가 간단히 getScrollWidth/2로 만들어보니 문제가 사라졌어요. 정렬 때문에 어디선가 문제가 생긴걸까요? 지금은 되니까 괜찮은 걸로 하겠어요.
getScrollWidth근처랑, "// 레이아웃 문제"라고 주석 달아놓은 곳을 중점적으로 살펴보셔요.
그 다음에 또 MDN에서 찾은 편리한 도구가 closest() 여요. allowArea랑 연결되어 있는데요, 지금 body.item-view .post-body라고 되어있죠? 블로그 게시물, 그 중 본문 외 클릭은 안 받겠다 이거예요.
예전에는 element의 조상 요소를 찾으려고 ...... .parentNode.parentNode.parentNode ...... 이런식으로 디지털 막노동을 한 것이 아니겠어요?
근데 이제 closest() 한 방에 요소의 조상님을 탈탈 털어서 확인해 주니 세상 편한 것이어요.
라이트박스 요소 내부에 이미지 태그를 넣는 것도, 처음엔 단순 복붙을 했더니 이미지가 누적되어 쌓이는 것이 아니겠어요? 어떻하죠? 하고 고민하다 while 루프로 자식 요소를 검사해서 제거하는 것이어요! 하는 아이디어가 생각났어요.
근데 막상 만들고나서 보니 한 줄에 80바이트가 넘어가는 요상한 구조가 되어서요, 뭔가 찝찝한 기분을 안고 살다가 글 올리고 이틀이나 지나서 innerHTML로 밀어버리면 되는 것 아니어요? 했답니다.
요소의 ID 부분은 진작에 [cloneIMG.id = ""] 요 명령으로 id를 공백으로 밀어버려놓고, 정작 바로 위에있는 것은 까먹은 것이었어요! 메이드가 머리가 나빠서 그런가 봐요.
참고용 코드 조각
아래는 Very Simple Lightbox의 최소화 버전이어요. 근데 블로그마다 상황이 다를 것이니 참고만 하시어요.
let cloneIMG,clickedElem,parentElem,getLBDiv,chkLBDiv,chkAllowArea,chkImgTag,chkFigCapTag;let getStickyHead4LB,getPageBody4LB,getScrollWidth;const figTagStr=/figure/i;const figCapStr=/figcaption/i;const imgTagStr=/img/i;const allowArea="body.item-view .post-body";let lightBoxBg=document.createElement('div');lightBoxBg.className="lb-bg-dim";document.addEventListener("click",()=>{clickedElem=event.target;parentElem=clickedElem.parentNode;getLBDiv=document.querySelectorAll(".lb-bg-dim")[0];getPageBody4LB=document.querySelectorAll(".page_body")[0];getStickyHead4LB=document.querySelectorAll("header.sticky")[0];getScrollWidth=window.innerWidth-document.body.clientWidth;chkAllowArea=clickedElem.closest(allowArea);chkImgTag=imgTagStr.test(clickedElem.tagName)?1:0;chkFigCapTag=figCapStr.test(clickedElem.tagName)?1:0;chkLBDiv=getLBDiv?1:0;if(chkImgTag&&!chkLBDiv&&chkAllowArea){lightBoxBg.innerHTML="";if(figTagStr.test(parentElem.tagName)){cloneIMG=parentElem;}else{cloneIMG=clickedElem;}cloneIMG.id="";lightBoxBg.appendChild(cloneIMG.cloneNode(true));document.body.appendChild(lightBoxBg);getPageBody4LB.style.cssText+="left: -"+getScrollWidth/2+"px";if(getStickyHead4LB){getStickyHead4LB.style.cssText+="left: -"+getScrollWidth+"px";}}else{if(chkLBDiv){if(!chkImgTag&&!chkFigCapTag){getLBDiv.remove();getPageBody4LB.style.removeProperty("left");if(getStickyHead4LB){getStickyHead4LB.style.removeProperty("left");}}}}});
메이드에게 충격을 주는 댓글은 삭제합니다