디버깅하기

Jekyll 디버깅과 트러블슈팅 완전 가이드

아무리 클로드랑 GPT랑 디버깅을 한다고 해도, 빌드 과정이나 서빙 과정에서 보여지는 로그(주로 콘솔)만으로 문제가 뭔지 특정하기에는 어려운 점이 있습니다. 지킬과 자바스크립트의 조합에서는 다양한 문제가 발생하기 때문입니다… 이번 포스트에서는 자바스크립트를 모르는 제가 브라우저에서 문제를 찾아 디버깅하는 과정을 알아보겠습니다.

가장 중요한 디버깅 도구는 브라우저에 내장된 개발자 도구입니다. F12를 눌러서 열 수 있어요.

브라우저의 개발자 도구에서 얻을 수 있는 정보

Elements 탭에서 HTML 구조 확인

카테고리 시스템이 제대로 작동하지 않을 때 가장 먼저 확인해야 할 것들:

// 브라우저 콘솔에서 실행할 수 있는 디버깅 코드들

// 1. 서브카테고리 카드들이 올바른 데이터를 가지고 있는지 확인
document.querySelectorAll(".subcategory-card").forEach((card) => {
  console.log("카드:", card.dataset.subcategory, "상위:", card.dataset.parent);
});

// 2. 서브카테고리 포스트 컨테이너들이 존재하는지 확인
document.querySelectorAll("[data-subcategory]").forEach((container) => {
  if (container.classList.contains("posts-container")) {
    console.log(
      "컨테이너:",
      container.dataset.subcategory,
      "상위:",
      container.dataset.parent
    );
  }
});

// 3. 특정 선택자로 컨테이너를 찾을 수 있는지 테스트
const container = document.querySelector(
  '[data-subcategory="genai-llm"][data-parent="ML&AI"]'
);
console.log("찾은 컨테이너:", container);

Console 탭에서 JavaScript 에러 추적

JavaScript 코드가 실행될 때 발생하는 에러들을 실시간으로 확인할 수 있습니다:

// 에러 처리를 포함한 개선된 이벤트 핸들러
document.addEventListener("click", function (e) {
  if (e.target.closest(".subcategory-card")) {
    try {
      const subcatCard = e.target.closest(".subcategory-card");
      const subcategory = subcatCard.dataset.subcategory;
      const parentCategory = subcatCard.dataset.parent;

      // 필수 데이터 검증
      if (!subcategory || !parentCategory) {
        throw new Error(
          `필수 데이터 누락: subcategory=${subcategory}, parent=${parentCategory}`
        );
      }

      console.log(
        "✅ 서브카테고리 클릭:",
        subcategory,
        "상위:",
        parentCategory
      );

      // 컨테이너 찾기
      const subcatContainer = document.querySelector(
        `[data-subcategory="${subcategory}"][data-parent="${parentCategory}"]`
      );

      if (!subcatContainer) {
        throw new Error(
          `컨테이너를 찾을 수 없음: ${subcategory} (${parentCategory})`
        );
      }

      // 성공적으로 처리
      showSubcategoryPosts(subcatContainer, subcatCard);
    } catch (error) {
      console.error("❌ 서브카테고리 처리 중 오류:", error.message);
      console.error("클릭된 요소:", e.target);

      // 사용자에게 알림
      alert("카테고리를 불러오는 중 오류가 발생했습니다.");
    }
  }
});

function showSubcategoryPosts(container, card) {
  // 모든 컨테이너 숨기기
  document
    .querySelectorAll(".posts-container, .category-section")
    .forEach((el) => {
      el.style.display = "none";
    });

  // 선택된 컨테이너 표시
  container.style.display = "block";

  // 제목 업데이트
  const subcatName =
    card.querySelector(".subcat-title")?.textContent || "알 수 없음";
  const titleElement = document.querySelector(".current-category h1");
  if (titleElement) {
    titleElement.textContent = `${subcatName} 포스트`;
  }

  console.log("✅ 포스트 컨테이너 표시 완료");
}

일반적인 문제들과 해결책

1. 데이터 속성 불일치 문제

문제: 카드의 data-parent="ML&AI"와 컨테이너의 data-parent="ML&AI"가 일치하지 않음

원인: HTML 엔티티 인코딩

해결책: Jekyll에서 일관된 슬러그 생성

Jekyll에서 일관된 슬러그 생성을 위한 개선된 방법:

  • 카테고리와 서브카테고리에 대해 slugify 필터를 사용
  • 데이터 속성에 일관된 슬러그 값 적용

검증 코드:

// 데이터 속성 불일치 검사
function validateDataAttributes() {
  const cards = document.querySelectorAll(".subcategory-card");
  const containers = document.querySelectorAll(
    ".posts-container[data-subcategory]"
  );

  const cardPairs = new Set();
  const containerPairs = new Set();

  cards.forEach((card) => {
    const pair = `${card.dataset.subcategory}|${card.dataset.parent}`;
    cardPairs.add(pair);
  });

  containers.forEach((container) => {
    const pair = `${container.dataset.subcategory}|${container.dataset.parent}`;
    containerPairs.add(pair);
  });

  // 매칭되지 않는 카드 찾기
  cardPairs.forEach((pair) => {
    if (!containerPairs.has(pair)) {
      console.warn("⚠️ 매칭되지 않는 카드:", pair);
    }
  });

  // 매칭되지 않는 컨테이너 찾기
  containerPairs.forEach((pair) => {
    if (!cardPairs.has(pair)) {
      console.warn("⚠️ 매칭되지 않는 컨테이너:", pair);
    }
  });
}

// 페이지 로드 후 검증 실행
document.addEventListener("DOMContentLoaded", validateDataAttributes);

2. Jekyll 빌드 시 Liquid 오류

문제: Liquid 템플릿에서 예상치 못한 에러 발생

해결책: 안전한 Liquid 코드 작성

Jekyll에서 안전한 속성 접근 방법:

  • 안전하지 않은 방법: 속성 존재 여부를 확인하지 않고 직접 접근
  • 안전한 방법: if 조건문으로 속성 존재 여부 확인 후 접근
  • 더 안전한 방법: default 필터를 사용하여 기본값 설정

3. JavaScript 선택자 문제

문제: 특수문자가 포함된 속성으로 요소 선택 실패

해결책: CSS.escape() 사용 또는 속성 선택자 개선

// 문제가 될 수 있는 코드
const category = "ML&AI";
const element = document.querySelector(`[data-category="${category}"]`); // 실패 가능

// 안전한 방법 1: CSS.escape 사용
const safeCategory = CSS.escape(category);
const element1 = document.querySelector(`[data-category="${safeCategory}"]`);

// 안전한 방법 2: 속성 선택자 직접 사용
const element2 = Array.from(document.querySelectorAll("[data-category]")).find(
  (el) => el.dataset.category === category
);

// 안전한 방법 3: 미리 슬러그 변환
function findElementBySlug(selector, slug) {
  return Array.from(document.querySelectorAll(selector)).find((el) => {
    const elementSlug = el.dataset.category || el.dataset.subcategory;
    return elementSlug === slug;
  });
}

4. 포스트 필터링 문제

문제: 서브카테고리별로 포스트가 올바르게 그룹화되지 않음

진단 코드:

// Jekyll 사이트 데이터 확인 (개발 모드에서)
function diagnosePosts() {
  console.group("📊 포스트 진단 정보");

  // 1. 전체 포스트 수 확인
  const allPosts = document.querySelectorAll(".post-item");
  console.log("전체 포스트 수:", allPosts.length);

  // 2. 카테고리별 포스트 분포
  const categoryCount = {};
  const subcategoryCount = {};

  allPosts.forEach((post) => {
    const container = post.closest("[data-parent]");
    if (container) {
      const parent = container.dataset.parent;
      const subcat = container.dataset.subcategory;

      categoryCount[parent] = (categoryCount[parent] || 0) + 1;
      subcategoryCount[`${parent}/${subcat}`] =
        (subcategoryCount[`${parent}/${subcat}`] || 0) + 1;
    }
  });

  console.log("카테고리별 포스트 수:", categoryCount);
  console.log("서브카테고리별 포스트 수:", subcategoryCount);

  console.groupEnd();
}

성능 문제 디버깅

DOM 조작 최적화

// 비효율적인 코드
function hideAllContainers() {
  document.querySelectorAll(".posts-container").forEach((container) => {
    container.style.display = "none"; // 각각 리플로우 발생
  });
}

// 효율적인 코드
function hideAllContainersOptimized() {
  const containers = document.querySelectorAll(".posts-container");

  // 한 번에 처리하기 위해 클래스 사용
  containers.forEach((container) => {
    container.classList.add("hidden");
  });
}
/* CSS에서 처리 */
.posts-container.hidden {
  display: none !important;
}

메모리 누수 방지

// 이벤트 리스너 정리
function setupCategorySystem() {
  const controller = new AbortController();

  document.addEventListener("click", handleCategoryClick, {
    signal: controller.signal,
  });

  // 페이지 언로드 시 정리
  window.addEventListener("beforeunload", () => {
    controller.abort();
  });
}

function handleCategoryClick(e) {
  // 이벤트 처리 로직
}

종합 디버깅 도구

모든 문제를 한 번에 확인할 수 있는 디버깅 도구를 만들어보겠습니다:

// 종합 디버깅 함수
function debugCategorySystem() {
  console.clear();
  console.log("🔍 Jekyll 카테고리 시스템 디버깅 시작");

  const results = {
    domElements: checkDOMElements(),
    dataAttributes: validateDataAttributes(),
    eventListeners: checkEventListeners(),
    performance: checkPerformance(),
  };

  // 결과 요약
  console.group("📋 디버깅 결과 요약");
  Object.entries(results).forEach(([category, result]) => {
    const icon = result.success ? "" : "";
    console.log(`${icon} ${category}:`, result.message);
  });
  console.groupEnd();

  return results;
}

function checkDOMElements() {
  const elements = {
    categoryItems: document.querySelectorAll(".category-item").length,
    subcategoryCards: document.querySelectorAll(".subcategory-card").length,
    postsContainers: document.querySelectorAll(".posts-container").length,
    categorySections: document.querySelectorAll(".category-section").length,
  };

  const hasElements = Object.values(elements).every((count) => count > 0);

  return {
    success: hasElements,
    message: hasElements ? "DOM 요소들이 정상적으로 생성됨" : "DOM 요소 누락",
    details: elements,
  };
}

function validateDataAttributes() {
  const cards = document.querySelectorAll(".subcategory-card");
  const containers = document.querySelectorAll(
    ".posts-container[data-subcategory]"
  );

  const mismatches = [];

  cards.forEach((card) => {
    const subcategory = card.dataset.subcategory;
    const parent = card.dataset.parent;

    const matchingContainer = document.querySelector(
      `[data-subcategory="${subcategory}"][data-parent="${parent}"].posts-container`
    );

    if (!matchingContainer) {
      mismatches.push(`${subcategory} (${parent})`);
    }
  });

  return {
    success: mismatches.length === 0,
    message:
      mismatches.length === 0
        ? "모든 카드-컨테이너 매칭 성공"
        : `${mismatches.length}개 매칭 실패`,
    details: mismatches,
  };
}

function checkEventListeners() {
  // 이벤트 리스너가 정상적으로 등록되었는지 확인
  const testCard = document.querySelector(".subcategory-card");
  const testCategory = document.querySelector(".category-item");

  const hasClickHandlers = testCard && testCategory;

  return {
    success: hasClickHandlers,
    message: hasClickHandlers
      ? "이벤트 리스너 정상 등록"
      : "이벤트 리스너 등록 실패",
    details: {
      subcategoryCards: !!testCard,
      categoryItems: !!testCategory,
    },
  };
}

function checkPerformance() {
  const startTime = performance.now();

  // DOM 쿼리 성능 테스트
  document.querySelectorAll(".subcategory-card");
  document.querySelectorAll(".posts-container");

  const endTime = performance.now();
  const queryTime = endTime - startTime;

  return {
    success: queryTime < 10, // 10ms 이하면 양호
    message: `DOM 쿼리 시간: ${queryTime.toFixed(2)}ms`,
    details: { queryTime },
  };
}

// 자동 실행 (개발 환경에서만)
if (
  window.location.hostname === "localhost" ||
  window.location.hostname === "127.0.0.1"
) {
  document.addEventListener("DOMContentLoaded", () => {
    setTimeout(debugCategorySystem, 1000); // 1초 후 실행
  });
}

실시간 오류 모니터링

사용자가 실제로 사용할 때 발생하는 오류를 캐치하고 기록하는 시스템:

// 전역 오류 처리기
window.addEventListener("error", function (e) {
  console.error("🚨 JavaScript 오류 발생:", {
    message: e.message,
    filename: e.filename,
    line: e.lineno,
    column: e.colno,
    stack: e.error?.stack,
  });

  // 개발 환경에서는 상세 정보 표시
  if (isDevelopment()) {
    showErrorNotification(e);
  }
});

window.addEventListener("unhandledrejection", function (e) {
  console.error("🚨 Promise 오류:", e.reason);

  if (isDevelopment()) {
    showErrorNotification(e);
  }
});

function isDevelopment() {
  return (
    window.location.hostname === "localhost" ||
    window.location.hostname === "127.0.0.1" ||
    window.location.search.includes("debug=true")
  );
}

function showErrorNotification(error) {
  const notification = document.createElement("div");
  notification.className = "debug-notification error";
  notification.innerHTML = `
        <h4>🚨 개발 오류 알림</h4>
        <p><strong>메시지:</strong> ${error.message || error.reason}</p>
        <button onclick="this.parentElement.remove()">닫기</button>
    `;

  document.body.appendChild(notification);

  // 5초 후 자동 제거
  setTimeout(() => {
    if (notification.parentElement) {
      notification.remove();
    }
  }, 5000);
}

Jekyll 개발 환경 최적화

로컬 개발 시 실시간 피드백

// 개발 모드에서만 활성화되는 도구들
if (isDevelopment()) {
  // 1. CSS 변경 감지 (Hot Reload)
  let lastStylesheetModified = Date.now();

  setInterval(() => {
    const stylesheets = document.querySelectorAll('link[rel="stylesheet"]');
    stylesheets.forEach((link) => {
      const href = link.href;
      const newHref = href.split("?")[0] + "?v=" + Date.now();

      fetch(href, { method: "HEAD" }).then((response) => {
        const lastModified = response.headers.get("last-modified");
        if (
          lastModified &&
          new Date(lastModified) > new Date(lastStylesheetModified)
        ) {
          link.href = newHref;
          lastStylesheetModified = Date.now();
          console.log("🎨 스타일시트 업데이트됨");
        }
      });
    });
  }, 2000);

  // 2. 개발자 도구 단축키
  document.addEventListener("keydown", function (e) {
    // Ctrl/Cmd + Shift + D: 디버깅 실행
    if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "D") {
      e.preventDefault();
      debugCategorySystem();
    }

    // Ctrl/Cmd + Shift + R: 강제 새로고침
    if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "R") {
      e.preventDefault();
      location.reload(true);
    }
  });

  // 3. 개발자 패널 추가
  createDeveloperPanel();
}

function createDeveloperPanel() {
  const panel = document.createElement("div");
  panel.id = "developer-panel";
  panel.innerHTML = `
        <div class="dev-panel-header">
            <h3>🛠️ 개발자 도구</h3>
            <button onclick="toggleDevPanel()">최소화</button>
        </div>
        <div class="dev-panel-content">
            <button onclick="debugCategorySystem()">시스템 진단</button>
            <button onclick="showDataAttributes()">데이터 속성 보기</button>
            <button onclick="validateHTML()">HTML 검증</button>
            <button onclick="clearConsole()">콘솔 정리</button>
        </div>
    `;

  document.body.appendChild(panel);
}

function showDataAttributes() {
  console.group("📊 모든 데이터 속성");

  document
    .querySelectorAll("[data-category], [data-subcategory], [data-parent]")
    .forEach((el) => {
      const data = {
        element:
          el.tagName.toLowerCase() +
          (el.className ? "." + el.className.split(" ").join(".") : ""),
        category: el.dataset.category,
        subcategory: el.dataset.subcategory,
        parent: el.dataset.parent,
      };

      console.log(data);
    });

  console.groupEnd();
}

function validateHTML() {
  console.group("🔍 HTML 구조 검증");

  // 필수 요소 확인
  const requiredElements = [
    ".category-item",
    ".subcategory-card",
    ".posts-container",
    ".current-category",
  ];

  requiredElements.forEach((selector) => {
    const elements = document.querySelectorAll(selector);
    const status = elements.length > 0 ? "" : "";
    console.log(`${status} ${selector}: ${elements.length}개`);
  });

  // 중복 ID 확인
  const ids = Array.from(document.querySelectorAll("[id]")).map((el) => el.id);
  const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index);

  if (duplicateIds.length > 0) {
    console.warn("⚠️ 중복 ID 발견:", duplicateIds);
  } else {
    console.log("✅ 중복 ID 없음");
  }

  console.groupEnd();
}

프로덕션 환경 모니터링

배포된 블로그에서 발생하는 문제를 추적하기 위한 간단한 로깅 시스템:

// 프로덕션에서 사용할 간단한 오류 수집
if (!isDevelopment()) {
  const errorLog = [];

  window.addEventListener("error", function (e) {
    errorLog.push({
      type: "javascript-error",
      message: e.message,
      timestamp: new Date().toISOString(),
      url: window.location.href,
      userAgent: navigator.userAgent,
    });

    // 로컬 스토리지에 저장 (선택적)
    if (errorLog.length <= 10) {
      // 최대 10개만 보관
      localStorage.setItem("error-log", JSON.stringify(errorLog));
    }
  });

  // 사용자 액션 추적
  document.addEventListener("click", function (e) {
    if (e.target.closest(".subcategory-card, .category-item")) {
      console.log("사용자 액션:", {
        element: e.target.tagName,
        timestamp: new Date().toISOString(),
      });
    }
  });
}

마무리

디버깅은 개발 과정에서 가장 중요한 스킬 중 하나입니다. 체계적인 접근 방식으로 문제를 해결하면:

  1. 문제 발생 시 당황하지 않고
  2. 단계별로 원인을 찾고
  3. 재발 방지를 위한 개선책을 마련할 수 있습니다

다음 포스트에서는 프라이빗/퍼블릭 레포지토리 분리 전략을 알아보겠습니다. 소스 코드는 비공개로 유지하면서 블로그는 안전하게 배포하는 방법을 다룰 예정입니다.

디버깅 과정에서 새로운 문제를 발견하시거나 더 효과적인 해결 방법이 있다면 댓글로 공유해 주세요!


참고 자료: