단일 HTML 파일로 JSON diff 도구를 만들었습니다 (빌드 단계 없이)

발행: (2026년 1월 7일 오후 12:49 GMT+9)
4 min read
원문: Dev.to

Source: Dev.to

Overview

JSON 페이로드를 나란히 비교해야 할 일이 계속 생겼습니다 (API 응답 vs 기대값, 이전 vs 이후, 프로덕션 vs 스테이징). 매번 무작위 온라인 도구에 붙여넣고, 눈으로 살펴보고, 몇 분 뒤에 다시 같은 과정을 반복했습니다.

이를 해결하기 위해 fknjsn.com을 만들었습니다 – 빌드 단계가 없는 단일 HTML 파일에 존재하는 로컬‑우선 JSON 비교 도구입니다.

  • CDN을 통한 Vue 3 (전역 빌드)
  • CDN을 통한 vue-json-pretty 로 트리 렌더링
  • 영속성을 위한 localStorage

그게 전부 – webpack, Vite, node_modules, package.json 없이도 됩니다. 단 하나의 index.html만 있으면 되고, 심지어 누군가에게 이메일로 보낼 수도 있습니다.

Setup

전체 앱은 두 개의 <script> 태그와 Vue 전역 내보내기의 구조 분해 할당으로 부트스트랩됩니다:

const { createApp, ref, computed, watch, onMounted, nextTick } = Vue;

// ... the rest is just Vue

전역 빌드는 최신 트렌드는 아니지만 동작합니다. 번들러도, import map도 없이 – 2014년처럼 <script> 태그만 사용하면 되면서도 반응형 프레임워크를 얻을 수 있습니다.

Paste‑anywhere UX

앱은 전역에서 paste 이벤트를 청취하지만, 입력창이나 텍스트 영역이 포커스된 경우에는 무시합니다. 이를 통해 페이지 어디서든 JSON을 붙여넣을 수 있고, 선택된 행에 자동으로 들어갑니다.

window.addEventListener('paste', (e) => {
  const tag = document.activeElement?.tagName?.toLowerCase()
  if (tag === 'input' || tag === 'textarea') return

  try {
    const json = JSON.parse(e.clipboardData.getData('text'))
    addJsonToSelectedRow(json)
  } catch {
    // not valid JSON, ignore
  }
})

각 JSON 블록마다 자체 검색 입력란이 있습니다. 필터는 트리를 순회하면서, 하위 항목 중 하나라도 매치되면 해당 부모 노드를 유지합니다. 개념적인 구현은 다음과 같습니다:

function filterJson(obj, search) {
  if (!search) return obj
  const lower = search.toLowerCase()

  if (Array.isArray(obj)) {
    const filtered = obj
      .map(item => filterJson(item, search))
      .filter(item => item !== undefined)
    return filtered.length ? filtered : undefined
  }

  if (obj && typeof obj === 'object') {
    const result = {}
    for (const [key, value] of Object.entries(obj)) {
      if (key.toLowerCase().includes(lower)) {
        result[key] = value
      } else {
        const filtered = filterJson(value, search)
        if (filtered !== undefined) result[key] = filtered
      }
    }
    return Object.keys(result).length ? result : undefined
  }

  // primitives
  const str = String(obj).toLowerCase()
  return str.includes(lower) ? obj : undefined
}

Debounced Persistence

상태는 localStorage에 저장되지만, 급격한 변경이 있을 때 저장을 과도하게 하지 않도록 500 ms 디바운스로 처리합니다:

watch(rows, () => {
  clearTimeout(saveTimeout)
  saveTimeout = setTimeout(() => {
    localStorage.setItem('json-rows', JSON.stringify(state))
  }, 500)
}, { deep: true })

Scaling Ideas

도구가 더 큰 JSON 페이로드나 추가 기능을 다뤄야 한다면 다음을 고려할 것입니다:

  • 큰 페이로드 필터링을 위한 Web Worker 추가
  • 트리 뷰를 위한 virtual scrolling 사용
  • 단순 나란히 비교가 아니라 실제 diff view 구현

현재는 제가 필요로 하는 기능을 정확히 수행하고, 전체가 제 머리 속에 편안히 들어갑니다.

  • Live demo: https://fknjsn.com
  • Source: view‑source of the page (the entire app is right there)

The name is pronounced exactly as you think it is.

Back to Blog

관련 글

더 보기 »

React 컴포넌트에서 TypeScript Generics

소개 제네릭은 React 컴포넌트에서 매일 사용하는 것은 아니지만, 특정 경우에는 유연하고 타입‑안전한 컴포넌트를 작성할 수 있게 해줍니다.

React에서 간단한 Carousel/Slider 만들기

캐러셀 또는 슬라이더는 이미지나 콘텐츠를 하나씩 표시하는 훌륭한 방법입니다. 버튼을 사용하여 이를 탐색할 수 있습니다. 아래는 간단한 구현...