HTML, JavaScript, CSS를 단일 파일로 결합한 ‘HTML 도구’를 만들 때 생산성을 높여주는 패턴들을 실제 사례와 함께 정리했다.
URL: https://simonwillison.net/2025/Dec/10/html-tools/
2025년 12월 10일
저는 최근 HTML 도구(HTML tools) 라는 표현을 쓰기 시작했습니다. 이는 제가 만들어 온 HTML 애플리케이션을 가리키는데, 하나의 파일 안에 HTML, JavaScript, CSS를 함께 넣고 유용한 기능을 제공하는 형태입니다. 지난 2년 동안 이런 것들을 150개 이상 만들었고, 그중 거의 전부는 LLM이 작성했습니다. 이 글에서는 그 과정에서 발견한 유용한 패턴들을 모아 소개합니다.
먼저 제가 말하는 것이 어떤 것인지 보여주는 예시 몇 가지부터:
이것들은 최근 특히 마음에 드는 것들입니다. 이와 비슷한 도구가 수십 개 더 있고, 저는 이를 नियमित적으로 사용합니다.
제 컬렉션은 tools.simonwillison.net 에서 둘러볼 수 있습니다. 전체를 훑어보려면 월별(by month) 뷰가 유용합니다.
코드와 프롬프트를 보고 싶다면, 이 글의 예시 대부분은 푸터에 GitHub의 “view source” 링크가 있습니다. GitHub 커밋에는 보통 프롬프트 자체 또는 도구를 만드는 데 사용한 트랜스크립트 링크가 들어 있습니다.
제가 이런 유형의 도구를 만들 때 가장 생산적이라고 느낀 특징은 다음과 같습니다.
결과적으로 깔끔하게 GitHub 저장소에 복사-붙여넣기 할 수 있는 수백 줄의 코드가 만들어집니다.
이런 도구를 만드는 가장 쉬운 방법은 ChatGPT, Claude, Gemini에서 시작하는 것입니다. 세 가지 모두 간단한 HTML+JavaScript 애플리케이션을 작성하고 바로 보여주는 기능을 제공합니다.
Claude에서는 이를 “Artifacts”라고 부르고, ChatGPT와 Gemini에서는 “Canvas”라고 부릅니다. Claude는 기본적으로 활성화되어 있고, ChatGPT와 Gemini는 “tools” 메뉴에서 토글을 켜야 할 수도 있습니다.
Gemini 또는 ChatGPT에서 다음 프롬프트를 시도해 보세요:
Build a canvas that lets me paste in JSON and converts it to YAML. No React.
Claude에서는 다음 프롬프트를 사용하세요:
Build an artifact that lets me paste in JSON and converts it to YAML. No React.
저는 항상 프롬프트에 “No React”를 덧붙입니다. 그렇지 않으면 React로 만들어 버리는 경향이 있고, 그러면 LLM에서 코드를 복사해 다른 곳에 쓰기가 더 어려운 파일이 됩니다. React를 사용하는 시도는 (빌드 단계를 실행해야 해서) 표시되기까지 더 오래 걸리고, 특히 ChatGPT에서는 이유는 모르겠지만 크래시 버그를 더 자주 포함하는 것 같습니다.
세 도구 모두 완성된 애플리케이션으로 연결되는 “share” 링크(즉 URL)를 제공합니다. 예시:
Claude Code나 Codex CLI 같은 코딩 에이전트는 Playwright 같은 도구로 작업 중에 스스로 코드를 테스트할 수 있다는 장점이 있습니다. 위에서 보여준 Bluesky 스레드 뷰어처럼 더 복잡한 것을 만들 때 저는 종종 이런 도구로 업그레이드합니다.
또한 기존 도구를 수정할 때는 Claude Code for web 같은 비동기 코딩 에이전트도 자주 사용합니다. 이에 대해서는 Building a tool to copy-paste share terminal sessions using Claude Code for web에서 영상을 공유했습니다.
Claude Code for web과 Codex Cloud는 제 simonw/tools 저장소에 직접 붙어서 동작합니다. 그래서 제가 직접 복사-붙여넣기 하지 않아도 Pull Request로 도구를 배포하거나 업그레이드할 수 있습니다(여기 수십 개 예시).
도구의 일부로 추가 JavaScript 라이브러리를 사용할 때마다 저는 CDN에서 로드하는 것을 선호합니다.
세 주요 LLM 플랫폼은 Artifacts/Canvas 기능에서 특정 CDN을 허용 목록(allow-list) 형태로 지원합니다. 그래서 “Use PDF.js” 같은 식으로 말하면, 허용 목록에 있는 CDN으로의 URL을 알아서 구성해 주는 경우가 많습니다.
가끔은 cdnjs나 jsDelivr에서 URL을 찾아 채팅에 붙여넣어야 할 때도 있습니다.
이런 CDN들은 충분히 오랫동안 존재해 왔기 때문에, 특히 패키지 버전이 포함된 URL의 경우 저는 신뢰하게 되었습니다.
CDN의 대안은 npm을 사용하고 프로젝트에 빌드 단계를 두는 것입니다. 하지만 저는 이렇게 하면 개별 도구를 빠르게 해킹하는 생산성이 떨어지고, 셀프 호스팅도 더 어려워진다고 느낍니다.
저는 LLM 플랫폼 자체에 HTML 도구를 그대로 호스팅해 두는 것을 좋아하지 않습니다. 몇 가지 이유가 있습니다.
첫째, LLM 플랫폼은 대개 도구를 강한 샌드박스에서 실행하며 제한이 많습니다. 외부 URL에서 데이터나 이미지를 로드하지 못하는 경우가 많고, 때로는 다른 사이트로의 링크 기능조차 비활성화되어 있습니다.
최종 사용자 경험도 종종 좋지 않습니다. 새 사용자에게 경고 메시지를 보여주고, 로딩 시간이 더 걸리며, 도구를 만든 플랫폼의 홍보를 열심히 띄우기도 합니다.
또한 다른 정적 호스팅 방식만큼 신뢰성이 높지 않습니다. ChatGPT나 Claude가 장애 상태일 때도 저는 과거에 만든 도구에 접근하고 싶습니다.
쉽게 셀프 호스팅할 수 있다는 점이 제가 “no React”를 고집하고 의존성은 CDN을 쓰는 주된 이유입니다. 빌드 단계가 없으면, 다른 제공자에 옮겨 호스팅하는 것이 단순히 코드를 복사해 붙여넣는 일로 끝납니다.
이때 제가 선호하는 제공자는 GitHub Pages입니다. github.com에서 HTML 블록을 파일로 붙여넣기만 하면, 몇 초 뒤에 영구 URL로 호스팅됩니다. 제 도구 대부분은 simonw/tools 저장소에 들어가며, 이는 tools.simonwillison.net에서 정적 파일을 제공하도록 설정돼 있습니다.
HTML 도구에서 가장 유용한 입출력 메커니즘 중 하나는 복사와 붙여넣기 형태로 나타납니다.
저는 붙여넣은 콘텐츠를 받아 어떤 방식으로든 변환한 뒤, 사용자가 다시 클립보드로 복사해 다른 곳에 붙여넣을 수 있게 하는 도구를 자주 만듭니다.
휴대폰에서 복사-붙여넣기는 번거롭기 때문에, 한 번의 터치로 클립보드를 채워주는 “클립보드로 복사” 버튼을 자주 넣습니다.
대부분의 운영체제 클립보드는 같은 데이터의 여러 포맷을 함께 담을 수 있습니다. 그래서 워드프로세서에서 복사한 내용을 붙여넣을 때 서식이 유지되지만, 텍스트 편집기에 붙여넣으면 서식이 제거된 텍스트만 들어오는 것입니다.
이런 풍부한(rich) 복사 동작은 JavaScript의 paste 이벤트에서도 사용할 수 있어, HTML 도구에 다양한 기회를 열어줍니다.
흥미로운 HTML 도구를 만들기 위한 핵심은 ‘무엇이 가능한지’를 이해하는 것입니다. 커스텀 디버깅 도구를 만드는 것은 이런 가능성을 탐색하는 훌륭한 방법입니다.
clipboard-viewer 는 제가 가장 유용하게 쓰는 도구 중 하나입니다. 텍스트, 리치 텍스트, 이미지, 파일 등 무엇이든 붙여넣을 수 있고, 클립보드에 들어 있는 모든 종류의 붙여넣기 데이터를 순회하며 보여줍니다.

이 도구는 다른 많은 도구를 만드는 데 핵심이었습니다. 클립보드에서 사용할 수 있는 ‘보이지 않는 데이터’를 보여줬고, 그걸 기반으로 다른 흥미로운 기능을 부트스트랩할 수 있었기 때문입니다.
다른 디버깅 예시:
KeyCode 값)를 보여줍니다.HTML 도구는 저장을 위한 서버 사이드 데이터베이스에 접근할 수 없을 수도 있지만, 놀랍게도 URL 안에 아주 많은 상태를 저장할 수 있습니다.
저는 북마크하거나 다른 사람과 공유하고 싶을 수 있는 도구에 이 방식을 좋아합니다.
브라우저 API인 localStorage는 서버에 노출하지 않고도 사용자의 기기에서 데이터를 영속적으로 저장할 수 있게 해줍니다.
저는 URL에 담기엔 큰 상태(state)나, 서버 가까이에 두고 싶지 않은 API 키 같은 비밀값에 이를 사용합니다. 정적 호스트라 하더라도 제 영향권 밖의 서버 로그가 존재할 수 있기 때문입니다.
prompt() 함수로 사용자에게 API 키를 요청한 다음 localStorage에 저장합니다. 이 도구는 Claude Haiku를 사용해 사용자의 웹캠을 통해 보이는 것에 대한 하이쿠를 씁니다.CORS는 Cross-origin resource sharing의 약자입니다. 한 사이트에서 실행 중인 JavaScript가 다른 도메인에 호스팅된 API에서 데이터를 가져올 수 있는지 제어하는 비교적 로우레벨 디테일입니다.
CORS 헤더를 개방해 둔 API는 HTML 도구에게는 금광입니다. 시간이 지나며 이런 API를 모아두는 것은 충분히 가치가 있습니다.
제가 좋아하는 것들:
GitHub Gist는 특히 좋아합니다. 크로스 오리진 API 호출로 영구적인 Gist에 상태를 저장하는 앱을 만들 수 있기 때문입니다.
.whl 파일을 가져와(브라우저 메모리에서) 압축을 풀고 파일을 탐색할 수 있게 해줍니다.OpenAI, Anthropic, Gemini 세 곳 모두 HTML 도구에서 CORS로 직접 접근할 수 있는 JSON API를 제공합니다.
하지만 여전히 API 키가 필요하고, 눈에 보이는 HTML에 그 키를 박아 넣으면 누구나 훔쳐서 여러분 계정으로 비용을 발생시킬 수 있습니다.
저는 앞서 말한 localStorage 비밀값 패턴을 사용해 API 키를 저장합니다. 사용자 경험 측면에서는 좋지 않습니다. 사용자에게 API 키를 만들고 도구에 붙여넣으라고 하는 건 마찰이 크지만, 작동하긴 합니다.
예시:
<input type="file"> 요소를 유용하게 쓰기 위해 파일을 서버에 업로드할 필요는 없습니다. JavaScript는 그 파일의 내용을 직접 접근할 수 있고, 이는 유용한 기능을 위한 엄청난 가능성을 열어줍니다.
예시:
PDF.js와 Tesseract.js를 사용해 사용자가 브라우저에서 PDF를 열면 페이지별 이미지로 변환한 다음 OCR을 수행합니다.ffmpeg 명령을 복사해 갈 수 있습니다.HTML 도구는 서버 도움 없이도 다운로드용 파일을 생성할 수 있습니다.
JavaScript 라이브러리 생태계에는 다양한 유용한 포맷의 파일을 생성하는 패키지가 엄청나게 많습니다.
Pyodide는 WebAssembly로 컴파일된 파이썬 배포판으로, 브라우저에서 직접 실행되도록 설계되었습니다. 이는 공학적 경이로움이며, 파이썬 세계에서 가장 과소평가된 구석 중 하나입니다.
게다가 CDN에서 깔끔하게 로드할 수 있으니, HTML 도구에서 Pyodide를 쓰지 않을 이유가 없습니다!
더 좋은 점은 Pyodide 프로젝트가 micropip도 포함한다는 것입니다. 이는 CORS를 통해 PyPI에서 추가적인 pure-Python 패키지를 로드할 수 있게 해주는 메커니즘입니다.
Pyodide가 가능한 것은 WebAssembly 덕분입니다. WebAssembly는 원래 다른 언어로 작성된 방대한 소프트웨어 컬렉션을 HTML 도구에서도 로드할 수 있게 합니다.
Squoosh.app은 이 패턴의 힘을 납득시킨 첫 사례였습니다. 여러 ‘최고 수준’ 이미지 압축 라이브러리를 브라우저에서 직접 사용할 수 있게 해줍니다.
저도 몇몇 도구에 WebAssembly를 사용했습니다.
100개 이상의 도구를 공개 컬렉션으로 가지고 있는 가장 큰 장점은, 제 LLM 조수들이 이를 흥미롭게 재조합하기 쉽다는 점입니다.
가끔은 이전 도구를 컨텍스트에 복사해 붙여넣기도 하지만, 코딩 에이전트를 쓸 때는 이름으로 참조할 수 있습니다. 또는 에이전트에게 작업 시작 전에 관련 예시를 검색하라고 할 수도 있습니다.
작동하는 도구의 소스 코드는 그 자체로, 편집 라이브러리 사용 패턴 등을 포함한 명확한 문서 역할을 합니다. 컨텍스트에 기존 도구 한두 개만 있어도, LLM이 작동하는 코드를 내놓을 가능성이 크게 높아집니다.
저는 pypi-changelog 를 Claude Code에게 이렇게 말해서 만들었습니다.
Look at the pypi package explorer tool
그리고 Claude Code가 zip-wheel-explorer의 소스 코드를 찾아 읽은 뒤에는 이렇게 말했습니다.
Build a new tool pypi-changelog.html which uses the PyPI API to get the wheel URLs of all available versions of a package, then it displays them in a list where each pair has a "Show changes" clickable in between them - clicking on that fetches the full contents of the wheels and displays a nicely rendered diff representing the difference between the two, as close to a standard diff format as you can get with JS libraries from CDNs, and when that is displayed there is a "Copy" button which copies that diff to the clipboard
여기 전체 트랜스크립트가 있습니다.
또 다른 자세한 리믹스 예시는 Running OCR against PDFs and images directly in your browser를 참고하세요.
저는 LLM과 함께 한 모든 작업의 기록을 남기고(그리고 공개하는 것까지) 좋아합니다. 시간이 지나며 이를 통해 LLM을 사용하는 기술을 성장시키는 데 도움이 되기 때문입니다.
LLM 플랫폼과 직접 채팅해서 만든 HTML 도구는 각 플랫폼의 “share” 기능을 사용합니다.
Claude Code나 Codex CLI 같은 코딩 에이전트(또는 다른 에이전트)의 경우에는, 터미널의 전체 트랜스크립트를 복사해 제 terminal-to-html 도구에 붙여넣고, Gist로 공유합니다.
두 경우 모두, 완성된 도구를 저장소에 저장할 때 커밋 메시지에 해당 트랜스크립트 링크를 포함합니다. 이는 tools.simonwillison.net 콜로폰에서 확인할 수 있습니다.
지난 1년 반 동안 이런 방식으로 LLM의 역량을 탐험하는 것은 정말 엄청나게 재미있었습니다. 이렇게 도구를 만드는 과정은 HTML로 도구를 구축할 수 있는 잠재력과, 제가 함께 쓰고 있는 LLM들의 능력을 이해하는 데 엄청난 도움이 되었습니다.
여러분도 자신만의 컬렉션을 시작해 보고 싶다면 강력 추천합니다! 시작에 필요한 것은 GitHub Pages가 활성화된 무료 GitHub 저장소뿐입니다(Settings -> Pages -> Source -> Deploy from a branch -> main). 그리고 어떤 방식으로든 생성한 .html 페이지를 복사해 넣기 시작하면 됩니다.
보너스 트랜스크립트: 이 글에 스크린샷을 추가하기 위해 제가 Claude Code와 shot-scraper를 어떻게 사용했는지 여기에서 확인할 수 있습니다.