|
| 1 | +--- |
| 2 | +title: "보이스카웃 규칙, 책에서 어디선가 본 적은 있긴 한데,," |
| 3 | +categories: [협업] |
| 4 | +tags: [협업, 컨벤션, 장인정신] |
| 5 | +image: |
| 6 | + path: ../assets/img/posting-images/20260426/20260426_thumbnail.png |
| 7 | + alt: 보이스카웃 해본 적은 없는데..(?) 뭐 하튼 그런 규칙이 있다고 합니다~ 👮♂️🏕️ |
| 8 | + width: 2048 |
| 9 | + height: 2048 |
| 10 | +--- |
| 11 | + |
| 12 | +며칠 전, 회사에서 팀장님과 PR 리뷰를 진행하다가, 팀장님께서 내게 물으셨다. |
| 13 | + |
| 14 | +*"추천해드린 Clean Code 책에 보면 '보이스카웃 규칙'이라고 나오는데, 살펴본 적 있어요?"* |
| 15 | + |
| 16 | +책에서 본 것 같긴 했다. 분명 어딘가 페이지를 넘기다 마주친 단어였다. 그런데 그게 정확히 어떤 내용이었는지, 그 자리에서 답을 내놓을 수가 없었다. 더듬더듬 비슷한 말을 꺼내보긴 했는데, 정확한 정의도 아니었고 내 것도 아니었다. |
| 17 | + |
| 18 | +당황스러우면서 솔직히 부끄러웠다. 열심히 하는 모습만 보여드리고 싶었는데, 그러지 못한 게 그날 내내 마음에 걸렸다. 그런데 신기하게도 그 부끄러움은 며칠이 지나도 사라지지 않고, 오히려 그 키워드를 머릿속에 못박아두는 역할을 했다. 이 글은 그 키워드가 왜 내게 그렇게 들러붙었는지, 그리고 그게 어떤 깨달음으로 이어졌는지에 대한 기록이다. |
| 19 | + |
| 20 | +## **보이스카웃 규칙이 뭔지** |
| 21 | + |
| 22 | +원래 보이스카웃에는 이런 격언이 있다고 한다. *"캠프장은 처음 왔을 때보다 깨끗하게 하고 떠나라."* Robert C. Martin은 이걸 코드에 적용했다. 내가 작성하지 않은 코드라도, 내가 손댄 김에 조금이라도 더 낫게 만들고 떠나라는 것. 변수명 하나라도 더 명확하게, 죽은 코드 한 줄이라도 지우고, 흐름이 꼬인 부분이 있으면 살짝이라도 펴주고. |
| 23 | + |
| 24 | +말로 들으면 당연한 얘기다. 그래서 책에서 봤을 때도 "그렇지" 하고 넘겼던 것 같다. 진짜로 이 규칙이 살아 움직이기 시작한 건, 며칠 뒤 내가 짠 코드를 다시 마주한 순간부터였다. |
| 25 | + |
| 26 | +## **2달 전 내 코드를 살펴보니... 심각하다** |
| 27 | + |
| 28 | +그날 나는 2달 전쯤 작성해둔 코드를 다시 열어봐야 했다. 기능을 조금 손봐야 하는 상황이었다. |
| 29 | + |
| 30 | +코드를 읽기 시작하자마자 거부감이 올라왔다. 내가 짠 코드인데, 내가 거부감이 들었다. |
| 31 | + |
| 32 | +뭐가 거슬렸는지 하나하나 떠올려보면 이렇다. |
| 33 | + |
| 34 | +변수명과 함수명이 의미 불명이었다. `data`, `result`, `tmp` 같은 이름들이 곳곳에 흩어져 있어서, 이 변수가 뭘 담고 있는 건지 위로 올라가서 다시 읽어야 했다. 함수 하나가 너무 길어서 흐름을 한 번에 따라가기 힘들었다. 무슨 의도로 짠 건지 주석이라도 있으면 좋았을 텐데, 정작 필요한 곳엔 주석이 없었고 엉뚱한 곳에 옛날 주석이 남아있었다. 팀에서 정한 컨벤션에서 벗어난 부분도 곳곳에 섞여 있었다. 어디는 따르고 어디는 안 따르고. 거기에 더해 쓰이지 않는 함수와 주석 처리된 코드 블록이 그대로 방치되어 있었다. |
| 35 | + |
| 36 | +그게 누가 짠 코드도 아니고 2달 전의 나였다. 책에서 보이스카웃 규칙이 "남이 더럽혀놓은 캠프장을 청소하는 이야기"인 줄 알았는데, 정작 마주한 건 **내가 더럽혀놓은 캠프장**이었다. |
| 37 | + |
| 38 | +이 장면 위에 팀장님이 던지셨던 키워드가 겹쳐졌다. 그제야 그 개념이 머릿속에서 살아 움직이기 시작했다. 책으로 봤을 때는 "당연한 말"이었던 게, 내 코드를 보고 나니 "절실한 말"이 되어 있었다. |
| 39 | + |
| 40 | +## **왜 그렇게 짰냐 기철아,,👊** |
| 41 | + |
| 42 | +거부감보다 더 마음에 걸렸던 건, 왜 내가 그렇게 짰는지를 돌아봤을 때 나오는 답이었다. |
| 43 | + |
| 44 | +이유는 솔직히 명확했다. |
| 45 | + |
| 46 | +첫째, Django에 대한 기초적인 이해가 부족했다. ORM이 제공하는 도구를 충분히 알지 못한 채로 짜다 보니, 우회하는 코드가 많이 나왔다. |
| 47 | + |
| 48 | +둘째, 일정이 급했다. "일단 돌아가게" 만드는 게 급선무였고, 다듬는 건 나중 일이었다. 그리고 그 "나중"은 오지 않았다. |
| 49 | + |
| 50 | +셋째, 이게 가장 마음에 걸리는 부분인데, Claude Code의 플랜 모드만으로 거의 블랙박스처럼 구현을 맡겨버렸다. 빠르게 결과물이 나오니까 검토 없이 흘려보냈다. 그런데 Claude Code가 참고한 건 팀 컨벤션을 따르지 않은 채 남아있던 레거시 코드였다. 결과적으로 컨벤션이 더 흐트러진 코드가 생성됐고, 그게 그대로 머지됐다. |
| 51 | + |
| 52 | +빠르게 짜준 도구를 탓하려는 게 아니다. 도구는 도구일 뿐이고, 결국 캠프장을 떠날 때 한 번 더 둘러보지 않은 건 나였다. 다만 이 경험에서 한 가지는 분명해졌다. |
| 53 | + |
| 54 | +**속도와 청소는 별개의 문제다.** AI 도구는 짜는 속도를 줄여준다. 그런데 청소까지 자동으로 해주진 않는다. 오히려 빠르게 짜는 만큼 캠프장이 더러워지는 속도도 빨라진다. 게다가 AI가 참고하는 게 이미 더러운 캠프장이라면, 결과물은 그 더러움을 학습해서 한 번 더 더럽힌다. |
| 55 | + |
| 56 | +생각해보면 AI 시대에는 보이스카웃 규칙이 더 중요해진 게 아닐까. 더러워지는 속도가 빨라진 만큼, 의식적으로 청소하지 않으면 캠프장은 손쓸 수 없게 망가진다. |
| 57 | + |
| 58 | +### 코드로 보면 이런 거다 (Python/Django) |
| 59 | + |
| 60 | +가상의 예로 사용자 목록을 가져오는 코드를 보자. 2달 전의 나라면 이렇게 짰을 법한 모습이다. |
| 61 | + |
| 62 | +```python |
| 63 | +def get_data(req): |
| 64 | + # 활성 유저 가져오기 |
| 65 | + tmp = User.objects.all() |
| 66 | + result = [] |
| 67 | + for u in tmp: |
| 68 | + if u.is_active == True: |
| 69 | + result.append(u) |
| 70 | + # data = User.objects.filter(is_active=True) # 옛날 방식 |
| 71 | + |
| 72 | + final = [] |
| 73 | + for r in result: |
| 74 | + final.append({ |
| 75 | + 'id': r.id, |
| 76 | + 'name': r.name, |
| 77 | + }) |
| 78 | + return JsonResponse({'data': final}) |
| 79 | +``` |
| 80 | + |
| 81 | +`tmp`, `result`, `final`, `data` — 다 무슨 뜻인지 모르겠는 이름들이다. `is_active == True` 같은 불필요한 비교, 주석 처리된 죽은 코드, Django ORM이 제공하는 `filter`와 `values`를 두고 Python에서 직접 도는 비효율까지. 짧은 함수인데도 다 거슬린다. |
| 82 | + |
| 83 | +캠프장을 청소하고 떠나면 이렇게 된다. |
| 84 | + |
| 85 | +```python |
| 86 | +def get_active_users(request): |
| 87 | + users = User.objects.filter(is_active=True).values('id', 'name') |
| 88 | + return JsonResponse({'data': list(users)}) |
| 89 | +``` |
| 90 | + |
| 91 | +거창한 리팩터링이 아니다. 함수명을 의도가 드러나게 바꾸고, ORM의 도구를 쓰고, 죽은 코드를 지웠을 뿐이다. 그런데 다음에 이 코드를 열어볼 누군가는(아마도 미래의 나는) 거부감 없이 읽을 수 있다. |
| 92 | + |
| 93 | +### 프론트엔드도 마찬가지 (JS/React) |
| 94 | + |
| 95 | +상품 검색 컴포넌트를 가정해보자. 정리되지 않은 버전이다. |
| 96 | + |
| 97 | +```jsx |
| 98 | +import React, { useState, useEffect } from 'react'; |
| 99 | +import axios from 'axios'; |
| 100 | +import _ from 'lodash'; // 사용 안 함 |
| 101 | + |
| 102 | +function Comp({ d, fn }) { |
| 103 | + const [data, setData] = useState([]); |
| 104 | + const [tmp, setTmp] = useState(''); |
| 105 | + |
| 106 | + useEffect(() => { |
| 107 | + axios.get('/api/products?q=' + tmp).then(r => { |
| 108 | + setData(r.data); |
| 109 | + }); |
| 110 | + }, [tmp]); |
| 111 | + |
| 112 | + // const handleOld = () => { ... } |
| 113 | + |
| 114 | + return ( |
| 115 | + <div> |
| 116 | + <input value={tmp} onChange={e => setTmp(e.target.value)} /> |
| 117 | + {data.map(x => <div key={x.id} onClick={() => fn(x)}>{x.n}</div>)} |
| 118 | + </div> |
| 119 | + ); |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +`Comp`, `d`, `fn`, `tmp`, `x`, `n` — 컴포넌트 이름과 prop 이름부터 의미를 알 수 없다. 안 쓰이는 import, 주석 처리된 핸들러, 검색어가 바뀔 때마다 디바운스 없이 호출되는 API까지. 한 번 보고 흐름을 이해하기가 쉽지 않다. |
| 124 | + |
| 125 | +지나가는 김에 조금만 청소하면 이렇게 된다. |
| 126 | + |
| 127 | +```jsx |
| 128 | +import React, { useState, useEffect } from 'react'; |
| 129 | +import axios from 'axios'; |
| 130 | + |
| 131 | +function ProductSearch({ onSelect }) { |
| 132 | + const [products, setProducts] = useState([]); |
| 133 | + const [keyword, setKeyword] = useState(''); |
| 134 | + |
| 135 | + useEffect(() => { |
| 136 | + if (!keyword) return; |
| 137 | + const timer = setTimeout(() => { |
| 138 | + axios.get(`/api/products?q=${keyword}`).then(res => setProducts(res.data)); |
| 139 | + }, 300); |
| 140 | + return () => clearTimeout(timer); |
| 141 | + }, [keyword]); |
| 142 | + |
| 143 | + return ( |
| 144 | + <div> |
| 145 | + <input value={keyword} onChange={e => setKeyword(e.target.value)} /> |
| 146 | + {products.map(product => ( |
| 147 | + <div key={product.id} onClick={() => onSelect(product)}> |
| 148 | + {product.name} |
| 149 | + </div> |
| 150 | + ))} |
| 151 | + </div> |
| 152 | + ); |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +언어와 프레임워크가 달라도 캠프장 청소의 원리는 같다. 의미 있는 이름, 죽은 코드 제거, 명백한 버그성 패턴(디바운스 누락) 보완. 작업하러 들어온 김에 눈에 보이는 만큼만 정리하는 것. |
| 157 | + |
| 158 | +### 5S, TPM — 같은 이야기를 다른 분야에서 하고 있더라 |
| 159 | + |
| 160 | +이 깨달음을 곱씹다 보니, 비슷한 개념을 다른 분야에서 본 적이 있다는 게 떠올랐다. |
| 161 | + |
| 162 | +제조업에는 **5S**라는 개념이 있다. 정리(필요 없는 것 버리기), 정돈(필요한 걸 찾기 쉽게), 청소, 청결(앞의 세 단계를 표준화하기), 생활화(습관으로 만들기). 그리고 그 위에 **TPM(전사적 생산보전)** 이라는 더 큰 틀이 있다. 핵심은 단순하다. *작업 환경은 만들어지는 게 아니라 유지되는 것이다.* |
| 163 | + |
| 164 | +보이스카웃 규칙은 결국 소프트웨어 버전의 5S였다. 분야가 전혀 다른데 결론이 같다는 건, 이게 직군을 넘는 보편 원리에 가깝다는 뜻 같다. 좋은 환경은 만들어내는 게 아니라, 매일 조금씩 지켜내는 것이라는 원리. |
| 165 | + |
| 166 | +### 다만 과하면 독이 된다 |
| 167 | + |
| 168 | +물론 균형 감각도 필요하다. |
| 169 | + |
| 170 | +모든 PR마다 캠프장 전체를 청소하려 들면 PR 범위가 폭발한다. 원래 작업과 무관한 변경이 많아지면 리뷰어가 힘들어지고, 변경 의도가 흐려지고, 코드 리뷰는 본질에서 멀어진다. "이번 PR이 뭘 바꾸려는 거였는지" 자체가 안 보이게 된다. |
| 171 | + |
| 172 | +그래서 보이스카웃 규칙은 *완벽주의*가 아니라 *생활화*에 가까운 개념이라고 생각한다. 지나가는 김에 눈에 들어오는 만큼만 깔끔하게. 한 번에 캠프장 전체를 갈아엎는 게 아니라, 매번 조금씩 더 나은 상태로 떠나는 것. 5S에서 마지막 항목이 "생활화"인 것도 같은 맥락일 거다. |
| 173 | + |
| 174 | +## **이제는 나도 보이스카웃 규칙이 뭔지 몸으로 깨달았다.** |
| 175 | + |
| 176 | +팀장님이 다시 물어봐주신다면, 이제는 답할 수 있을 것 같다. |
| 177 | + |
| 178 | +다만 책에서 본 정의를 외워서가 아니다. 2달 전 내 코드를 마주한 그날의 거부감이 답이 됐기 때문이다. 책의 한 페이지로 머물던 개념이, 내 키보드 앞에서의 감각으로 바뀌었다. |
| 179 | + |
| 180 | +소프트웨어 개발주기의 80%는 유지보수라고들 한다. 그 80%를 좌우하는 건 거창한 아키텍처나 화려한 신기술이 아니라, *지나갈 때마다 조금씩 청소하는 생활화* 일지도 모른다. 코드를 빠르게 짜주는 도구는 점점 늘어나고 있지만, 캠프장을 떠날 때 한 번 더 둘러보는 건 여전히 사람의 몫이다. |
| 181 | + |
| 182 | +부끄러웠던 그날의 질문이, 결국 가장 오래 남는 배움이 됐다. |
0 commit comments