json-tool 5년과 3,000명의 활성 사용자 이후

발행: (2026년 2월 4일 오전 04:54 GMT+9)
14 min read
원문: Dev.to

Source: Dev.to

3천 명의 사용자?

주간 활성 사용자 수는 Snapcraft 채널 배포에서 가져옵니다. “3k”는 그곳에서 나온 것이지만, 웹 버전도 기여합니다.

문제

2021년에 나는 JSON을 자주 다루는 작업 흐름을 가지고 있었다: API 응답 디버깅, 서드‑파티 서비스와의 통합, 데이터 구조 검사 등. 많은 개발자들처럼 나는 “JSON prettier”를 구글에 검색하고 가장 먼저 나타나는 웹사이트를 사용하곤 했다. JSON FormatterJSON Pretty Print 같은 도구는 잘 작동했지만, 다음과 같은 대가가 따랐다:

  • 어디에나 광고가 존재함
  • 데이터 처리에 대한 투명성 부족
  • 내 JSON 문자열이 어디선가 기록되고 있지 않을 것이라는 보장 전무

개인 프로젝트에서는 약간 번거로울 뿐이었지만, 프로덕션 데이터나 민감한 정보를 다루는 작업에서는 책임감이 없다고 느껴졌다. ThoughtWorks Tech Radar 27호는 바로 이 문제를 강조하며, 데이터 관할권 요구사항을 충족하지 못하는 포맷팅 도구에 대해 개발자들에게 경고했다.

나는 다른 무언가를 원했다: 데이터를 전혀 수집하지 않고, 광고가 없으며, 오픈‑소스 코드로 투명성을 유지하는 도구. 복잡한 기능 세트는 필요 없었다—사용자 프라이버시를 존중하는 신뢰할 수 있는 JSON 포맷팅만 있으면 충분했다. 그렇게 json‑tool이 탄생했다.

첫 번째 구현

초기 구현은 간단했습니다:

  • React 로 구축된 Electron
  • snapcraft.io 에 게시
  • CypressReact Testing Library 를 사용한 외부‑인 TDD (핵심 기능이 처음부터 신뢰성 있게 작동하도록 제가 작성한 실천법)

첫 번째 버전은 일반적인 사용 사례에 잘 작동했습니다. 개발자는 다음을 할 수 있었습니다:

  1. 로그에서 JSON 붙여넣기
  2. 검증하기
  3. 사용자 정의 간격으로 포맷하기
  4. 결과를 클립보드에 복사하기

프라이버시 우선 접근 방식 덕분에 민감한 데이터를 다루는 기업 개발자들이 기업 방화벽에 대한 우려 없이 사용할 수 있었으며, 가장 중요한 점은 Snapcraft를 통해 설치된 후 오프라인에서도 실행된다는 것입니다.

Source:

프리즈

그때 현실이 다가왔습니다: 사용자가 1 MB가 넘는 JSON 문자열을 붙여넣기 시작했습니다.

앱이 완전히 멈췄습니다—UI가 응답하지 않고, 버튼이 작동을 멈추며 전체 경험이 무너졌습니다.

console.time() / console.timeEnd() 로 벤치마킹한 결과, 병목 현상이 메인 스레드에서 발생하는 JSON 파싱 및 포맷팅 때문이라는 것을 확인했습니다. JavaScript의 단일 스레드 특성과 이벤트 루프 실행 모델 때문에, 차단되는 작업은 UI 전체를 정지시킵니다.

웹 워커 도입

Ido Green의 책 “Web Workers: Multithreaded Programs in JavaScript” 에 따르면, 웹 애플리케이션에서 150 ms 이상 걸리는 작업을 수행해야 할 경우 웹 워커 사용을 고려해야 합니다. Green은 특히 **“큰 문자열의 인코딩/디코딩”**을 주요 사용 사례로 꼽고 있는데, 바로 json‑tool이 수행하던 작업과 일치합니다.

Architecture Shift

Before (synchronous)

const onJsonChange = useCallback(async (value: string) => {
  setError('');
  if (!spacing) return;

  try {
    if (value) {
      JSON.parse(value);
    }
  } catch (e: any) {
    setError('invalid json');
  }

  let format = new Formatter(value, 2);
  const parseSpacing = parseInt(spacing);
  if (!isNaN(parseSpacing)) {
    format = new Formatter(value, parseSpacing);
  }

  const result = await format.format();
  setOriginalResult(value);
  setResult(result);
}, [spacing]);

After (event‑driven, off‑loading to a worker)

// UI thread
const onChange = (eventValue: string, eventSpacing: string) => {
  if (worker.current) {
    worker.current.postMessage({
      jsonAsString: eventValue,
      spacing: eventSpacing,
    });
  }
  setOriginalResult(eventValue);
  setInProgress(true);
};
// worker.js
importScripts('https://unpkg.com/format-to-json@2.1.2/fmt2json.min.js');

if (typeof importScripts === 'function') {
  addEventListener('message', async (event) => {
    const { jsonAsString: value, spacing } = event.data;

    if (value) {
      const format = await fmt2json(value, {
        expand: true,
        escape: false,
        indent: parseInt(spacing),
      });

      try {
        JSON.parse(value);
      } catch (e) {
        postMessage({
          error: true,
          originalJson: value,
          result: format.result,
        });
        return;
      }

      postMessage({
        error: false,
        originalJson: value,
        result: format.result,
      });
    }
  });
}

Did this improve raw computation time? No—the actual processing time remained the same. But the user experience was transformed. The UI stays responsive, and users can continue interacting while the worker does the heavy lifting.

요점

  • Web Workers는 CPU 집약적인 작업(예: 대용량 JSON 문자열 파싱/포맷팅)을 오프로드하는 데 매우 유용합니다.
  • 알고리즘 실행 시간이 변하지 않더라도, 작업을 메인 스레드에서 분리하면 UI 정지를 방지할 수 있습니다.
  • 프라이버시 우선, 오프라인 우선 접근 방식은 특히 기업 환경에서 상당한 사용자 기반을 끌어들일 수 있습니다.
  • 사용 패턴이 변화함에 따라 지속적인 성능 모니터링(벤치마크, 프로파일링)이 필수적입니다.

참고문헌

  • Green, Ido. Web Workers: Multithreaded Programs in JavaScript.
  • ThoughtWorks Tech Radar, Volume 27.

전용 워커가 있는 이벤트‑드리븐 시스템

const onChange = (eventValue: string, eventSpacing: string) => {
  if (worker.current) {
    worker.current.postMessage({
      jsonAsString: eventValue,
      spacing: eventSpacing,
    });
  }
  setOriginalResult(eventValue);
  setInProgress(true);
};

워커가 파싱 및 포맷팅을 처리합니다

importScripts('https://unpkg.com/format-to-json@2.1.2/fmt2json.min.js');

if (typeof importScripts === 'function') {
  addEventListener('message', async (event) => {
    const { jsonAsString: value, spacing } = event.data;

    if (!value) return;

    const format = await fmt2json(value, {
      expand: true,
      escape: false,
      indent: parseInt(spacing, 10),
    });

    try {
      JSON.parse(value);
    } catch (e) {
      postMessage({
        error: true,
        originalJson: value,
        result: format.result,
      });
      return;
    }

    postMessage({
      error: false,
      originalJson: value,
      result: format.result,
    });
  });
}

성능이 향상되었나요?
아니요. 실제 계산 시간은 동일했지만 사용자 경험은 크게 변했습니다.

웹 워커가 구원한다

아키텍처 변경 및 성능 영향 탐구.

TDD 규율 유지

  • outside‑in TDD로 시작했고 타협을 거부했습니다.
  • 워커를 도입하면서 테스트 복잡도가 증가했습니다: Worker API는 브라우저에 존재하지만 Jest의 jsdom 환경에서는 정의되지 않아 테스트 스위트가 깨집니다.

해결책: jsdom-worker

  1. 설치

    npm i jsdom-worker
  2. setupTests.ts에 임포트

    import 'jsdom-worker';

이 라이브러리는 jsdom용 Worker API를 모킹합니다. 실제 스레드를 생성하지 않으며 제한 사항이 있습니다(공유 워커 없음, Blob 패턴 사용 필요).

예시 테스트 (행위 기반)

it('should format json from uploaded file', async () => {
  const file = new File(['{"a":"b"}'], 'hello.json', {
    type: 'application/json',
  });

  const { getByTestId } = render();

  await act(async () => {
    await userEvent.upload(getByTestId('upload-json'), file);
  });

  await waitFor(() => {
    expect(getByTestId('raw-result')).toHaveValue(`{
  "a": "b"
}`);
  });
});
  • 테스트는 무엇이 일어나야 하는지를 설명하고, 어떻게 일어나는지는 설명하지 않습니다.
  • 워커 사용 여부와 관계없이 업로드된 JSON이 올바르게 포맷되는지를 검증합니다.
  • 교훈: 웹 워커는 구현 세부 사항이며, 테스트는 사용자에게 보여지는 동작에 초점을 맞춰야 합니다.

라이브 코딩 세션

  • 동기 코드를 워커를 사용하도록 리팩터링하면서 테스트 커버리지를 유지했습니다.
  • 코딩 도장 환경에서 TDD를 시연했습니다.

워커 구현의 트레이드오프

  • 장점: UI 멈춤 현상이 사라짐.
  • 단점: 이벤트 기반 아키텍처 도입으로 다음에 대한 이해가 필요함:
    • 워커 수명 주기 관리
    • 메시지 전달
    • 메인 스레드 외 실행

완화 방안: 워커 로직을 격리하고, 나머지 앱은 단순하게 유지 (React 컴포넌트, 직관적인 상태 관리, 최소한의 추상화). 문서에 필요한 세부 사항이 포함되어 있음.

Open‑Source Maintenance Realities

  • Dependencies need regular updates. → 의존성은 정기적인 업데이트가 필요합니다.
  • Security vulnerabilities must be patched. → 보안 취약점은 반드시 패치해야 합니다.
  • Browser APIs evolve; testing libraries change. → 브라우저 API가 진화하고 테스트 라이브러리가 변경됩니다.

Sustainability Over Rapid Development

  • Tech stack: React, TypeScript, Tailwind, CodeMirror – all stable, well‑maintained projects with large communities. → Tech stack: React, TypeScript, Tailwind, CodeMirror – 모두 안정적이고, 커뮤니티가 큰 잘 관리된 프로젝트입니다.
  • Avoid cutting‑edge dependencies that might be abandoned. → 버려질 수 있는 최신 의존성을 피하십시오.

Update cadence: Batches of updates occur when I’m actively using the tool and notice a need. This is a community‑driven project, not a business with SLA guarantees. → Update cadence: 업데이트 배치는 제가 도구를 적극적으로 사용하고 필요성을 느낄 때 이루어집니다. 이는 SLA 보장이 있는 비즈니스가 아니라 커뮤니티 주도 프로젝트입니다.

오픈 소스에 대한 교훈

  1. 투명성은 신뢰를 구축한다 – 사용자는 코드를 감사하고 추적, 데이터 유출, 광고가 없음을 확인할 수 있다.
  2. 집중된 도구가 문제를 더 잘 해결한다json‑tool은 한 가지 일을 잘한다: 프라이버시 보장을 갖춘 JSON 포맷팅.
  3. 테스트 주도 개발은 큰 이익을 가져온다 – 초기 TDD 투자는 수년 후 작업자에게 안전한 리팩터링을 가능하게 했다.

미래 로드맵 (지속적인 개선)

  • Performance: 100 MB가 넘는 초대형 JSON 파일 처리 최적화.
  • Accessibility: 스크린 리더 지원 추가 및 키보드 탐색 개선.
  • Editor sync: 두 패널이 동시에 스크롤되도록 유지.
  • Dependencies: 최신 CodeMirror 버전으로 업그레이드하고, React와 TypeScript를 최신 상태로 유지.

수익화, 분석 도구 추가, 핵심 목적을 넘어선 확장은 계획에 없습니다. GitHub Sponsors를 통한 후원을 환영합니다.

마무리 생각

이 여정에서 가장 보람 있었던 부분은 3,000명의 사용자를 달성한 것이 아니라, 실제 문제를 해결하는 무언가를 단순함, 신뢰성, 그리고 프라이버시를 핵심으로 구축한 것이었습니다.

원칙을 타협하지 않으며. 추적 없음. 광고 없음. 데이터 수집 없음.
그저 약속을 지키고 사용자들을 존중하는 도구.

json‑tool의 소스 코드는 여전히 저장소에서 제공되며, 도구 자체는 Snapcraft 스토어에서 다운로드할 수 있습니다. 5년이 흘렀고—프라이버시를 존중하고 개발자에게 훌륭히 봉사하는 소프트웨어를 앞으로도 오랫동안 만들 수 있기를 바랍니다.

Back to Blog

관련 글

더 보기 »

크라우드펀딩 플랫폼

GitHub Copilot CLI 챌린지 제출 이것은 GitHub Copilot CLI 챌린지에 대한 제출입니다 https://dev.to/challenges/github-copilot. Repository bash git...