회고알고수 블로그 관계 그래프·검색 기능의 안전한 삭제
· 알고수
- #deletion
- #yagni
- #dead-code
- #refactoring
코드를 추가할 때는 결과가 눈에 보입니다. 컴포넌트가 화면에 나타나고, 테스트가 통과하고, 빌드가 성공하면 됐다는 걸 압니다.
삭제는 다릅니다. 파일을 지우고 tsc --noEmit(타입 검사)을 돌려 에러를 고칩니다. 빌드가 됩니다. PR이 머지됩니다. 그리고 스프린트를 닫을 때에야 CI가 깨져 있던 걸 압니다. 지운 파일을 참조하는 스크립트가 저장소 루트에 있었는데, grep을 하위 폴더 안에서만 돌려서 보지 못한 거죠.
알고수에서 저는 블로그 기능을 두 번 지웠습니다. 알고수는 스프린트(짧은 작업 주기)를 수백 번 돌렸고, 아래 두 번의 삭제는 그중 190번째를 넘긴 무렵의 일입니다.
ADR 관계 그래프
결정 기록끼리 서로 어떻게 이어지는지 그림(mermaid)으로 보여주던 기능입니다. 전용 그래프 페이지와 상세 페이지 옆의 작은 그래프가 있었어요. 189번째 스프린트에 만들어 193번째 스프린트에 지웠습니다.
ADR 검색
블로그 머리글에서 결정 기록의 제목·본문을 찾아 주던 검색창입니다. 빌드 때 검색용 색인 파일(
search-index.json)을 만들어 두고 브라우저에서 찾는 방식이었어요. 157번째 스프린트에 만들어 201번째 스프린트에 지웠습니다.
두 번 모두 "YAGNI(필요해질 때까지 만들지 마라)"나 "기능 설계 실수"보다 더 실용적인 걸 배웠어요. 안전하게 지우는 방법이었습니다.
문제
지울 때마다 무엇이 따라 깨질지 모른다. 기능 삭제에도 방법이 있는가?
결정
삭제는 세 단계로 끝낸다. 착수 전 grep으로 분류하기, 죽은 필드를 함께 지우기, 저장소 전체에서 연결 찾기.
결과
그래프를 지울 때는 세 원칙을 모두 지켰다. 검색을 지울 때는 세 번째 원칙을 어겼다. 두 사례의 차이가 원칙의 가치를 보여준다.
원칙 1: grep으로 나누기
기능을 지우기 전에 할 첫 번째 일은 그 기능만 쓰는 것과 다른 곳에서도 쓰는 것을 grep으로 가르는 것입니다. 이 분류를 잘못하면 삭제가 옆 기능을 조용히 깨뜨립니다.
검색을 지울 때는 착수 전에 화면 문구의 번역 키(i18n 키: 한국어·영어 문구를 찾아 쓰는 이름표)부터 grep으로 나눴습니다.
검색 전용 (지워도 안전)
searchPlaceholder, searchEmpty 등. 검색창 컴포넌트(SearchBox)에서만 쓰임
공유 (반드시 유지)
kindPermanent, metaSprint 등. ADR 카드·카테고리 탭·스프린트 타임라인·사이드바에서도 쓰임
함수도 마찬가지였습니다. 검색용 문서를 만드는 toSearchDoc 같은 함수와 SearchDoc 타입은 검색 전용이라 지웠습니다. 반면 buildUrl, groupByKind 같은 함수는 ADR 페이지를 그리는 데 계속 쓰여서 남겼어요.
그래프를 지울 때도 같은 분류를 먼저 했습니다. buildGraph, getSubgraph 같은 그래프 계산 함수는 그래프 전용이라 지웠습니다. 하지만 그래프 배경색·격자 CSS 토큰(--diagram-bg, --grid)은 블로그 본문 다이어그램에서도 쓸 수 있고, 남겨도 해가 없어 그대로 뒀어요. 사이드바의 "관련 ADR" 텍스트 링크(RelatedLinks)도 그래프와 완전히 별개인 컴포넌트라 그대로 뒀습니다.
핵심은 이 분류를 코드를 지우기 전에 한다는 겁니다. 지우기 시작하면 관성이 붙어서 "이것도 아마 그래프 전용이겠지"라는 추측이 끼어들어요. 착수 전에 grep을 돌리면 추측이 아니라 사실로 경계를 그을 수 있습니다.
원칙 2: 죽은 필드도 삭제
기능을 지우다 보면 종종 발견하는 게 있습니다. 기능이 살아 있을 때 만들어졌지만 아무도 읽지 않던 필드들이에요.
검색을 지울 때 AdrIndex.searchDocs 필드가 그랬습니다. ADR 목록을 만드는 함수가 빌드 때마다 이 필드를 채웠지만, 실제로 읽는 곳은 한 군데도 없었어요. ADR 목록·아카이브·글 페이지는 모두 다른 필드만 읽었습니다. 사실상 죽은 필드였고, 검색을 지울 때 함께 정리했습니다.
그래프를 지울 때의 AdrDoc.outgoingLinks도 같았습니다. 문서 사이의 링크를 모아 두는 필드였는데, 그래프를 그리는 함수에서만 읽혔어요. 그래프가 사라지면 이 필드도 의미가 없습니다. 이 필드를 채우던 추출 함수(extractOutgoingLinks)와 그 함수만 쓰던 정규식(ADR_LINK_RE)까지 함께 지웠습니다.
죽은 필드를 남겨 둬도 당장은 아무 일도 없습니다. 하지만 몇 달 뒤 코드를 다시 읽을 때 "이 필드는 왜 여기 있지?"라는 의문이 생겨요. 죽은 코드는 맥락을 흐립니다. 기능을 지울 때가 함께 정리하기 가장 자연스러운 때입니다.
원칙 3: 범위 밖의 파손
가장 중요한 원칙이고, 직접 겪기 전에는 잘 믿기지 않는 원칙입니다.
검색을 지운 첫 PR은 blog/ 폴더 안에서 끝나는 작업처럼 보였습니다. 검색창 컴포넌트, 색인 파일을 만드는 스크립트(generate-search-index.mjs), 검색용 타입과 필드, 검색 라이브러리(minisearch)가 전부 blog/ 아래에 있었으니까요. grep도 blog/ 안에서만 돌렸습니다. 타입 에러 0, next build 성공, CI 대기.
PR이 머지됐습니다.
그런데 main이 깨진 상태였습니다.
저장소 루트의 scripts/check-adr-links.mjs는 ADR 블로그의 링크가 깨지지 않았는지 검사하는 스크립트입니다. 그런데 이 스크립트가 빌드 결과물에 search-index.json이 있는지도 확인하고 있었어요. 색인 파일이 더는 만들어지지 않으니 검사가 실패(exit 2)했고, 블로그를 정적 페이지로 빌드하는 CI 작업(Build Blog (SSG))이 실제로 실패했습니다.
그런데도 머지가 됐습니다. GitHub의 브랜치 보호 설정에는 "이 검사가 통과해야만 머지할 수 있다"고 지정하는 필수 검사(required check)가 있는데, 이 작업이 거기에 들어 있지 않았거든요. 그래서 실패해도 머지(squash merge)를 막지 못했습니다.
이걸 잡은 건 스프린트를 끝내는 명령(/stop)의 검사 단계였습니다. 스프린트 종료 때 로컬에서 check-adr-links.mjs를 직접 실행하는데, 거기서 문제가 드러났어요. 결국 후속 PR을 따로 올려 check-adr-links.mjs에서 색인 파일 확인 부분을 빼고, 링크 검사만 남겼습니다.
교훈은 이겁니다. 지운 파일을 참조하는 코드는 그 파일이 있던 폴더 밖에 있을 수 있습니다. search-index.json을 만드는 스크립트는 blog/scripts/에 있었지만, 그 파일이 있는지 검사하는 스크립트는 루트 scripts/에 있었어요. grep 범위를 blog/로 좁히면 이 연결이 보이지 않습니다.
기능을 지울 때 grep 범위는 저장소 전체여야 합니다. 이름 하나로 git grep을 돌리면 저장소 어디에 있는 참조든 찾을 수 있어요. "아마 이 폴더 안에만 있겠지"는 확인이 아니라 가정입니다.
삭제 대상 밖에서 깨진 경로
설계로서의 삭제
그래프를 지울 때는 결과가 깔끔했습니다. 원칙 1의 예시는 검색 쪽 번역 키였는데, 그래프를 지울 때도 번역 키 44개(한국어·영어 각 22개)를 전용과 공유로 나누고, 죽은 필드 outgoingLinks를 추출 함수·정규식과 함께 지우고, 남은 참조가 없는지 저장소 전체를 grep으로 확인했습니다. 타입 에러 0, 빌드 성공, 리뷰 에이전트(Critic)가 찾은 문제 0건, CI 37개 통과 / 0개 실패.
그 뒤 검색을 지울 때는, 두 번째 원칙은 지켰지만(searchDocs 제거), 세 번째 원칙을 어겼습니다. grep을 blog/로 좁혔고, 루트 검사 스크립트가 기대던 파일이 보이지 않았어요. main이 깨진 채 머지됐고, /stop의 검사가 잡아냈고, 후속 PR로 메웠습니다.
두 사례의 차이가 원칙의 가치를 보여줍니다.
코드를 추가할 때 "이게 필요한가"를 물어야 한다면, 코드를 지울 때는 "이게 어디서 쓰이는가"를 물어야 합니다. 기능을 지운다는 건 그 기능이 혼자 떨어져 있지 않았다는 걸 확인하는 과정이에요. grep 결과가 그려 주는 의존 관계를 따라가다 보면, 그 기능만의 코드와 다른 곳에 발이 걸린 코드의 경계가 드러납니다.
그 경계를 정확히 따라 지운다. 그게 삭제의 기술이에요.
세 원칙
지우기 전에 grep으로 나눈다
그 기능만 쓰는 것과 함께 쓰는 것을 구분하고, 함께 쓰는 것은 건드리지 않는다.
죽은 필드는 기능과 함께 죽인다
그 기능에서만 쓰이던 필드·타입·파서를 함께 정리해 죽은 코드를 남기지 않는다.
grep 범위는 저장소 전체다
지운 파일을 참조하는 코드는 하위 폴더 밖에 있을 수 있다.
git grep으로 저장소 전체를 본다.