Building personal software with Claude

Nelson Elhage

Claude로 만드는 개인 소프트웨어

원문은 Nelson Elhage님이 에 게재했습니다. 이 블로그 구독하기

이번 달 초, 나는 Claude를 이용해 Emacs 패키지의 일부를 Rust로 포팅했고, 실행 시간을 1,000배 이상 단축했다(구체적인 사례 하나에서는 90초에서 약 15ms로 줄었다).

이런 종류의 야크 셰이빙은 업무적으로나 개인 컴퓨팅 환경을 위해서나 내가 꽤 일상적으로 하는 일이다. 하지만 이번에는 Claude가 내 감독 하에 거의 한 줄의 코드도 직접 쓰지 않고 프로젝트 거의 전체를 수행해 냈고, 직접 손으로 했을 때에 비해 프로젝트 속도를 상당히 높여 주었다.

이 프로젝트에 들어가면서도 나는 Claude가せいぜい “더 나은 문서 검색 엔진이자 더 나은 Stack Overflow” 수준의 작은 도움 정도에 그칠 거라고 예상했다. Anthropic에서 일하면서 Simon Willison의 글을 틈틈이 읽고 있었음에도, 내 기대는 분명 크게 시대에 뒤처져 있었던 셈이다!

이번 경험은 소프트웨어 엔지니어링과 내 작업에서 LLM이 하는 역할에 대한 내 생각 상당 부분을 바꿔 놓았다. 이런 생각들은 아직 정리되는 중이지만, 이 글은 내 경험을 기록하고 나의 행동과 믿음, 기대를 어떻게 업데이트할지 고민하며 생각을 소리 내어 정리해 보려는 시도다.

문제

지난 1년여 동안 나는 Emacs org-mode filesWorkflowy 조합을 대체해 Obsidian.md의 헤비 유저가 되었다. Obsidian에서 마음에 드는 점은 많지만, 이 글과 관련해 중요한 점은 (a) 데이터를 로컬 디스크의 일반 Markdown 파일에 그대로 저장한다는 것과 (b) 그 파일들과 상호작용할 수 있는 괜찮은 Emacs mode가 있다는 것이다. 노트북에서도 여전히 주로 앱을 쓰지만, Emacs로 들어가 복잡한 텍스트 편집을 하거나 코딩하면서 에디터 안에 TODO 리스트를 유지할 수 있다는 점은 내게 매우 소중하다.

하지만 vault가 커지면서 문제가 생겼다. obsidian.el도저히 쓸 수 없을 정도로 느려진 것이다. 노트 하나를 여는 것만으로도 Emacs가 멈췄고, 처음에는 몇 초, 나중에는 1분 이상이었다. 문제는 알고 보니 obsidian-update 함수에 있었다. obsidian.el은 내부 인벤토리를 업데이트하기 위해 주기적으로 vault 전체를 다시 순회하며 모든 노트에서 태그와 메타데이터를 스캔하는데, 이 스캔(완전히 elisp로 작성된)이 결국 매우 느렸던 것이다.

나의 계획

나는 elisp를 어느 정도 다룰 줄은 알지만 프로파일링이나 최적화 경험은 없고, GitHub issue에는 이미 다른 사람들의 시도가 몇 가지 올라와 있었다. 그래서 다른 접근법을 택하기로 했다.

vault를 스캔해 인벤토리를 만들고 최소한의 메타데이터를 JSON으로 출력하는 짧은 프로그램을 Rust로 작성한 뒤, obsidian.el이 그 출력을 소비하도록 수정할 계획이었다. 나는 Rust가 더 편하고, 일반적으로 성능이 매우 뛰어나며 훌륭한 프로파일링 도구를 갖추고 있다는 것도 알고 있었다.

이런 해결책을 업스트림에 포함시키기는 까다로울 수 있지만, 내 개인 용도로는 비교적 규모가 작은 프로젝트가 될 거라 꽤 낙관했고, 그렇게 하면 내 문제는 확실히 해결할 수 있을 거라 확신했다.

Claude 활용하기

문득, 이 문제 중 얼마나 많은 부분을 Claude가 해결해 줄 수 있을지 시험해 보기로 했다. 크게 잘 될 거라 기대하진 않았지만 일단 해보기로 했다. 어쩌면 Claude가 나를 놀라게 할지도 모르니까. 비교적 명확한 목표를 가진, 잘 정의된 요청으로 시작하되, 프로젝트 대부분을 한 번에 해달라고 부탁하기로 했다.

[Uploaded file: obsidian.el]

obsidian-update 함수가 너무 느립니다. 관련 로직을 Rust 프로그램으로 포팅해서 관련 정보를 JSON으로 출력하도록 하여 속도를 높이고 싶습니다. 그런 Rust 프로그램의 첫 버전을 작성해 줄 수 있나요? vault 디렉터리를 커맨드라인 인자로 받도록 하고, 다른 모든 값은 기본값으로 설정된 것으로 가정해도 됩니다.

한마디로, 그냥 됐다. 초기 버전은 여기 있다.

단 한 번의 프롬프트로, chain-of-thought나 다른 명시적인 추론 없이 Claude는 약 1,000줄의 Emacs Lisp를 읽고, obsidian-update와 관련된 약 200줄을 식별해 내고, 그 코드와 파일의 나머지 부분 사이의 데이터 경계를 파악한 뒤, 그 데이터를 위한 JSON 포맷을 설계하고, 관련 로직을 약 150줄의 Rust로 포팅했다.

그 결과 코드는 첫 시도에 컴파일되어 동작했고, Claude는 필요한 의존성이 담긴 정상 작동하는 Cargo.toml까지 출력했다.

더 나아가 보기로 했다. Emacs 쪽 코드도 작성할 수 있을까? 좀 더 야심 차게, 업스트림 프로젝트를 포크하거나 패치하지 않고도 내 설정에 포함할 수 있는 결과물을 만들려는 의도로, obsidian.el을 패치하는 데 Emacs advice를 사용해 달라고 부탁했다. 프롬프트는 다음과 같았다.

[Uploaded file: obsidian.el]
[Uploaded file: obsidian-scan/src/main.rs]

이제 obsidian.elobsidian-scan을 사용하도록 패치하는 elisp 코드를 작성해 주세요. 원본 파일을 직접 수정하는 대신 elisp의 “advice” 기능을 사용해 주세요. 그래서 obsidian.el을 로드한 뒤 내 설정에 당신의 스니펫을 포함할 수 있도록 하고 싶습니다.

이것 역시 거의 그냥 바로 동작했다. (내가 아는 한) 버그가 하나 있었다. Emacs 쪽에서 태그 앞에 #을 붙이고 있었는데, Rust 쪽에서도 이미 하나를 포함하고 있어 ##writing 같은 태그가 생긴 것이다. Claude에게 수정을 요청했다.

obsidian--tags-list##writing처럼 ##가 이중으로 들어간 항목이 생기고 있습니다. Rust 코드와 elisp 중 어느 쪽을 고치는 게 좋을까요? 하나를 선택해서 업데이트해 주세요.

Claude는 elisp를 선택해 사소한 버그 수정을 반영하여 파일을 다시 작성했다. 이는 이 프로젝트에서 남은 모든 상호작용의 특징이었다. 내가 개선이나 수정을 요청하면 Claude는せいぜい 사소한 시행착오만으로 그것을 실행했다.

이런 식으로 몇 번 더 오갔다. 특히 Claude에게 (file -> tags) 매핑을 출력하고 Emacs에서 obsidian-tag-find에 사용하도록 요청했다. 이제 나는 Rust 프로그램과 elisp 패치 모두를 내 개인 시스템 설정에 추가했고, 지금까지 매우 만족하고 있다!

프로젝트 전체는 대략 반나절 정도 걸렸다.

회고

Claude는 내가 생각했던 것보다 코드를 훨씬 잘한다

나는 원래 프롬프트를 던질 때 크게 기대하지 않았다. 하지만 Claude 3.5 Sonnet이 코드 작성 능력이 크게 개선되었다는 얘기를 들었고, 일반적으로 실패를 예상하더라도 주기적으로 LLM을 시험해 보는 편인데, 그래야만 그들의 역량에 대해 정확히 감을 유지할 수 있다고 생각하기 때문이다.

우선 첫 번째 재작성에 감탄했다. 명확한 스펙이 주어지면 Claude가 100줄 정도의 코드를 작성하는 건 잘할 거라고 생각했지만, 천 줄의 elisp 안에서 암시적인 스펙을 추출해 내고 그 스펙에 맞춰 실행하는 것은 나를 놀라게 했다. 시스템을 반복적으로 개선해 나가는 과정에서도 더욱 감탄했는데, Claude는 컨텍스트 윈도우 안의 적당한 분량의 코드를 읽고 정교한 방식으로 업데이트하거나 대응하는 데 계속해서 매우 효과적이었다. 내가 시도한 거의 모든 것이 “그냥 됐다.”

나는 이 모델들이 기하급수적인 속도로 개선되고 있으며, 6개월이나 12개월 전에 형성된 인상은 거의 확실히 크게 시대에 뒤처졌다는 점을 머리로는 아주 잘 이해하고 있다. 하지만 그런 추상적인 이해만으로는 새로운 현황에 대한 정확한 인상을 형성하기에 충분하지 않다! 내가 아는 유일한 방법은 그냥 계속 모델을 사용하고 상호작용하며 (그리고 그렇게 하는 다른 사람들의 보고를 읽는) 것이다. 바로 그 사실 때문에 나는 실패를 예상하면서도 이 프롬프트를 시도했고, 지금 이 회고를 쓰는 이유이기도 하다.

“감탄해야” 할 일인가?

이 글을 쓰는 과정에서도 Claude에 대한 내 자신의 반응과 생각이 매우 흥미롭다.

위에서 설명했듯이, 내 첫 반응은 “놀랍고 인상적이다”였고, Claude는 기대를 뛰어넘었다. 원래의 obsidian.el은 약 10,000토큰 길이인데, Anthropic이 그 정도 분량의 텍스트를 아예 처리할 수 있는 첫 공개 모델을 출시한 것이 불과 2023년이었다. 그리고 내 작업에서 Claude는 입력에서 단 하나의 사실이나 요약된 요지를 추출하는 것을 넘어, 파일 곳곳에 흩어진 많은 세부 사항들을 정확히 이해하고 해석해야 했다. 최근까지만 해도 모델들은 그런 일을 할 수 없었다! 그것도 단 한 번의 프롬프트로, chain-of-thought나 명시적인 추론 없이 해낸 것이다! 그러니 감탄한 것도 당연하다.

동시에, 이 글을 초안으로 쓰는 와중에도 내 마음속에서 익숙한 골대 옮기기가 일어나는 것을 느꼈다! 나는 놀라움과 경이로움에서 거의 염세적인 축소로 옮겨가는 나 자신을 발견하는데, 적어도 두 가지 관점에서 그렇다.

  1. 글쎄, 하지만 당연히 예상했어야 하는 거 아니었나? 나는 많은 엔지니어들이 Claude 3.5 Sonnet의 코딩 능력에 감탄하는 것을 봤다. Anthropic 면접 문제를 해결하는 것도 직접 봤고, 이번 작업은 코드가 조금 더 많긴 했지만, 훨씬 더 큰 코드 덩어리를 읽고 질문에 답하는 것도 본 적이 있다. 나는 놀랐지만, 그건 모델이 대단해서라기보다는 내 기대치의 문제였다!
  2. 이 작업이 정말 그렇게 인상적인가? 이 프로젝트는 “고작” 1,000줄 안팎의 코드를 다뤘다. 내가 작업하는 많은 프로젝트는 수십만, 수백만 줄의 코드에 손을 댄다! elisp를 Rust로 다시 쓰는 것이 듣기에는 인상적으로 들릴지 몰라도, 근본적으로는 “그저” 번역 문제에 가깝다. 꽤 명확하게 정의된 작업이고, 언어 모델은 일반적으로 “번역처럼 보이는” 작업에 꽤 강하다.

나는 이 두 관점, 즉 경외감과 축소 모두 각기 다른 면에서 타당하다고 생각한다! 다음은 모두 사실이다.

  • LLM은 1년 전보다 코딩을 훨씬, 훨씬 더 잘한다
  • 그리고 더욱이, 지금의 능력은 불과 2~3년 전만 해도 절대 불가능한 SF처럼 들렸을 것이다
  • 하지만 동시에, 현재 모델은 어느 정도 “그냥 같은 추세의 연장”에 가깝다. 인상적이긴 하지만, 1년 전 ML 내부자들과 리더들이 예측했던 지점과 그렇게 크게 다르지 않아 보이기도 한다
  • 또한 현재 모델은 인상적이긴 하지만 여전히 전문 인간의 성능에는 한참 못 미친다. 적절한 맥락에서는 유용하지만, (아직!) “내 일을 대신할” 수는 없다

이것이야말로 지금 ML과 LLM의 본질이라고 생각한다. 기하급수적인 성능 향상 속도는 우리가 영원히 경외와 놀라움과 SF 같은 경험과, “어, 그냥 예전과 같은 것의 연장 아닌가, 뭐가 대수지?”라는 감각 그 중간 어딘가에 머물게 만든다.

조심스럽게 설렌다

이 프로젝트는 Claude나 다른 LLM의 도움을 받아 소프트웨어를 만드는 것에 대해, 이전에는 느껴보지 못했던 개인적인 설렘과 열정을 느끼게 해준 것 같다.

구체적으로 말하면, 내가 겪는 문제를 해결하고 내가 원하는 도구를 만들기 위해, 즉 “내 가려운 곳을 긁기 위해” 소프트웨어를 작성하는 기쁨과 설렘을, 최근 몇 년간 거의 느껴보지 못했던 방식으로 느꼈다. 더 어릴 때, 특히 학부 시절에는 둘러싸여 있었다. 내가 직접, 혹은 나와 비슷한 친구들이 우리의 문제를 해결하고 스스로에게 새로운 능력을 부여하기 위해 만든 크고 작은 소프트웨어 조각들에. 우리는 엄청난 힘과 낙관, 가능성을 느꼈고, 우리 스스로 컴퓨팅 환경을 만들어 가고, 우리만의 도구를 만들고 원하는 경험을 빚어낼 수 있다는 살아 있는 믿음을 가지고 있었다. 이러한 정신은 몇몇 친구들이 쓰는 작은 스크립트부터 당시 가장 크고 성공적인 오픈소스 프로젝트들에 이르기까지 다양한 규모에 걸쳐 존재했다.

그 이후로 세상도 변했고 나도 변했다. 우리의 데이터는 무관심한 곳부터 악의적인 곳까지 다양한 독점 기업이 소유하고 운영하는 고립된 사일로화된 클라우드 서비스 안에 점점 더 갇히게 되었다. 모든 것이 더 복잡해졌고, 겹겹이 쌓인 추상화와 복잡성, 난해한 OAuth 뒤에 가려졌다. 나 역시 그런 종류의 조직에서 일하는 것에 대해 읽고 배우며 커리어를 보냈고, 모든 소프트웨어를 마치 대규모로 배포되어 수년간 살아남을 것처럼 반사적으로 작성하는 법을 배웠다. 나는 그 작업 방식 자체, 견고하게 구축하고 단단한 기반을 찾는 도전조차 즐기기도 한다. 하지만 Google API에 인증하는 방법을 알아내거나 JavaScript 프레임워크의 새 버전으로 마이그레이션할 때마다 느끼는 극심한 절망과 결합되면서, 결국 나는 나와 친구들을 위한 작은 소프트웨어를 만드는 일을 대부분 그만두게 되었다.

이제 오랜만에 처음으로 그 옛 설렘의 불씨를 다시 느낀다! Claude는 난해한 인증 시스템을 다루거나 최신 React 버전을 아는 일 등에서 내가 평생 도달할 수 없을 만큼 훨씬 더 뛰어나다. 이제, 혹은 적어도 곧, 내가 바라는 도구나 스크립트를 정확히 설명하기만 하면 Claude가 궂은일을 해주는 세상에 살고 있는 것처럼 느껴진다. 시간이 흘러 API가 deprecate되고 어떤 프레임워크의 v27로 마이그레이션해야 할 때도 Claude가 아마 그 작업을 해낼 수 있을 것이다. 심지어 몇 년마다 코드를 전부 버리고, 같은 문제 정의와 최신 문서 사본을 들고 Claude와 새로운 세션에서 처음부터 다시 시작하는 세상에 살게 될지도 모른다. 나는 희망과 설렘의 불씨를 느낀다. 지난 20년간 소프트웨어 엔지니어링에 스며든 무수한 귀찮은 일들을 Claude가 처리해 주면서, 동시에 나의 강박적인 완벽주의가 세부 사항에 빠져 헤매지 않도록 높은 수준의 설계와 개념 차원에 머무를 수 있게 해준다면, 나 자신이나 내 커뮤니티를 위해 작은 소프트웨어를 만드는 법을 다시 배울 수 있지 않을까?

도구와 함께한 게 아니라 도구와 싸웠다

Claude.app / claude.ai는 이 문제에 잘 맞춰 설계된 느낌이 아니었다. 나는 UI에서 디스크의 파일로 복사해 붙여 넣고, 다시 오류나 다른 출력을 Claude에게 복사해 붙여 넣는 데 많은 시간을 썼다.

대화에 적절한 컨텍스트를 어떻게 유지해야 할지 파악하는 것이 어려웠다. 결국 세 개의 파일이 생겼다. 원본 obsidian.el, Rust 포팅본 main.rs, 그리고 elisp 어댑터 코드 obsidian-scan.el이다. 하나의 긴 대화를 그대로 써야 할지, 아니면 이들을 모두 Project로 묶고 주기적으로 새 대화를 만들어야 할지(언제?) 명확하지 않았다. 하나의 긴 대화에서 작업하는 것은 간단했지만, 컨텍스트 윈도우가 길어질수록 느려졌고, 결국 Claude가 히스토리에서 볼 수 있는 파일의 서로 다른 버전들 때문에 더 혼란스러워하는 것을 느꼈다. Claude가 생성한 artifacts를 프로젝트로 다시 복사하기 위해 “add to project” 버튼을 써 보았지만, 두 가지 주요 문제가 있었다.

  • 생성된 artifacts를 두고 Claude와 빠르게 반복하다 보니 Project에는 오래된 버전이 남게 되었다
  • Claude가 artifacts의 이름을 바꾸거나 제목을 다시 지정하는 것을 허용하지 않아, 결국 “Obsidian.el Performance Enhancement with Rust Scanner”라는 이름의 파일이 생겼고, 질문을 하거나 특정 파일에 대한 변경을 요청할 때 지칭하기 어려웠다.

이 영역은 현재 활발히 개발이 진행 중인 분야다. 적절한 MCP server나 새로 나온 많은 AI-first IDE 중 하나를 사용했다면 더 간단했을 수도 있다.

전반적으로 이번 경험은 LLM 주변의 툴링과 인터페이스 설계가 실제 역량에 한참 뒤처져 있으며, 향후 모델 개선과는 별개로 활발한 실험과 개발이 이루어지는 영역이라는 내 믿음을 강화했다.

정의된 인터페이스 사이에서 작업하기

Claude와 작업하면서 나는 본능적으로 문제를 비교적 잘 정의되고 테스트 가능한 인터페이스를 가진 단위로 나누는 선택을 하고 있음을 깨달았다. 예를 들어 Rust와 elisp 코드를 한 번의 질의로 변경해 달라고 하는 대신, Rust 쪽에 기능을 추가해 달라고 요청한 뒤 출력된 JSON을 검사해 스팟 체크를 하고, 그 다음에 대응되는 elisp 변경을 요청하는 식이었다.

이것이 얼마나 자주 필요했는지는 잘 모르겠지만, Claude를 위해서만큼이나 나 자신을 위해서 그렇게 했던 것 같다. 인터페이스 경계와 testability를 기준으로 조각을 나누는 것이 내가 무슨 일이 일어나고 있는지 이해하고 검증할 수 있다는 느낌을 주었고, 일이 꼬일 때 어떻게 개입하고 디버깅해야 할지 알 수 있게 해주었다. 이는 적어도 부분적으로는 주니어 엔지니어와 함께 일하는 방식과 크게 다르지 않다. 시스템 설계나 분해를 내가 더 많이 하고, 잘 정의된 컴포넌트를 위임하는 식이다.

이는 정확성과 테스팅에도 분명 도움이 될 것이다. 나는 Claude의 Rust 코드가 원래 elisp와 얼마나 잘 일치하는지 굳이 검증하지 않았다. 내가 알아차릴 만한 버그가 있을 때까지는 사실 신경 쓰지 않기 때문이다. 하지만 “이 파일시스템 트리가 주어지면, 이 태그와 기타 메타데이터를 포함한 이 노트들을 찾아야 한다”는 매우 쉽게 테스트할 수 있는 문제이며, 만약 더 신경 쓰거나 버그를 발견하게 되더라도 내가 직접 내부를 파헤치기보다는 이를 테스트 케이스로 인코딩해 Claude에게 코드를 다듬어 달라고 요청함으로써 해결할 수 있다.

나는 적어도 당분간은 이것이 강력한 패턴으로 남을 것이라 예상한다. 인간이 정의한 인터페이스 경계나 시스템 분해, 그리고 그 “사이”를 Claude가 채우는 방식이다. 아마 시간이 지나면 Claude는 점점 더 큰 서브시스템을 대부분 자율적으로 처리할 수 있게 될 것이고, 우리 역시 더 모호한 스펙을 가진 것들을 다루는 데 대한 자신감과 기술을 쌓게 될 것이다.

코드는 그 어느 때보다 저렴해졌다

한동안 나는 코드 라인 자체는 비교적 저렴하다는 관점을 점점 더 강하게 갖게 되었다. 소프트웨어 개발에서 더 가치 있는 자원은 코드에 대한 이해, 즉 코드가 무엇을 하고 왜 하는지에 대한 이해, 그리고 어떤 코드를 작성해야 하는지에 대한 지식에 가깝게 보인다1.

LLM은 적어도 단기적으로는 그 역학을 엄청나게 증폭시킬 가능성이 높아 보인다. 이제 단 몇 센트로 수천 줄의 코드를 생성할 수 있다. 하지만 어떤 인간도 그 코드를 이해하지 못할 것이고, LLM은 지금으로서는 그 코드들을 생성하는 것보다 디버깅하고 리팩터링하고 설계하고 유지하는 데 더 서툴다. 따라서 코드는 그 어느 때보다 저렴해졌지만, 통찰과 좋은 아키텍처 설계, 그리고 이해는 적어도 지금으로서는 그 어느 때보다 더 가치 있어질 것이라 생각한다.

나는 이 변화가 어떤 아키텍처 패턴이 합리적인지에도 영향을 미칠 것이라 본다. 아마 삭제하기 쉬운 코드를 작성하는 것이 그 어느 때보다 더 중요해질지도 모른다. 모듈 전체를 그냥 버리고 Claude N+1이나 GPT M+1에게 처음부터 다시 시작해 달라고 할 수 있도록 말이다. 어쩌면 여러 프로그래밍 언어를 아우르는 “다중 언어” 시스템을 구축하는 허들도 훨씬 낮아질지 모른다. 서로 다른 언어 생태계나 성능 특성의 이점을 취하면서도, 네 가지 난해한 언어에 능숙한 엔지니어를 채용해야 한다는 걱정을 덜 수 있다면 말이다. 앞으로가 흥미로울 것이다.


  1. 나는 Naur의 Programming as Theory-Building에 대해 언젠가 글을 써봐야겠다고 계속 생각해 왔다. 내가 가장 좋아하는 논문 중 하나이자, 이 통찰을 포착한 최초의 글 중 하나로 내가 알고 있는 논문이다. ↩︎

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글