입사 2년차 때 일이다. 지금 생각하면 그때가 제일 위험한 시기였다. 뭔가 알 것 같은데 실제로는 잘 모르는, 그 어중간한 구간. 코드를 짜는 속도는 빨라졌고, PR을 올리면 대부분 한두 번의 리뷰로 머지됐다. "나 꽤 잘하는 거 아닌가?"라는 생각이 은연중에 있었다.
그러다 그 PR을 올렸다.
500줄짜리 자신감
당시 맡은 작업은 대시보드 페이지의 필터 시스템이었다. 사용자가 날짜 범위, 카테고리, 상태값 등을 조합해서 데이터를 필터링하는 기능. 기획서를 보면 필터 조건이 7개였고, 조합에 따라 API 호출 파라미터가 달라져야 했다.
나는 꽤 열심히 짰다. useFilter라는 커스텀 훅을 만들고, 각 필터의 상태를 하나의 객체로 관리했다. 필터 값이 바뀔 때마다 URL 쿼리 스트링과 동기화하고, 뒤로 가기를 누르면 이전 필터 상태가 복원되도록 했다. useReducer를 써서 상태 변경 로직을 깔끔하게 분리했다고 생각했다. 리듀서 안에 액션 타입이 12개였는데, 각 케이스마다 주석을 달아놨으니 "가독성도 좋다"고 자부했다.
PR은 약 500줄이었다. 길긴 했지만 파일을 잘 나눠놨다고 생각했다. useFilter.ts, filterReducer.ts, filterUtils.ts, FilterPanel.tsx, FilterChips.tsx. 이름만 봐도 뭐 하는 건지 알 수 있잖아. 나름 자랑스러운 PR이었다.
코멘트가 하나둘 달리기 시작했다. 대부분 소소한 것들이었다. 타입 네이밍 컨벤션, optional chaining 하나 빠진 곳, 테스트 케이스 추가 요청 같은. 예상 범위 안이었고, 금방 반영했다.
그런데 팀의 시니어 개발자가 코멘트를 하나 남겼다. filterReducer.ts 파일의 29번째 줄 근처, 리듀서 함수 전체를 가리키는 코멘트였다.
그 한 줄
"이 리듀서가 아니라 URL이 진실의 원천(source of truth)이 되면 어떨까요?"
끝이었다. 수정 요청이 아니었다. "이렇게 바꿔라"도 아니었다. 그냥 질문 하나.
처음에는 무슨 말인지 이해를 못 했다. URL이 source of truth? 지금도 URL이랑 동기화하고 있는데? useEffect로 필터 상태가 바뀌면 URL을 업데이트하고, URL이 바뀌면 필터 상태를 업데이트하고. 양방향으로 싱크하고 있으니까 잘 하고 있는 거 아닌가?
답변을 달려다가 멈췄다. "잘 하고 있는데요"라고 달기 전에 한 번 더 생각해보자.
URL이 source of truth가 되면. 그러니까 필터 상태를 따로 useState나 useReducer로 관리하지 않고, URL 쿼리 스트링 자체가 상태가 되면.
잠깐. 그러면 리듀서가 필요 없어진다.
머릿속이 무너지는 순간
자리에서 일어나서 커피를 타왔다. 종이에 그림을 그려봤다. 지금 내 코드의 데이터 흐름은 이랬다.
사용자 액션 → 리듀서로 상태 변경 → useEffect로 URL 업데이트 → URL 변경 감지 → 리듀서로 상태 다시 세팅 → API 호출
이게 왜 복잡하냐면, 상태가 두 곳에 있기 때문이다. 리듀서 안에도 있고 URL에도 있다. 두 곳을 계속 동기화해야 하니까 useEffect가 두 개 필요하고, 동기화 타이밍이 어긋나면 깜빡이는 현상이 생긴다. 실제로 개발하면서 이 이슈를 만났는데, setTimeout으로 대충 해결하고 넘어간 부분이 있었다.
시니어 개발자가 말한 방식은 이랬다.
사용자 액션 → URL 직접 변경 → URL에서 필터 값 파싱 → API 호출
상태가 한 곳에만 있다. 동기화할 게 없다. useEffect도, setTimeout도 필요 없다. 리듀서도 필요 없다. 그 12개의 액션 타입도, 깔끔하게 달아놓은 주석도, 전부 필요 없어진다.
500줄 중에 200줄 넘게 쳐낸다는 뜻이었다.
자존심이 상했다. 솔직히 말하면. 이틀 동안 열심히 짠 코드를 반 넘게 삭제해야 한다니. 리듀서 패턴 적용하면서 "오, 이거 구조 진짜 깔끔하다" 했는데, 그게 통째로 불필요하다니.
근데 아무리 생각해도 시니어 개발자가 맞았다. URL이 source of truth가 되면 코드가 단순해질 뿐만 아니라, 사용자 경험도 좋아진다. 필터를 적용한 상태에서 링크를 복사해서 동료에게 보내면 동일한 필터 상태가 재현된다. 새로고침해도 필터가 유지된다. 브라우저 뒤로 가기가 자연스럽게 동작한다. 전부 URL이 상태니까 당연히 되는 것들이다.
내 원래 코드에서는 이것들을 하나하나 구현해야 했다. 뒤로 가기 지원을 위해 popstate 이벤트를 리슨하고, 링크 공유를 위해 상태를 URL에 인코딩하고... 이미 URL이라는 완벽한 상태 저장소가 있는데 옆에 리듀서라는 상태 저장소를 하나 더 만들어놓고 둘을 억지로 연결하고 있었던 거다.
고치고 나서 느낀 것
PR을 다시 작성했다. useFilter 훅은 내부적으로 useSearchParams를 감싸는 얇은 래퍼가 됐다. 리듀서 파일은 삭제했다. 유틸 함수 절반도 삭제했다. 500줄이 200줄 조금 넘는 정도로 줄었다. 그리고 가장 중요한 건, setTimeout 핵이 사라졌다는 거다.
기능은 완전히 동일했다. 사실 더 나았다. 이전에는 특정 순서로 필터를 빠르게 바꾸면 상태가 꼬이는 엣지 케이스가 있었는데, URL 기반으로 바꾸니까 그게 원천적으로 불가능해졌다.
근데 기술적인 개선보다 더 중요한 게 있었다. 그 코멘트가 남긴 진짜 교훈은 "URL을 source of truth로 써라"가 아니었다.
**"너의 솔루션이 문제를 복잡하게 만들고 있는 건 아닌지 한 발 물러서 봐라"**였다.
나는 필터 기능을 구현하면서 문제를 있는 그대로 본 게 아니라, 내가 알고 있는 패턴에 끼워 맞춘 거다. 상태 관리? useReducer. 복잡한 상태 로직? 액션 타입 나누기. 이렇게 기계적으로 도구를 꺼내들었다. "이 문제에 가장 적합한 상태 관리 방식이 뭘까?"를 먼저 물어봤어야 했는데, "useReducer를 어떻게 쓸까?"부터 생각했다.
솔루션이 너무 빨리 나왔다. 문제를 충분히 들여다보기 전에.
그 후로 바뀐 것들
이 경험 이후로 내가 코드 리뷰를 하는 방식이 완전히 바뀌었다.
예전에는 눈에 보이는 걸 지적했다. 변수명이 이상하다, 이 함수는 너무 길다, 여기 타입이 빠졌다. 물론 이런 것도 필요하다. 하지만 이건 린터가 할 수 있는 수준의 리뷰다.
지금은 먼저 전체 구조를 본다. 이 PR이 풀려는 문제가 뭔지, 그리고 선택한 접근 방식이 그 문제에 적합한지를 본다. 코드의 줄 하나하나를 보기 전에 파일 구조와 데이터 흐름을 훑는다.
그리고 지적 대신 질문을 한다.
"이 상태를 여기서 관리하는 특별한 이유가 있나요?" "이 useEffect 없이도 같은 결과를 얻을 수 있을까요?" "이 추상화가 나중에 요구사항이 바뀌면 어떻게 될까요?"
지적은 방어를 부른다. "이거 틀렸어요"라고 하면 상대방은 본능적으로 "왜 그게 틀린 건데"를 생각한다. 하지만 질문은 사고를 부른다. "이게 아니라 저러면 어떨까요?"라고 하면 상대방은 "음, 그러면..." 하고 직접 생각하기 시작한다. 결론에 스스로 도달하면 훨씬 깊이 이해하게 된다.
시니어 개발자가 나한테 그렇게 한 것처럼.
그 사람은 "리듀서 삭제하고 URL 기반으로 바꾸세요"라고 할 수 있었다. 그게 더 빠르기도 하다. 하지만 그랬으면 나는 시키는 대로 고쳤을 거다. 왜 그래야 하는지 깊이 이해하지 못한 채로. 그리고 다음번에 비슷한 상황이 오면 또 같은 실수를 했을 거다.
질문 형태의 코멘트는 시간이 좀 더 걸린다. 상대방이 스스로 깨달을 때까지 기다려야 하니까. 하지만 그 깨달음은 오래 남는다. 3년이 넘었는데 아직도 PR을 올릴 때마다 스스로에게 묻는다. "지금 이 솔루션이 문제를 복잡하게 만들고 있는 건 아닌가?"
좋은 코멘트의 조건
그 이후로 코드 리뷰를 많이 하면서 나름대로 정리한 게 있다. 좋은 코멘트가 되려면 몇 가지가 필요하다.
첫째, 맥락을 이해해야 한다. 왜 이 코드가 이렇게 작성됐는지를 먼저 이해하려고 노력해야 한다. 내가 리듀서를 쓴 데는 이유가 있었다. 필터 상태가 복잡하고, 상태 변경 로직을 한곳에 모으고 싶었으니까. 시니어 개발자는 이 의도를 이해한 상태에서 더 나은 방법을 제안한 거다. 맥락 없이 "이거 왜 이렇게 짰어요?"라고 물으면 공격처럼 느껴진다.
둘째, 구체적이어야 한다. "구조가 좀 이상한데요"보다 "이 리듀서가 아니라 URL이 source of truth가 되면 어떨까요?"가 훨씬 유용하다. 방향을 제시하되 답을 주지는 않는 수준. 상대방이 생각할 여지를 남겨두면서도 막막하지 않을 만큼.
셋째, 톤이 중요하다. 같은 내용이라도 어떻게 말하냐에 따라 완전히 다르게 느껴진다. "이건 안티패턴인데요"와 "이런 식으로 하면 더 단순해질 것 같은데 어떻게 생각하세요?"는 천지 차이다. 코드 리뷰는 코드를 리뷰하는 거지 사람을 리뷰하는 게 아니다. 그런데 받는 사람 입장에서는 구분이 안 될 때가 있다.
넷째, 모든 걸 지적하지 않아야 한다. PR에 개선할 점이 10개 보여도 진짜 중요한 2-3개만 집는다. 나머지는 다음에, 또는 아예 안 하는 게 낫다. 코멘트가 20개 달린 PR은 받는 사람이 지친다. 의욕이 꺾인다. 코드 퀄리티를 위해 사람의 의욕을 갉아먹으면 장기적으로는 코드 퀄리티가 더 떨어진다.
완벽한 코드 리뷰는 없다
솔직히 말하면 나도 아직 잘 못한다. 바쁠 때는 "LGTM"으로 대충 넘기기도 하고, 가끔은 톤 조절에 실패해서 상대방이 기분 나빠한 적도 있다. 질문 형태로 코멘트를 남겼는데 의도가 전달이 안 돼서 "그래서 어떻게 하라는 건데요?"라는 답변을 받은 적도 있다.
코드 리뷰는 결국 커뮤니케이션이다. 코드를 매개로 한 대화다. 모든 대화가 그렇듯 완벽할 수 없고, 매번 다르다. 상대방의 경험 수준, 그날의 컨디션, 프로젝트의 긴급도에 따라 같은 코멘트도 다르게 작용한다.
다만 하나 확실한 건, "이걸 고쳐라"보다 "이러면 어떨까?"가 더 오래 남는다는 거다.
3년 전 그 시니어 개발자의 코멘트처럼. 한 줄이었는데, 아직도 코드를 짤 때마다 떠오른다. 가장 좋은 코드 리뷰 코멘트는 PR이 머지된 한참 후에도 효력이 남아있는 코멘트다.
누군가의 PR에 코멘트를 남길 때, 나도 그런 한 줄을 쓸 수 있으면 좋겠다고 생각한다. 매번은 무리지만, 가끔이라도.
