내가 LLM과 함께 코딩하는 방식

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

요즘 많은 사람들이 그러하듯이 나 역시 최근에는 LLM과 함께 코딩하는 일이 많아졌다. 내가 LLM과 함께 코딩한다고 말하면 의외라는 듯이 반응하는 사람들도 있고, 어떤 식으로 LLM을 활용하냐는 질문도 종종 받는다. 그래서 생각난 김에 내가 LLM을 코딩에 어떻게 활용하는지를 대략적으로 적어보고자 한다.

전제

당연하지만 내가 LLM을 활용하는 방식은 내가 주로 다루는 종류의 작업에 맞춰져 있다. 따라서 일반적인 다른 코딩에는 잘 맞지 않을 수도 있다. 내가 주로 다루는 작업이란 협업자들이나 소비자들과 주로 서면을 통해 비동기로 의사 소통을 하는 오픈 소스 프로젝트이며, 그것도 주로 애플리케이션 개발이 아닌 라이브러리 개발이다. 주로 사용하는 프로그래밍 언어는 TypeScript로 상당히 LLM이 잘 다루는 축에 속한다. 한편으로는 비교적 기존 지식의 도태가 빠르게 이뤄지는 생태계이기도 해서, 어떤 면에서는 불리한 점도 있다고 볼 수 있다.

어찌 되었든 내 LLM 활용 방식은 내가 주로 다루는 종류의 작업에 맞춰져 있으나, 그럼에도 이 글에서는 일반적인 코딩에 두루 적용할 수 있는 팁을 공유하려고 노력했다.

맥락이 왕이다

LLM 활용에 있어서 모델 자체의 성능 등 여러 고려 사항들이 있지만, 사람들이 LLM을 사용하는 것을 관찰했을 때 가장 흔히 놓치는 것이 충분한 맥락의 제공이다. 사람들은 자신이 무언가를 판단할 때 얼마나 많은 맥락에 의존하는지 의식하지 못한다. 머릿속 사소한 기억 조각부터 내가 접근 가능한 최신 문서들, 이슈에 담긴 캡처 이미지까지… 이들 대부분은 LLM이 기본적으로는 혼자서 접근할 수 없는 경우가 많다. 제아무리 LLM이 똑똑하다고 해도 필요한 맥락이 주어지지 않으면 엉뚱한 결과를 내놓기 마련이다. 주변에서 「LLM은 코딩을 너무 못한다」고 토로하는 경우를 보면 정말로 LLM이 풀기 어려운 문제를 주로 다루는 분들도 계셨지만, 대부분의 경우에는 LLM에게 충분히 맥락을 제공하지 못해서 그럴 때가 많았다.

아마도 이 글에서 다룰 대부분의 이야기는 결국 「어떻게 LLM에게 맥락을 잘 제공할까?」라는 고민에서 나온 팁들이라고 봐도 무방하다.

내가 쓰는 모델과 코딩 에이전트

2025년 9월 현재, 나는 거의 대부분의 작업에 Claude Code를 사용한다. 지난 몇 달 간 Claude Code를 주로 사용해 왔으며, 주기적으로 다른 도구들도 시도해 보고 있으나 여전히 Claude Code가 나에게 가장 맞다고 판단했다. 실은, 나는 대부분의 프로그래머에게 Claude Code가 가장 적합할 거라고 생각한다. 엄밀한 벤치마크에 근거한 것은 아니고, 체감일 따름이지만… 이유는 다음과 같다:

  • 다른 모델들은 도구 호출을 잘 못한다. 도구를 제공하면 필요한 순간에 도구를 활용할 수 있어야 한다. Claude 모델들은 이 부분에서 확실히 뛰어나다. 도구 호출은 풍부한 맥락을 제공하기 위해서는 반드시 필요하기 때문에, 도구 호출의 성능이 떨어지면 결과적으로 LLM을 동반한 코딩 자체의 성과가 떨어진다.

  • 다른 모델들은 질답이 길어질 때 성능이 떨어진다. 적어도 나는 LLM과 코딩할 때 한 방(one-shot)에 결과를 내놓는 방식, 즉 바이브 코딩(vibe coding)을 선호하지 않는다. 따라서 모델의 멀티턴(multi-turn) 성능이 중요한데, 다른 모델들은 질답이 길어짐에 따라 앞서 했던 이야기를 망각하는 경향이 있다.

  • 똑같이 Claude의 모델을 사용하더라도 Claude Code 자체의 성능이 다른 LLM 코딩 에이전트에 비해 뛰어나다. 이건 파인 튜닝이나 시스템 프롬프트 등에 비법 소스가 있기 때문이라고 여겨진다. 이 때문에 Claude Code를 Claude가 아닌 모델과 함께 쓸 수 있게 해주는 비공식 프록시 등도 존재한다.

물론, Claude 및 Claude Code에도 단점이 있다:

  • 상대적으로 문맥 윈도(context window)가 짧다. 그래서 토큰을 아껴야 한다. Claude Code의 대화 압축(conversation compaction)은 그럭저럭 잘 동작하는 편이긴 해도, 여전히 스트레스이긴 하다. (이를테면, 대화 압축은 영어로 이뤄지기 때문에 후속 세션에서는 갑자기 영어로 대답하기 시작한다. 아, 나는 프롬프트를 한국어로 적는다.)

  • 일부 LLM 코딩 에이전트들이 제공하는 LSP 지원이 아직 없다. 따라서 타입 오류나 린트 오류 등을 명령어 실행 등을 통해 따로 보여줄 수 있어야 한다. 대신 Claude Code에서는 (hooks) 기능이 제공되므로, 이를 잘 활용하면 어느 정도는 비슷한 효과를 볼 수 있다.

하지만 장점이 단점을 압도하기 때문에 앞으로도 상황에 큰 변화가 없다면 Claude Code를 주로 쓸 것 같다.

세부 지시는 서면으로

지시(prompt)는 세부적일 수록 좋다. 지시가 한 문장으로 끝난다면 좋은 지시가 아닐 가능성이 높다. 때로는 지시를 만드는 지시가 필요하기도 하다. 나의 지시 방식은 다음과 같다.

우선 대부분의 지시는 GitHub 이슈에 작성한다. 필요한 충분한 링크를 제공해야 하고, 가급적이면 캡처 이미지나 도표와 같은 시각적 정보에는 크게 의존하지 않아야 한다. 물론, 이슈는 기본적으로 LLM을 위한 것이 아니라 사람을 위한 것이므로 LLM에게만 필요한 정보는 이슈에 담고 싶지 않을 수 있다. 그런 건 이슈에 담지 않아도 된다.[1] 이미 남이 만든 이슈가 있다면 그 이슈를 활용해도 된다. 남이 만든 이슈에 맥락이 충분하지 않다고 여겨지면, 댓글로 맥락을 보충한다.

때로는 이슈 자체도 LLM으로 작성하기도 한다. 관련 문서나 상황을 충분히 공유하여 이슈를 작성해 달라고 하는 식이다. 예를 들면, 다음은 내가 만든 이메일 전송 라이브러리인 UpyoPlunk 트랜스포트를 추가하는 이슈 #11 Plunk transport를 작성하기 위해 사용했던 지시문이다:

Plunk라는 이메일 전송 프로바이더가 있습니다. Plunk의 트랜스포트를 Upyo에 추가하면 좋을 것 같은데요. 이슈 트래커에 일단 이슈를 먼저 만들고자 합니다. 이슈 제목과 내용을 영어로 작성해 주실 수 있을까요? 너무 문제 정의 및 해결책 제시가 구분되는 식의 형식적인 글 대신, 좀 더 사람이 쓴 것 같은 자연스러운 톤으로 부탁드립니다. 너무 길 필요도 없고요. 한두 문단 정도면 충분할 것 같습니다.

참고 링크:

단, 이 때 나는 Claude의 프로젝트 기능을 이용해서 Upyo의 기존 문서를 RAG로 제공한 상태에서 지시를 했다는 점을 밝힌다.

그 다음에 Claude Code의 계획 모드(plan mode)[2]에서 다음과 같이 지시한다.

https://github.com/dahlia/upyo/issues/11 이슈를 구현해야 합니다. 이슈 본문과 본문에서 링크된 관련 링크들을 모두 살펴본 뒤, Upyo 프로젝트에 Plunk 트랜스포트를 추가할 구현 계획을 세부적으로 세워주세요.

정확히는, 나는 이슈 링크를 제공하는 대신 이슈 번호만 제공하고 GitHub MCP를 이용해서 이슈를 직접 읽도록 하는 걸 선호한다. HTML이 아니라 Markdown 형식으로 이슈를 읽기 때문에 본문에 걸린 링크 등을 더 잘 따라가기 때문이다. 링크가 정말 중요한 경우 Claude Code의 지시에서 한 번 더 적기도 한다. 이슈에 미처 적지 못했던 LLM만을 위한 정보도 이 때 모두 적는다.

구현할 때 살펴봐야 할 소스 파일이 무엇인지 잘 알고 있다면, 그런 정보도 함께 제공하면 더 좋다. LLM이 코드베이스를 탐색하느라 삽질을 훨씬 덜 하고, 토큰도 덜 쓰기 때문이다.

나는 GitHub 이슈를 세부 지시를 적는 용도로 썼지만, PLAN.md 같은 문서 파일을 만들어서 거기다 적는 방법도 많이 쓰인다고 알고 있다.

설계는 사람이, 구현은 LLM이

내가 LLM을 코딩에 활용할 때의 기본적인 원칙은 큰 설계는 내 스스로 하고, 세부적인 구현은 LLM에게 맡긴다는 것이다. 지시할 때는 설계 의도를 정확히 제시하고, 구현 과정에서 실수할 수 있을 것 같은 우려점에 대해서 충분히 짚고 넘어간다. 특히, 나는 프로젝트를 처음 시작할 때 디렉터리나 패키지 구조를 여전히 직접 손으로 할 때가 많다. (하지만 이 부분은 내가 원체 LLM 시대 이전부터 쿠키커터 류의 프로젝트 템플릿도 좋아하지 않았기 때문일 수도 있다. 템플릿을 쓰든 LLM을 쓰든 내 마음에 들게 나오지 않기에.)

LLM은 자신이 가장 익숙한 기술로 문제를 해결하려는 경향이 있기 때문에, 기술 선택에 있어서도 명시적으로 지시를 하는 것이 좋다. 아직은 사소한 라이브러리 하나조차도 사람이 검토할 필요가 있다. LLM에게 모든 것을 맡기다 보면 보안 패치도 안 된 옛날 버전을 가져다 쓰거나 하는 일이 흔하기 때문이다. 나 같은 경우에는 후술할 AGENTS.md 문서에서 라이브러리를 설치하기 전에는 npm view 명령어를 통해 해당 패키지의 최신 버전이 무엇인지 먼저 확인하라는 지시를 포함시키기도 한다.

추상화를 할 때도, 적어도 API 설계는 여전히 내가 직접할 때가 많다. 어째서일까, LLM이 설계한 API는 아직은 별로일 때가 많다. 내가 받은 인상은, LLM은 구현해 나가며 필요할 때 API를 즉석(ad-hoc)에서 설계할 때가 잦다는 것이다. 물론, 사람이라도 이런 방식을 선호할 수 있는데, 그런 경우에는 LLM이 설계한 API에 불만이 없을지도 모른다. 하지만 적어도 내 경우에는 불만족스러울 때가 많다.

결국에는 LLM을 만능 노예가 아니라 똑똑하기도 하지만 미숙한 점도 많은 동료로 보고 LLM이 서툰 부분에는 최대한 사람이 도와서 일을 해낸다는 관점이 필요한 것 같다.

AGENTS.md

대부분의 LLM 코딩 에이전트들은 AGENTS.md 내지는 그에 준하는 기능을 제공하고 있다. 예를 들어 Claude Code는 CLAUDE.md 파일을 바라보며, Gemini CLIGEMINI.md를 바라보는 식인데, 점차 AGENTS.md 파일로 표준화되고 있는 추세이다. 나는 특정 벤더에서만 쓰는 파일들을 모조리 AGENTS.md로 심볼릭 링크를 건 다음 AGENTS.md 파일만을 정본으로 삼게 하고 있다. 이렇게 하면 나와 다른 LLM 코딩 에이전트를 사용하는 협업자들과 같은 지침을 공유할 수 있다.

아무튼, AGENTS.md 문서의 역할은 간단하다. 이 프로젝트에 대한 지침, 즉 시스템 프롬프트이다. 대부분의 LLM 코딩 에이전트들은 이 파일을 자동으로 생성하는 기능을 제공하는데, 안 쓸 이유는 없다. 자동으로 생성하게 한 후, 잘못된 부분만 고쳐서 써도 된다. 그보다 더 중요한 건 시간이 흐르면서 AGENTS.md 문서가 낡게 되는 것을 피하는 것이다. AGENTS.md 문서는 꾸준히 갈고 닦아야 한다. 특히, 대규모 리팩터링이 있거나 한 뒤에는 반드시 AGENTS.md 문서를 갱신해야 한다. 이 지시 자체도 AGENTS.md 문서에 넣어두는 게 좋다.

그러면 AGENTS.md 문서에는 어떤 내용을 넣을까? 나의 경우에는 다음과 같은 내용을 넣는다:

  • 프로젝트의 목표와 개요. 저장소 URL을 적어두는 것도 의외로 GitHub MCP 등을 활용할 때 쓸모가 있다.
  • 프로젝트가 사용하는 개발 도구. 예를 들어 npm은 절대 쓰지 않으며 pnpm만 쓴다는 식의 지시를 포함한다.
  • 디렉터리 구조와 각 디렉터리의 역할.
  • 빌드 및 테스트 방법. 특히, 그 프로젝트만의 특수한 절차가 있다면 반드시 기술한다. 예를 들어, 빌드나 테스트 전에 반드시 코드 생성을 해야 한다면, 이에 관해 적어야 한다—물론, 가장 좋은 것은 빌드 스크립트로 그러한 절차를 자동화하는 것이다. 그 편이 사람에게도 좋고, 토큰을 아끼는 데에도 좋다.
  • 코딩 스타일이나 문서 스타일. 포매터를 쓰는 방법을 적는 것도 좋다.
  • 개발 방법론. 이를테면 버그를 고칠 때는 회귀 테스트를 먼저 작성하고, 테스트가 실패하는 것으로 버그가 재현되는 것을 확인한 다음에 버그 수정을 하라는 지침 같은 것들.

일단은 위와 같이 시작하고, LLM 코딩 에이전트를 활용하면서 눈에 밟히는 실수들을 LLM이 할 때마다 지침을 추가하는 것을 권한다. 예를 들어, 나는 TypeScript 프로젝트에서 any 타입이나 as 키워드를 피하라는 지침을 추가하는 편이다.

다음은 내가 관리하는 프로젝트들의 AGENTS.md 문서들이다:

문서 제공

LLM에 지식 컷오프가 있다는 건 잘 알려져 있다. 방금 갓 나온 모델이 아닌 한, 내가 쓰는 라이브러리나 런타임 등의 API, CLI 도구 등에 대해 다소 낡은 지식을 가지고 있을 가능성이 높다는 뜻이다. 게다가 만약 비주류 프로그래밍 언어나 프레임워크 등을 쓰고 있다면 이 문제는 더욱 커진다.

따라서 내가 사용하는 프로그래밍 언어나 프레임워크 등에 대한 지식을 제공해야 하는데, 가장 쉽고 효율적인 방법은 Context7을 MCP로 붙이는 것이다. Context7은 다양한 기술 문서를 비교적 최신판으로 유지하면서 벡터 데이터베이스에 색인하고, LLM이 요청할 경우 관련된 문서 조각을 제공하는 RAG 서비스이다. 새로운 문서를 추가하는 것도 쉬워서, 만약 내가 필요한 문서가 등록되어 있지 않다면 얼마든지 새로 추가해서 사용할 수도 있다. 다만, 특별히 지시하지 않는 한 Context7을 따로 활용하지 않을 때도 있기 때문에, 「Context7 MCP를 통해 관련 문서를 확인해 보라」는 식의 지시가 필요할 수 있다.

RFC 같은 기술 명세 문서를 제공할 때는 평문(plain text) 형식이 제공되므로 평문 문서의 링크를 제공하는 게 좋다. 나의 경우 연합우주(fediverse) 관련 개발을 많이 하다 보니 FEP 문서를 제공해야 할 일이 많은데, 이 때도 HTML으로 렌더링 된 웹 페이지가 아닌 Markdown 소스 파일의 링크를 직접 제공하는 식으로 사용하고 있다.

이 외에도 웹사이트에서 /llms.txt/llms-full.txt 파일을 제공하는 관행이 퍼지고 있으므로, 이를 활용하는 것도 좋다. (내가 만든 소프트웨어 라이브러리들의 경우 프로젝트 웹사이트에서 모두 /llms.txt/llms-full.txt 파일을 제공하고 있다.)

다만 아무리 LLM에게 친화적인 평문이라고 해도 기술 문서 전체를 다 제공하는 건 아무래도 토큰 낭비가 심하기 때문에, Context7을 쓸 수 있다면 Context7을 쓰는 것을 추천한다.

계획 모드의 활용

Claude Code를 포함해 최근의 많은 LLM 코딩 에이전트는 계획 모드를 제공한다. 냅다 구현하는 것을 방지하고, 구현 계획을 LLM 스스로 세우게 한 뒤 사람이 먼저 검토할 수 있게 하는 것이다. 특히, 나는 계획 모드에서는 비싼 Claude Opus 4.1을 사용하고 실제 구현에서는 비교적 저렴한 Claude Sonnet 4를 사용하는 “Opus Plan Mode”를 사용하고 있다.[3]

나는 계획을 면밀히 검토하고 조금이라도 내 성에 차지 않으면 얼마든지 계획 수정을 요구한다. 작업에 따라 다르지만, 어떤 작업이든 적어도 서너 번 이상은 수정을 요구하는 것 같다. 반대로 얘기하면, 이 정도로 계획을 다듬지 않으면 내가 원하는 방향으로 구현하지 않을 가능성이 높다. LLM은 나와는 다른 전제를 품을 때가 많아서, 여러 세부 계획에서 나와는 동상이몽을 하고 있을 가능성이 높다. 그런 것들을 사전에 최대한 제거하여 내 의도에 일치시켜야 한다.

알아서 피드백 루프를 돌게

LLM 코딩 에이전트로 개발을 할 때 가장 중요하다고 여겨지는 부분은 바로, 스스로 웬만큼 방향을 조정할 수 있도록 여건을 마련하는 일이다. LLM의 각종 구현 실수를 하나하나 내가 지적하는 게 아니라, 각종 자동화된 테스트와 정적 분석을 통해 스스로 깨닫고 구현을 고칠 수 있도록 해주는 것이다.

예를 들어, CSS 버그를 고칠 때를 생각해 보자. LLM이 CSS 코드를 고치게 한 뒤, 내가 웹 브라우저를 확인하는 방식은 너무 번거롭다. 대신, Playwright MCP를 붙여서 스스로 화면을 볼 수 있게 하는 게 낫다. 요는, LLM의 작업 결과가 요구 사항을 충족하는지를 스스로 판단할 수 있게 하여, 요구 사항이 충족될 때까지 작업을 계속하게 만드는 것이다.

비슷한 이유에서, 구현하기에 앞서 테스트 코드를 먼저 작성하도록 지시하는 것이 여러모로 편하다. 테스트 코드를 작성하는 과정까지만 사람이 지켜보면 되고, 그 뒤는 상대적으로 신경을 덜 써도 되기 때문이다. 실은, 나는 LLM 코딩 에이전트를 활용할 때도 가끔은 테스트를 직접 짜기도 한다. 프롬프트로 요구 사항을 정확하게 검증하는 테스트 코드를 짜게 하는 것보다 내가 직접 테스트 코드를 짜는 게 빠르겠다고 느낄 때 그렇다.

이런 작업 흐름을 선호하다 보니, 좀 더 엄밀한 타입 시스템을 갖춘 프로그래밍 언어, 좀 더 엄격한 린트 규칙 등이 LLM 코딩 에이전트를 활용할 때 훨씬 유리하다고 생각하게 되었다. LLM 시대 이전에도 생각은 비슷하긴 했지만 말이다.

가끔은 손 코딩

하지만 여전히 LLM에 많은 한계점이 있기 때문에, 나는 아직도 가끔은 손 코딩을 한다. API를 설계할 때도 그렇고, 엄밀한 테스트 코드를 짜고 싶을 때도 그렇다. (LLM은 테스트 코드를 좀 대충 짜는 경향이 있다.) 그리고 무엇보다, 재밌을 것 같은 코딩은 내가 한다!

바이브 코딩에 깊게 심취했다가 코딩의 재미가 사라졌다는 소프트웨어 프로그래머들의 얘기를 종종 듣는다. 내 생각에는, 재미있는 부분은 LLM에게 시키지 않는 게 좋다. 결과의 품질 때문이 아니라, 소프트웨어 프로그래머로서 모티베이션을 유지하기 위해서 그렇다. 재미 없고 지루한 부분, 그러니까 코딩하기 싫어지게 하는 작업에서 최대한 LLM을 활용하는 것이 LLM과 공존하는 좋은 전략이 아닐까 생각한다. 뭐, 적어도 내게는 이 방식이 잘 먹히는 것 같다.


  1. 이건 한 가지 팁인데, LLM에게만 필요한 정보를 <!-- … --> 주석 안에 적는 방법도 있다. ↩︎

  2. Shift + Tab을 두 번 누르면 계획 모드에 진입할 수 있다. ↩︎

  3. Claude Code에서 /model 명령어를 통해 고를 수 있다. ↩︎

32
0
1

No comments

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/01996130-95d5-77a8-be87-8d36706b0853 on your instance and reply to it.