워드프레스 블록 에디터 이미지 클릭 확대: 가벼운 모달·내비게이션·유휴 초기화로 속도 영향 최소화

워드프레스 블록 에디터로 이미지를 삽입할 때 ‘클릭해서 확대하기’ 옵션을 선택하면 사용자가 이미지를 클릭하면 라이트박스 효과로 이미지가 확대되어 표시됩니다.

워드프레스 블록 에디터 이미지 클릭 확대: 가벼운 모달·내비게이션·유휴 초기화로 속도 영향 최소화 1

이미지를 삽입할 때 이 옵션을 선택하지 않더라도 링크가 없는 이미지를 사용자가 클릭하면 모달 이미지로 표시되도록 하고 싶은 경우 아래의 자바스크립트 코드를 활용할 수 있습니다.

워드프레스 블록 에디터 이미지 클릭 확대: 가벼운 모달·내비게이션·유휴 초기화로 속도 영향 최소화

워드프레스닷컴(WordPress.com)의 경우 비즈니스 요금제 이상에서만 플러그인을 설치하여 자바스크립트 코드를 추가할 수 있지만, 한시적으로 개인 요금제와 프리미엄 요금제에서도 플러그인 설치를 허용하는 행사를 진행하고 있어서 개인 요금제로 이 블로그를 개설했습니다.

이미지를 삽입할 때 ‘클릭하여 확대하기’ 옵션을 일일이 선택하는 것이 번거로워서 JS 코드로 링크가 설정되지 않은 포스트 내의 모든 이미지에 대하여 클릭 시 라이트박스 효과로 크게 확대되어 표시되도록 해보았습니다.

다음과 같은 사항을 고려하여 코드를 만들었습니다.

  • 가볍고 빠른 풀사이즈 이미지 모달 + 좌/우 내비게이션.
  • 링크 없는 블록 에디터 이미지 클릭 시 모달 오픈
  • Full 사이즈 우선 표시(data-full-url 등 → srcset 최대폭 → src)
  • 뷰포트 거터(좌우 여백) 적용: 이미지 프레이밍 + 탐색 버튼 항상 가시화
  • 내비게이션 버튼 + ←/→ 키로 이전/다음 전환, ESC/배경/X로 닫기
  • 첫 상호작용 시 지연 초기화(pointerdown + requestIdleCallback) → Lighthouse 친화적. 구글 페이지스피드 인사이트 점수에 영향 최소화.
  • 이벤트 위임 1회만 설치, 단일 모달 동적 생성(필요 시 CSS 주입)
  • 접근성: role=”dialog”, aria-modal 지원, 포커스 관리(열기/닫기 복귀)
  • 스크롤 잠금 + 스크롤바 폭 보정으로 CLS 방지
  • 수동 프리로드: 이웃 이미지 미리 로드로 전환 지연 감소
  • 반응형 데스크톱 전용(모바일<768px 제외), 링크로 감싼 이미지는 무시
  • 모던 UI: 투명 오버레이, 글래스 버튼(반투명+보더+블러), 호버/포커스 상태
  • CSS 변수로 여백/색상 톤 간단 커스터마이즈 가능

이미지를 클릭하면 다음 그림과 같이 이미지가 확대되어 표시되며 좌우 내비게이션 버튼을 클릭하여 동일 포스트 내의 이전/다음 이미지를 이동이 가능합니다.

워드프레스 블록 에디터 이미지 클릭 확대: 가벼운 모달·내비게이션·유휴 초기화로 속도 영향 최소화 2

다음과 같은 자바스크립트를 활용할 수 있습니다.

<script>
/**
 * WordPress 블록 이미지 모달 (full size + 좌/우 내비 + 가터 + modern overlay/buttons)
 * - 오버레이: 살짝 투명
 * - 버튼: 밝은 반투명 surface + 보더 + 블러 + hover/focus 대비
 * - 지연 초기화/위임/CLS-safe/idle 설치는 동일
 */
(function () {
  "use strict";

  const SEL_IMG = ".wp-block-image img, .wp-block-image figure img";
  const SEL_CONTAINER_CANDIDATES = [
    "article", ".entry-content", ".post-content", ".single-post",
    ".hentry", ".site-main", "main", ".wp-block-post-content"
  ];

  let installed = false;
  let idleId = 0;
  let bootstrapBound = false;

  // modal/galleria state
  let root = null, imgEl = null, closeBtn = null, prevBtn = null, nextBtn = null, lastFocused = null;
  let gallery = [];
  let index = -1;

  // ---------- bootstrap (최소 오버헤드)
  function onPointerDownBootstrap(e) {
    const img = matchTargetImage(e.target);
    if (!img) return;
    initDelegation();
    openFrom(img);
    teardownBootstrap();
  }

  function scheduleIdleInstall() {
    const onIdle = () => { initDelegation(); teardownBootstrap(); };
    if ("requestIdleCallback" in window) {
      idleId = requestIdleCallback(onIdle, { timeout: 3000 });
    } else {
      idleId = setTimeout(onIdle, 1200);
    }
  }

  function attachBootstrap() {
    if (bootstrapBound) return;
    bootstrapBound = true;
    window.addEventListener("pointerdown", onPointerDownBootstrap, { capture: true, passive: true });
    scheduleIdleInstall();
  }

  function teardownBootstrap() {
    if (!bootstrapBound) return;
    bootstrapBound = false;
    window.removeEventListener("pointerdown", onPointerDownBootstrap, { capture: true });
    if ("cancelIdleCallback" in window && typeof idleId === "number") {
      try { cancelIdleCallback(idleId); } catch (_) {}
    } else {
      clearTimeout(idleId);
    }
  }

  // ---------- delegation install
  function initDelegation() {
    if (installed) return;
    installed = true;
    document.addEventListener("click", onClickDelegated, { passive: true });
    document.addEventListener("keydown", onKeydown, { passive: true });
  }

  // ---------- handlers
  function onClickDelegated(e) {
    const img = matchTargetImage(e.target);
    if (!img) return;
    openFrom(img);
  }

  function onKeydown(e) {
    if (e.key === "Escape") hide();
    if (!root || root.style.display !== "block") return;
    if (e.key === "ArrowLeft") { e.preventDefault?.(); prev(); }
    else if (e.key === "ArrowRight") { e.preventDefault?.(); next(); }
  }

  // ---------- target match
  function isDesktop() { return window.matchMedia("(min-width: 768px)").matches; }

  function matchTargetImage(target) {
    const img = target?.closest?.(SEL_IMG);
    if (!img) return null;
    if (!isDesktop()) return null;
    if (img.closest("a")) return null;
    return img;
  }

  // ---------- container & gallery
  function findPostContainer(fromEl) {
    let node = fromEl.parentElement;
    while (node && node !== document.body) {
      if (SEL_CONTAINER_CANDIDATES.some(sel => node.matches?.(sel))) return node;
      node = node.parentElement;
    }
    return document;
  }

  function collectGallery(container) {
    return Array.from(container.querySelectorAll(SEL_IMG))
      .filter(img => !img.closest("a") && isDesktop());
  }

  function findIndexInGallery(img, list) {
    const idx = list.indexOf(img);
    if (idx >= 0) return idx;
    const target = resolveFullSrc(img);
    return Math.max(0, list.findIndex(i => resolveFullSrc(i) === target));
  }

  // ---------- full-size resolver
  function resolveFullSrc(imgEl) {
    const figure = imgEl.closest("figure, .wp-block-image");
    const candAttrs = [
      "data-full-url", "data-orig-file", "data-large-file",
      "data-fullsize-url", "data-original", "data-src"
    ];
    for (const el of [imgEl, figure]) {
      if (!el) continue;
      for (const name of candAttrs) {
        const v = el.getAttribute?.(name);
        if (v) return v;
      }
    }
    const fromSrcset = pickLargestFromSrcset(imgEl);
    if (fromSrcset) return fromSrcset;
    return imgEl.currentSrc || imgEl.src || "";
  }

  function pickLargestFromSrcset(imgEl) {
    const ss = imgEl.getAttribute("srcset");
    if (!ss) return "";
    const candidates = ss.split(",").map(s => s.trim()).map(s => {
      const [url, d] = s.split(/\s+/);
      const w = d && d.endsWith("w") ? parseInt(d, 10) : 0;
      return { url, w };
    }).filter(c => c.url);
    if (!candidates.length) return "";
    candidates.sort((a, b) => b.w - a.w);
    return candidates[0].url;
  }

  // ---------- modal lifecycle
  function ensureModal() {
    if (root) return;

    if (!document.querySelector('style[data-cc="image-modal-styles"]')) {
      const style = document.createElement("style");
      style.dataset.cc = "image-modal-styles";
      style.textContent = `
        .cc-image-modal{
          display:none;position:fixed;inset:0;z-index:9999;
          /* 더 투명하고 깊이감 있는 오버레이 */
          background:rgba(10,10,12,.72);
          --cc-gutter:56px;    /* 좌우 여백(가터) */
          --cc-vpad:24px;      /* 상하 패딩 */
          --cc-surface: rgba(255,255,255,.18);  /* 밝은 반투명 버튼 배경 */
          --cc-surface-hov: rgba(255,255,255,.28);
          --cc-border: rgba(255,255,255,.30);
          --cc-icon: #fff;
          --cc-shadow: 0 4px 14px rgba(0,0,0,.35);
        }
        @media (min-width: 1200px){
          .cc-image-modal{ --cc-gutter:72px; --cc-vpad:28px; }
        }
        @media (max-width: 1024px){
          .cc-image-modal{ --cc-gutter:44px; --cc-vpad:20px; }
        }
        @media (max-width: 840px){
          .cc-image-modal{ --cc-gutter:36px; --cc-vpad:16px; }
        }
        .cc-image-modal[open]{display:block}
        .cc-image-modal__backdrop{position:absolute;inset:0}
        /* 이미지가 가터를 침범하지 않도록 프레임 안에서 최대화 */
        .cc-image-modal__img{
          position:absolute;
          top:var(--cc-vpad); bottom:var(--cc-vpad);
          left:var(--cc-gutter); right:var(--cc-gutter);
          margin:auto; display:block;
          max-width:calc(100vw - (var(--cc-gutter) * 2));
          max-height:calc(100vh - (var(--cc-vpad) * 2));
          width:auto; height:auto;
        }
        /* 닫기 버튼: 밝은 유리 표면 + 보더 + 블러 */
        .cc-image-modal__close{
          position:absolute; top:var(--cc-vpad); right:calc(var(--cc-gutter) - 8px);
          color:var(--cc-icon); cursor:pointer; border:1px solid var(--cc-border);
          background:var(--cc-surface); border-radius:12px; width:40px; height:40px;
          font-size:26px; line-height:36px; text-align:center; z-index:3;
          box-shadow: var(--cc-shadow);
          -webkit-backdrop-filter:saturate(140%) blur(6px);
          backdrop-filter:saturate(140%) blur(6px);
        }
        .cc-image-modal__close:hover{ background:var(--cc-surface-hov); }
        .cc-image-modal__close:focus{ outline:2px solid rgba(255,255,255,.7); outline-offset:2px; }

        /* 내비 버튼: 동일 톤, 항상 이미지 위 */
        .cc-image-modal__nav{
          position:absolute; top:50%; transform:translateY(-50%);
          border:1px solid var(--cc-border); background:var(--cc-surface); color:var(--cc-icon);
          font-size:24px; width:48px; height:64px; border-radius:14px; cursor:pointer;
          z-index:2; box-shadow: var(--cc-shadow);
          -webkit-backdrop-filter:saturate(140%) blur(6px);
          backdrop-filter:saturate(140%) blur(6px);
        }
        .cc-image-modal__nav:hover{ background:var(--cc-surface-hov); }
        .cc-image-modal__nav:focus{ outline:2px solid rgba(255,255,255,.7); outline-offset:2px; }
        .cc-image-modal__nav--prev{ left:16px; }
        .cc-image-modal__nav--next{ right:16px; }

        @media (prefers-reduced-motion:no-preference){
          .cc-image-modal[open] .cc-image-modal__img{ transition:opacity .18s ease; }
          .cc-image-modal__nav, .cc-image-modal__close{ transition:background-color .15s ease, transform .15s ease, box-shadow .15s ease; }
          .cc-image-modal__nav:hover, .cc-image-modal__close:hover{ transform:translateY(-1px); }
        }
      `;
      document.head.appendChild(style);
    }

    root = document.createElement("div");
    root.className = "cc-image-modal";
    root.setAttribute("role", "dialog");
    root.setAttribute("aria-modal", "true");
    root.setAttribute("aria-label", "Image preview");

    const backdrop = document.createElement("div");
    backdrop.className = "cc-image-modal__backdrop";

    imgEl = document.createElement("img");
    imgEl.className = "cc-image-modal__img";
    imgEl.alt = "";

    closeBtn = document.createElement("button");
    closeBtn.className = "cc-image-modal__close";
    closeBtn.setAttribute("aria-label", "Close");
    closeBtn.innerHTML = "×";

    prevBtn = document.createElement("button");
    prevBtn.className = "cc-image-modal__nav cc-image-modal__nav--prev";
    prevBtn.setAttribute("aria-label", "Previous image");
    prevBtn.textContent = "‹";

    nextBtn = document.createElement("button");
    nextBtn.className = "cc-image-modal__nav cc-image-modal__nav--next";
    nextBtn.setAttribute("aria-label", "Next image");
    nextBtn.textContent = "›";

    backdrop.addEventListener("click", hide, { passive: true });
    closeBtn.addEventListener("click", hide);
    prevBtn.addEventListener("click", () => prev());
    nextBtn.addEventListener("click", () => next());

    root.appendChild(backdrop);
    root.appendChild(imgEl);
    root.appendChild(closeBtn);
    root.appendChild(prevBtn);
    root.appendChild(nextBtn);
    document.body.appendChild(root);
  }

  // ---------- CLS-safe scroll lock
  function lockScroll(lock) {
    const html = document.documentElement;
    const body = document.body;
    if (lock) {
      const sw = window.innerWidth - html.clientWidth;
      if (sw > 0) {
        html.style.paddingRight = sw + "px";
        body.style.paddingRight = sw + "px";
      }
      html.style.overflow = "hidden";
      body.style.overflow = "hidden";
    } else {
      html.style.overflow = "";
      body.style.overflow = "";
      html.style.paddingRight = "";
      body.style.paddingRight = "";
    }
  }

  // ---------- open/close & navigation
  function openFrom(clickedImg) {
    const container = findPostContainer(clickedImg);
    gallery = collectGallery(container);
    if (gallery.length === 0) return;

    index = findIndexInGallery(clickedImg, gallery);
    ensureModal();
    updateNavVisibility();
    showIndex(index);
    lastFocused = document.activeElement;
    root.setAttribute("open", "");
    root.style.display = "block";
    lockScroll(true);
    closeBtn.focus({ preventScroll: true });
  }

  function hide() {
    if (!root) return;
    root.removeAttribute("open");
    root.style.display = "none";
    lockScroll(false);
    try { lastFocused?.focus?.({ preventScroll: true }); } catch (_) {}
  }

  function showIndex(i) {
    if (!gallery[i]) return;
    const src = resolveFullSrc(gallery[i]);
    if (!src) return;

    const run = () => {
      imgEl.src = src;
      preloadAround(i);
    };

    if (gallery[i].decode) {
      gallery[i].decode().catch(() => {}).finally(() => requestAnimationFrame(run));
    } else {
      requestAnimationFrame(run);
    }
  }

  function prev() {
    if (gallery.length <= 1) return;
    index = (index - 1 + gallery.length) % gallery.length;
    showIndex(index);
  }

  function next() {
    if (gallery.length <= 1) return;
    index = (index + 1) % gallery.length;
    showIndex(index);
  }

  function updateNavVisibility() {
    const visible = gallery.length > 1;
    prevBtn.style.display = visible ? "block" : "none";
    nextBtn.style.display = visible ? "block" : "none";
  }

  function preloadAround(i) {
    const idxs = [(i + 1) % gallery.length, (i - 1 + gallery.length) % gallery.length];
    idxs.forEach(k => {
      const g = gallery[k];
      if (!g) return;
      const url = resolveFullSrc(g);
      if (!url) return;
      const im = new Image();
      im.decoding = "async";
      im.src = url;
    });
  }

  // ---------- boot
  attachBootstrap();
})();
</script>

가입형 플러그인을 사용하는 경우 WPCode 플러그인을 설치하여 푸터 영역에 상기 자스 코드를 추가하면 됩니다.😄

상기 코드는 워드프레스 정보꾸러미 블로그에 소개된 코드를 참고하여 개선한 것입니다.

사이트 속도에 대한 영향을 최소화하도록 코드를 만들었기 때문에 사이트 속도에는 별 영향을 안 미칠 것이라 생각됩니다.

참고

댓글 남기기

워드프레스 가이드에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기