형편없는 타입을 지지하며

ko생성일: 2025. 9. 14.갱신일: 2025. 9. 16.

LLM 에이전트가 복잡한 타입 시스템과 LSP에서 왜 고전하는지, 대신 Go처럼 단순·구조적인 타입이나 JSDoc 같은 베스트 에포트 타입 힌트가 어떻게 더 실용적일 수 있는지에 대한 고찰.

작성일: 2025년 8월 4일

아마 여러분은 내가 Rust와 TypeScript를 사랑하고, 좋은 타입 시스템의 열렬한 지지자라는 걸 알고 있을 것이다. 내가 그것들을 유용하다고 느끼는 이유 중 하나는 자동 완성을 가능하게 해주기 때문이다. 보통 자동 완성은 좋은 기능이다. 말이 되는, 잘 통합된 타입 시스템이 있고 메모리 레이아웃에 대한 최적화 여지를 제공한다면 대체로 좋은 아이디어다.

그렇다면 자연스럽게, 이런 것들이 에이전트 기반 코딩 도구에도 훌륭할 거라고 생각하기 쉽다. 분명 이점이 있다. 에이전트에게 TypeScript를 쓰게 하고 타입을 추가하게 하면 꽤 잘한다. 생짜 JavaScript보다 더 잘하는지는 모르겠지만, 최소한 해가 되지는 않는 듯하다.

하지만 대부분의 에이전트형 도구는 LSP(언어 서버 프로토콜)에 접근할 수 없다. 타입 정보에 접근 가능한 LSP를 갖춘 에이전트 코딩 도구로 실험해본 바, 의미 있는 이득을 보지 못했다. LSP 프로토콜은 속도를 늦추고 컨텍스트를 심각하게 오염시킨다. 게다가 모델들이 이런 정보를 다루는 법을 충분히 학습하지도 못했다. 단순히 컴파일러의 타입 체크 실패를 텍스트 형태로 받아오는 편이 오히려 더 나은 결과를 준다.

결국 타입 체크를 켜지 않은 상태의 에이전트 코딩 루프에서는 에이전트가 코드를 쓰고 어딘가에 타입을 적어 넣으면서 앞으로 나아가게 된다. 그 결과물이 어떤 형태로든 JavaScript로 컴파일되기만 하면(만약 Bun을 쓰면 그중 상당수는 타입이 지워진다) 동작하는 코드를 만들고, 거기서 계속 진행한다. 하지만 그건 나쁜 진전이다—나중에 돌아와서 타입을 정리해야만 하는 종류의 진전이다.

흥미로운 점은 타입이 분명히 작성되고 있는데도 대부분 무시된다는 것이다. 루프에 타입 체크를 넣으면, 내 테스트에서는 오히려 성능이 더 나빠졌다. 그 이유는 에이전트가 우선 코드를 실행 가능하게 만든 뒤, 일이 끝난 다음에서야 타입 체크를 돌리기 때문이다. 그러고 나서, 어쩌면 훨씬 나중에, 타입 오류를 냈다는 걸 깨닫는다. 그러면 고치기 시작하고, 어쩌면 루프에 빠지고, 엄청난 컨텍스트를 낭비한다. 매 편집마다 타입 체크를 하게 만들면 컨텍스트는 더더욱 빠르게 소모된다.

타입 자체가 믿을 수 없을 만큼 복잡하고 직관적이지 않을 때는 상황이 정말 나빠진다. TypeScript에는 난해한 표현 기능이 있고, 일부 라이브러리는 복잡한 구성요소를 지나치게 사용한다(예: conditional types). LLM은 이런 것들을 읽는 법을 거의 모른다. 예를 들어 TanStack Router의 .d.ts 파일과, 라우터 시스템이 제대로 동작하도록 사용하는 포워드 선언 같은 장치를 접근 가능하게 해줘도, LLM은 그 어느 것 하나도 이해하지 못한다. 추측을 하고, 때로는 심하게 틀린다. 완전히 혼란스러워한다. 타입 오류를 만나면 온갖 조작을 시도하는데, 어느 것도 도움이 되지 않는다.

Python의 타이핑은 더 나쁜 문제를 안고 있다. 거기서는 서로 다른 타입 검사기들이 타입 검사가 어떻게 동작해야 하는지에 대해 합의조차 하지 못한다. 내 테스트 기준으로는, LLM이 mypy가 아닌 도구들의 타입 체크 오류를 어떻게 해결해야 하는지조차 완전히 이해하지 못하는 경우가 있다. 늘 나쁜 건 아니지만, 만약 당신이 스스로도 해결하지 못하는 복잡한 타입 검사 오류에 부딪히면, LLM 역시 무엇이 일어나는지 끝내 완전히 파악하지 못하거나 적어도 여러 번의 시도가 필요한 경우가 놀랄 만큼 자주 있다.

타입이 큰 가치를 더하는 빛나는 사례로는 Go가 있다. Go의 타입은 표현력이 훨씬 적고 매우 구조적이다. 어떤 타입이 특정 메서드를 갖고 있으면 그 자체로 인터페이스를 만족한다. LLM이 이를 이해하기 위해 알아야 할 것은 많지 않다. 또한 Go의 타입은 꽤 엄격하게 강제된다. 타입이 틀리면 컴파일되지 않는다. 복잡한 구성요소를 지원하지 않는 훨씬 단순한 타입 시스템 덕분에, LLM이 자신이 생산한 코드를 이해하기에도, 우리가 LLM에게 실제 라이브러리를 제공했을 때 LLM이 그 라이브러리를 이해하기에도 훨씬 잘 작동한다.

이걸 어떻게 해야 할지는 잘 모르겠지만, 이런 행동 양상은 JSDoc 같은 베스트 에포트 타입 시스템이나 타입 힌트가 훨씬 더 가치가 있음을 시사한다. 적어도 LLM 관점에서는 타입을 완전히 이해할 필요가 없고, 어떤 객체가 아마 무슨 타입일지를 대략적으로만 알면 된다. LLM에게는 에러 메시지의 타입 이름이 소스의 타입 이름과 일치하는지가 더 중요하다.

오늘날 LLM의 이러한 행동이 미래의 언어 설계에 영향을 줄지 여부는 흥미로운 질문이다. 그럴지 잘 모르겠지만, 적어도 Go와 Java 같은 언어로 이어진 일부 결정들에 꽤 설득력을 부여한다고 본다. 내가 과거에는 문제에 대한 그들의 다소 단순한 접근과, 개발자를 그다지 높이 평가하지 않는 듯한 설계를 비판해 왔지만, 이제 보니 그들은 실제로 상당히 좋은 위치에 서 있다. 내가 인정했던 것보다 그들의 설계에는 더 많은 우아함이 있다.

이 글은 aitypescript 태그가 달렸습니다.

복사 / 보기 마크다운