2년 전에 만든 토이 프로젝트를 바이브 코딩으로 심폐소생술 해보았습니다
오래된 레거시 프로젝트에 바이브 코딩을 적용했던 경험을 공유합니다.
4/7/2025, 11:11:00 PM
개요
최근 바이브 코딩이라는 용어가 개발자들 사이에서 뜨거운 주제인 것 같습니다. 제가 처음 이 용어를 접한 것은 한 유튜브 영상이었습니다. 피드를 내리던 도중, 분위기 좋은 카페에 앉은 한 남자분이 코딩을 하고 있는 썸네일이 보였고 제목은 바이브 코딩 어쩌구 하는 식이엇던 것 같습니다. 최근에 많이 보이는 ‘누구나 코딩 할 수 있다!’ ‘10분이면 뚝딱 만드는 xxx’ 식의 영상과 글들을 보며, 개발자를 업으로 삼는 입장에서 솔직히 적지 않은 스트레스를 받고 있던 터라 해당 영상도 눈쌀을 찌푸리며 휙 넘겼습니다.
‘바이브 코딩이라니.. 하다하다 이젠 코딩이 힐링의 영역으로 넘어갔단 말인가?’
그러고 며칠 후, 바이브 코딩은 OpenAI 의 공동창립자인 Andrej Karpathy 가 트윗에서 만들어낸 용어라는 것을 알게 되었습니다. 해당 트윗의 내용을 요약하면 이렇습니다.
뇌빼고 LLM에게 느낌 가는대로 시켜보니 잘 만들더라. 그리고 재밌다.
그러면서 그는 ‘주말에 대충 만드는 프로젝트라면 이 방식도 꽤 괜찮습니다.’ 라고 이야기 합니다.
이를 보고 저는 2년 전에 만들었던 토이 프로젝트가 하나 떠올랐습니다. 해당 프로젝트는 chatGPT의 창의성에서 영감을 받아 만든 채팅 기반의 웹게임으로, 매번 새로운 사건을 무한으로 제공하는 추리 게임입니다.
당시에는 프롬프트 엔지니어링을 잘하는 것이 LLM을 잘 다루는 비결이었는데, CoT, One-Shot 등 여러 기법을 활용하여 chatGPT 가 게임엔진으로서 충실하게 동작하도록 노력하였습니다.
그리하여 게임은 꽤나 재미있게 만들어졌다고 생각했습니다. 깃허브에 코드를 올려뒀는데 소수의 사람들이지만 star를 눌러준 것도 꽤나 고무적이었습니다.
그러나,, 너무나도 구린 UI 가 항상 마음 한 켠에 미련으로 남아있었습니다. UI만 조금 더 개선하면 훨씬 재미있게 즐길 수 있을 것 같았지만, 도저히 UI 작업에 의욕이 생기지 않았습니다. 바이브 코딩에 대해 알게 되었을때, 드디어 이 미련을 해소할 시점이 되었다는 것을 느꼈습니다. 결론적으로 바이브 코딩을 통해 UI는 다음과 같이 변화되었습니다.
기존 UI
변경된 UI
최종적으로 구현된 코드는 깃허브에서 확인할 수 있으며, 해당 페이지에서 직접 플레이 해볼 수도 있습니다.
보통 바이브 코딩을 비롯한 LLM 주도 개발의 경우 신규 프로젝트를 처음부터 LLM 과 함께 구축해나가는 상황을 전제로 할때가 많은 것 같습니다. 하지만 이미 수많은 프로젝트들은 인력(?)으로 만들어졌으며 이제 곧 LLM을 맞이해야할 운명입니다. 본 포스트에서는 레거시 프로젝트를 LLM 과 함께 개선해나간 경험을 공유해보려 합니다.
작업 내역
이번에 토이 프로젝트를 심폐소생술하면서 작업한 내역을 요약하면 다음과 같습니다.
- langchain, openai 라이브러리 버전 최신화
- 상태관리 라이브러리 zustand 도입 및 리팩토링
- 게임 feature 추가 ( 플레이 언어 선택 )
- UI 전면 개편
- styled-component 제거
- typescript 마이그레이션
각각의 작업 하나하나가 직접 해야한다면 손댈 엄두가 나지 않는 대수술이라는 것은 분명한 것 같습니다. 그러나 바이브 코딩을 통해 이 모든 작업을 수행하는데 퇴근 후 시간을 활용하여 총 6일밖에 걸리지 않았습니다.
하나씩 살펴보겠습니다.
라이브러리 버전 최신화
지난 2년 간 급속도로 진행된 LLM 의 진화와 함께 langchain과 openai 라이브러리에도 여러 변경사항이 존재했습니다. 그 중 제가 특히 집중한 것은 OutputParser 에 대한 변경점이었습니다. 2년 전에는 LLM이 일정한 형식으로 Output 을 내도록 하는 것이 상당히 어려웠습니다. 시스템 메시지로 Parser라는 역할을 부여하고, one-shot 기반으로 일정한 Output 을 유도해야 했습니다. 심지어 OutputFixingParser 라는 것도 존재해서, 일부 형식이 틀어지게 응답된 것을 다시 고쳐서 파싱하는 단계도 필요했습니다.
지금은 모델 자체가 structured output 을 지원하기 때문에 위와 같은 구조는 불필요한 요청을 발생시키는 오버 엔지니어링이 되었습니다. LLM을 사용하는 어플리케이션에서 요청을 최대한 효율적으로 하는 것은 비용과 직결되기 때문에 최우선으로 이 작업을 진행했습니다.
아쉽게도 단순히 ‘해당 라이브러리를 최신화 시켜줘’ 라는 바이브 만으로는 원하는대로 버전 업그레이드가 진행되지 않았습니다. 패키지 버전은 잘 최신화하였지만, 기존 OutputParser 기반의 코드 구조를 계속 참조하면서 최신 방식으로 코드를 개선하지는 못했습니다. 하지만 필요한 공식문서의 링크를 지시문에 함께 제공하자 곧잘 해내는 모습을 볼 수 있었습니다. 라이브러리 버전 업그레이드는 어플리케이션의 동작 자체와는 무관한 엔지니어링 작업이라고도 볼 수 있습니다. 이러한 기술적인 개선 작업에 대해서는 뚜렷한 목표를 기반으로 구체적인 지시문이 필요하다는 사실을 알 수 있습니다. 또한 앞으로 LLM과 함께 개발하기 위해서는 문서화가 더욱 중요해지겠다는 생각도 들었습니다.
typescript 마이그레이션
결론부터 이야기하자면 저는 바이브 코딩을 위해서라면 javascript보다는 typescript를 강력히 추천합니다. 왜냐하면 type 은 LLM에게 구체적인 명세를 제공할 뿐만 아니라, 사람이 코드를 이해하는 속도도 월등히 빠르게 만들어 주기 때문입니다. 즉, type이 있는것이 LLM과 사람 모두에게 확실히 유리합니다.
본 프로젝트는 본래 javascript로 작성되었는데, 바이브 코딩을 진행하면서 몇가지 한계를 느꼈습니다.
첫째로, javascript의 경우 LLM이 여러 곳에서 재사용되는 공통 메서드를 잘 다루지 못한다는 점입니다. 예를 들어 foo 라는 메서드가 A, B라는 각각의 컴포넌트에서 공통적으로 사용될때, LLM을 통해 A 에서 foo 를 활용하는 방식을 약간 수정하면 B에서는 에러가 나는 상황이 종종 발생했습니다.
물론 이는 foo 라는 메서드가 지나치게 많은 역할을 수행하고 있기 때문일 수도 있고, 충분한 맥락을 부여하지 않은 채 지시한 잘못일 수도 있겠습니다. 다만, 이러한 현상이 typescript 로 마이그레이션 한 후에는 확연히 줄었습니다. LLM agent 는 코드를 작성하면서 중간중간 type check(컴파일 에러 확인)를 수행하는 것으로 보였습니다. type 은 입력값과 출력값에 대한 정확한 명세이며 이것이 LLM이 작업을 올바로 수행하는데 필요한 컨텍스트를 제공한다고 볼 수 있습니다.
둘째로, javascript의 경우 LLM이 작성한 코드를 검토하는 것이 괴롭다는 점입니다. type 이 없으니 어떤 값이 어떤 형식으로 들어와 어떻게 변하는지 파악하는 것이 매우 짜증나는 일이 되었습니다. 스스로 코드를 작성할 때는 javascript의 유연성이 효율을 증가시키는 면이 있었지만 (그마저도 복잡성이 작을때이지만) , 남이 짠 코드를 보기에는 참으로 적절하지 않았습니다.
typescript로 마이그레이션 한 이후에는 애초에 예측하지 못할 만큼의 변화도 적었을 뿐더러 type 만 확인하더라도 입력값과 출력값이 무엇인지 빠르게 인지할 수 있었습니다.
마이그레이션 팁을 남기자면, core 한 영역부터 점진적으로 진행하라는 것입니다. 공통적으로 사용되는 영역부터 type 을 정의했을때 훨씬 수월하게 마이그레이션이 진행되었습니다. 그렇지 않은 경우, 위에서 이야기한 첫번째 문제 상황이 종종 발생했기 때문입니다.
저의 경우 LLM과 함께 전체 코드를 typescript 로 마이그레이션 하는데 한시간 정도밖에 걸리지 않았습니다.(물론 큰 프로젝트는 아닙니다) 기존에 진행하고 있던 javascript 프로젝트가 있다면 LLM과 함께 마이그레이션을 적극적으로 검토해보시길 추천합니다.
zustand 도입 및 리팩토링
기존에는 리액트에서 기본적으로 제공하는 context api와 상태 관리만을 활용하여 코드를 작성했습니다. 저는 기본적으로 최소한의 dependency 만을 가지고 작업하는 것을 좋아하지만, zustand 는 context api 보다 훨씬 직관적인 방식으로 전역 상태를 관리할 수 있게 해주기 때문에 최근 토이 프로젝트들에 적극적으로 도입하고 있습니다.
이번 심폐소생술 간에도 마찬가지로 zustand 를 도입하였고, 그에 따른 개발 경험은 매우 좋았습니다. 특히 context api 에서 zustand 로 전환하는 과정은 LLM 을 통해 아주 손쉽게 진행되었습니다.
zustand 의 직관적인 동작 방식은 개발자가 더 깨끗한 코드, 더 좋은 구조를 가진 코드를 작성하는데 도움을 줍니다. 재미있는 점은 LLM도 잘 설계된 코드를 더 좋아한다는 것입니다.
TDD 에서 테스트를 작성하기 어려운 코드는 곧 잘못 설계된 코드임을 암시합니다. 저는 바이브 코딩에서도 마찬가지 논리가 적용된다는 것을 발견했습니다. 역할과 책임이 제대로 분리되지 않은 코드 위에서는 바이브 코딩이 제대로 진행되지가 않습니다. 무엇 하나를 고치려고 해도 LLM은 한참을 해매다가 엉뚱한 코드를 짜버립니다. 즉, 바이브 코딩이 제대로 되지 않는다는 것은 코드가 잘못 설계되었음을 암시합니다.
따라서 LLM과 오랫동안 바이브하기 위해서는 적극적으로 리팩토링을 함께 진행하여야 합니다. 리팩토링을 통해 코드를 적절한 곳에 위치시키고, 명확한 역할과 책임을 부여할 때 LLM 은 그 위에서 놀라운 퍼포먼스를 보여줍니다.
UI 개편 + styled-component 제거
UI 개편은 전적으로 바이브했습니다. 저는 백엔드 개발자이고 UI 작업과 디자인은 그리 잘하지도 선호하지도 않습니다. 바이브 코딩이 빛나는 지점이 바로 이러한 영역이지 않을까 합니다. 그토록 염원했던 UI 개선이 그저 몇 문장의 명령만으로 순식간에 진행되는 모습을 보는 것은 정말로 힐링에 가까웠습니다. 특히 디자인에 대해서는 저의 기대와 의도를 한참 웃도는 결과를 보여줬습니다. 색상 스킴과 트랜지션 효과, 컴포넌트 간의 간격과 배치 모두 상상 이상의 결과물을 보여줬습니다.
그 과정 중 styled-component 는 제거하는 방향으로 진행하였습니다. 직접 디자인 요소를 건드려야 할때는 styled-component 가 하나의 컴포넌트 파일 안에서 스타일과 로직을 응집도있게 다룰 수 있어서 선호했지만, 디자인을 전적으로 LLM에게 맡기는 상황에서는 컴포넌트의 상태 기반 랜더링 로직과 css를 통한 스타일 정보가 하나의 파일에 장황하게 작성되어 컨택스트가 불필요하게 복잡해졌습니다. 마찬가지로 tailwind 또한 굳이 컴포넌트 코드를 난잡하게 만들거라 보았기 때문에 고려하지 않았습니다.
결론적으로 순수한 css 로 전환하도록 하였는데, 전역적인 디자인 요소를 관리하기도 좋고 디자인에 대한 책임도 한 곳으로 모였기 때문에 아주 만족스러웠습니다. 신경쓰고 싶지 않은 것들이 신경쓰이지 않게 되었습니다.
결론
제가 진행한 이번 심폐소생술은 엄밀한 의미의 ‘바이브 코딩’은 아닙니다. ‘바이브 코딩’ 은 코드가 존재한다는 사실조차 잊어버리고 LLM에게 모든 흐름을 맡기는 것을 의미하기 때문입니다.
그러나 LLM과 즐겁고 지속적으로 프로젝트를 만들어 나가기 위해서는 온전히 바이브 하기 보다는 어느 정도 코드에 신경을 쓸 필요가 있습니다. ‘바이브’ 하는 것이 너무 매몰될 필요는 없겠죠. 오히려 정성을 다해 만들던 장난감이 어느 순간 고장나버려서 더 이상 가지고 놀 수 없게 되는 것이 더욱 스트레스 받지 않을까요?
바이브 코딩 이라는 용어가 처음에는 거부감이 들었지만, 직접 해보니 확실히 즐거웠습니다. 토이 프로젝트를 진행하는데 스트레스 요소를 확실히 줄여주고, 만드는 것에 더욱 집중할 수 있게 해주었습니다.
그동안 미련으로 남았던 프로젝트를 정리하니 매우 상쾌한 기분이 듭니다. 많은 개발자분들이 바이브 코딩을 통해 그동안 묶혀두었던 아이디어들을 실현하는 즐거움을 느끼셨으면 좋겠습니다. 끝으로 좋은 사이드 프로젝트가 주는 선 이라는 글의 링크를 남기며 글을 마칩니다.
댓글
* 작성 이후에 수정/삭제 할 수 없어요!
아직 작성된 댓글이 없습니다.