뒤로가기
리팩토링하지 말아야 할 때

October 19, 2025

frontendcareer

어떤 코드를 보면 손이 근질거린다. 함수가 80줄이고, 변수명이 data2이고, 같은 로직이 세 군데에 복붙되어 있다. 이런 코드를 보면 본능적으로 리팩토링하고 싶어진다. 깔끔하게 정리하고, 중복을 제거하고, 좋은 이름을 붙이고. 그게 좋은 개발자가 하는 일이라고 배웠으니까.

근데 가끔은 그 본능을 무시해야 한다.

버그 3개의 탄생#

1년 전쯤의 일이다. 상품 목록 페이지를 담당하게 됐다. 전임자가 퇴사한 지 몇 달 된 코드였고, 처음 열었을 때의 감상은 한마디로 "끔찍하다"였다.

컴포넌트 하나에 400줄이 넘었다. 상태 변수가 12개. useEffect가 6개. 비슷한 로직이 여기저기 반복되고 있었다. API 호출 로직, 필터링 로직, 페이지네이션 로직이 하나의 컴포넌트에 뒤섞여 있었다. 커스텀 훅 같은 건 없었다.

그런데 이 코드는 잘 돌아가고 있었다. 프로덕션에서 6개월 넘게 별 문제 없이 동작하고 있었다. 사용자 불만도 없었다. 성능 이슈도 없었다. 그냥 돌아가고 있었다.

나는 참지 못했다. 당장의 업무는 이 페이지에 필터 하나를 추가하는 거였다. 필터 하나. 아마 원래 코드에 끼워 넣으면 30분이면 됐을 거다. 하지만 이 코드 구조로는 필터를 추가하기가 불편하다는 이유를 대면서, 리팩토링을 먼저 하겠다고 했다.

리팩토링 계획은 거창했다. API 호출을 커스텀 훅으로 분리, 필터 로직을 별도 훅으로 분리, 페이지네이션도 분리, 컴포넌트를 작은 단위로 쪼개기. 한 3일이면 되겠다고 추산했다.

5일이 걸렸다. 그리고 배포 후 버그가 3개 나왔다.

첫 번째는 필터를 적용한 상태에서 페이지를 이동하면 필터가 초기화되는 버그. 원래 코드에서는 필터 상태와 페이지네이션 상태가 하나의 useEffect 안에서 같이 처리되고 있었다. 보기에는 지저분했지만, 그 덕에 둘의 타이밍이 자연스럽게 맞았다. 내가 분리하면서 타이밍이 어긋났다.

두 번째는 목록이 비어있을 때 빈 상태 UI가 안 나오는 버그. 원래 코드에서 이걸 처리하는 조건문이 있었는데, 리팩토링하면서 옮기다가 조건의 순서가 바뀌었다. 특정 상황에서 빈 상태 체크보다 로딩 상태 체크가 먼저 실행되면서 빈 상태가 잡히지 않았다.

세 번째는 정렬 옵션을 바꾸면 깜빡이는 버그. 원래 코드에서 정렬이 바뀌면 로딩 상태를 안 거치고 바로 데이터를 교체하는 로직이 있었는데, 내가 모든 API 호출을 통일된 패턴으로 바꾸면서 항상 로딩 상태를 거치게 됐다. 기술적으로는 맞는 동작이었지만 사용자 경험은 더 나빠졌다.

세 개 다 고치는 데 또 이틀이 걸렸다. 원래 필터 하나 추가하는 작업이었는데, 리팩토링 포함해서 7일이 걸린 거다. 30분짜리가 7일이 됐다.

깨끗한 코드라는 강박#

왜 그랬을까. 냉정하게 돌이켜보면, 코드가 더러운 게 불편해서였다. 기능적으로 문제가 있어서가 아니라 미관상 불편해서. "이건 유지보수가 어려워질 거예요"라고 합리화했지만, 실제로 6개월 동안 별 문제 없이 유지되고 있었다. 미래의 유지보수 어려움을 걱정하면서 현재의 잘 돌아가는 코드를 부숴버린 거다.

Dan Abramov가 "Goodbye, Clean Code"라는 글에서 비슷한 경험을 쓴 적이 있다. 동료가 작성한 코드에 반복이 많았다. 도형별로 리사이즈 메서드가 비슷한 로직으로 되어 있었는데, Abramov가 밤에 이걸 공통 모듈로 리팩토링했다. 중복이 사라지고 코드가 깔끔해졌다. 뿌듯해하면서 잠들었는데, 다음 날 상사가 되돌리라고 했다.

처음에는 이해를 못 했다고 한다. 하지만 시간이 지나면서 깨달았다. 그 "중복"은 나중에 각 도형이 다른 방향으로 진화할 수 있는 여지를 담고 있었다. 추상화를 하는 순간 그 여지가 사라진다. 나중에 요구사항이 바뀌면 잘못된 추상화를 억지로 늘려야 한다. 원래의 "지저분한" 코드가 오히려 더 유연했던 거다.

그 글의 마지막 문장이 인상적이다. "Let clean code guide you. Then let it go." 클린 코드를 지침으로 삼되, 거기에 집착하지는 말라는 거다.

중복이 나은 경우#

이건 반직관적인데, 생각할수록 맞는 말이다. 잘못된 추상화보다 중복이 낫다.

예를 들어보겠다. 사용자 프로필 페이지와 설정 페이지에 비슷한 폼이 있다고 치자. 둘 다 이름, 이메일, 프로필 이미지를 수정할 수 있다. 처음에는 UserForm이라는 공통 컴포넌트를 만들고 싶은 유혹이 강하다.

하지만 시간이 지나면 이렇게 된다. 프로필 페이지의 폼에는 "공개 프로필 미리보기" 기능이 추가된다. 설정 페이지의 폼에는 "비밀번호 변경" 섹션이 추가된다. 프로필 페이지에서는 이미지 크롭 기능이 필요해지고, 설정 페이지에서는 이미지 없이 아바타만 선택하는 옵션이 추가된다.

UserForm 컴포넌트에 props가 점점 늘어난다. showPreview, showPasswordChange, imageCropEnabled, avatarOnly. 이런 boolean props가 5개, 10개 되면 컴포넌트 내부는 조건문 지옥이 된다. 결국 공통 컴포넌트가 양쪽 다 어중간하게 서포트하는 괴물이 된다. 처음부터 각각 만들었으면 깔끔했을 코드가, 잘못된 추상화 때문에 더 지저분해진다.

이걸 "premature abstraction"이라고 부른다. 너무 이른 추상화. 두 개의 코드가 비슷해 보인다고 해서 반드시 하나로 합쳐야 하는 건 아니다. 비슷해 보이지만 다른 이유로 존재하는 코드일 수 있다. 같은 방향으로 진화할 거라는 보장이 없다.

리팩토링의 타이밍#

그러면 리팩토링은 언제 해야 하나. 아예 하지 말라는 건 아니다.

리팩토링이 정당화되는 조건이 있다.

하나, 기능 추가가 구조적으로 어려울 때. "이 구조로는 새 기능을 넣을 수가 없다"가 아니라, 실제로 넣으려고 시도해봤는데 정말 안 될 때. 감으로 "어려울 것 같다"가 아니라 실제로 막혔을 때.

둘, 버그가 구조적 문제에서 반복될 때. 같은 종류의 버그가 같은 원인으로 반복된다면, 그건 구조를 고쳐야 한다는 신호다. 한두 번은 패치로 넘어갈 수 있지만, 세 번째부터는 근본 원인을 봐야 한다.

셋, 팀 전체가 그 코드를 이해 못 할 때. 코드를 수정해야 하는 사람이 코드를 읽지 못하면 리팩토링이 필요하다. 하지만 "나 혼자 보기 불편하다"와 "팀 전체가 이해를 못 한다"는 다른 문제다. 전자는 취향이고, 후자는 필요다.

반대로 리팩토링을 하면 안 되는 경우도 있다.

잘 돌아가는 코드가 보기 싫어서. 이건 내가 한 실수다. 코드가 못생겨도 동작하면 그건 일하는 코드다. 예쁘게 만드는 과정에서 동작을 깨뜨리면 아무 의미 없다.

릴리즈 직전에. 이건 누구나 아는 건데 가끔 유혹에 넘어간다. "이것만 정리하고 배포하자." 그 "이것만"이 사고를 부른다.

작업 범위가 불분명할 때. "이왕 하는 거 저것도 정리하자"가 시작되면 스코프가 끝없이 늘어난다. 리팩토링은 범위를 명확히 정하고, 그 범위 안에서만 해야 한다. 나머지는 티켓을 만들어놓고 나중에 한다.

코드는 목적이 아니라 수단이다#

리팩토링에 대한 생각이 바뀌면서 코드를 보는 눈도 좀 바뀌었다.

예전에는 코드가 "예뻐야" 한다고 생각했다. 짧고, 중복 없고, 구조가 깔끔하고, 네이밍이 완벽한 코드. 그게 좋은 코드라고.

지금은 코드가 "적절해야" 한다고 생각한다. 현재의 요구사항을 충족하고, 팀원이 이해할 수 있고, 합리적인 수준으로 수정 가능한 코드. 반드시 예쁠 필요는 없다.

변수명이 data2인 건 고쳐야 한다. 그건 가독성 문제니까. 하지만 같은 로직이 두 군데에 있다고 무조건 공통 함수로 빼야 하는 건 아니다. 세 군데에서 비슷한 패턴이 반복된다고 즉시 추상화해야 하는 것도 아니다. "Rule of Three"라는 게 있다. 세 번 반복되면 그때 추상화를 고려하라. 근데 나는 여기에 조건을 하나 더 단다. 세 번 반복되더라도, 같은 이유로 반복되는지를 확인해라.

코드는 제품을 만들기 위한 수단이다. 코드 자체가 목적이 아니다. 사용자는 코드가 예쁜지 아닌지 모른다. 코드가 못생겨도 화면이 잘 나오고 빠르게 동작하면 그게 좋은 코드다. 개발자의 미적 감각을 충족시키는 코드가 아니라 사용자에게 가치를 전달하는 코드가 좋은 코드다.

이 말이 불편하면 아마 예전의 나처럼 "클린 코드"에 정체성을 부여하고 있는 거다. "나는 깨끗한 코드를 짜는 사람이야"라는 자기 이미지. Abramov의 글에서도 이 부분을 지적한다. 개발자들이 코드의 깔끔함에 직업적 정체성을 부여한다고. 하지만 진짜 물어야 할 질문은 "이 코드가 예쁜가?"가 아니라 "이 코드가 어떻게 작성되고 수정되는 데 영향을 미치는가?"라는 거다.

동료의 코드를 존중하는 법#

리팩토링 충동은 내 코드에 대해서만 생기지 않는다. 동료의 코드에서 더 강하게 느껴진다. "왜 이렇게 짠 거지?"라는 생각이 드는 순간, 손이 근질근질해진다.

하지만 Abramov의 경험이 보여주듯, 동료의 코드를 무단으로 리팩토링하는 건 기술적 문제가 아니라 신뢰의 문제다. 코드에는 맥락이 있다. 그 사람이 그렇게 짠 데는 이유가 있을 수 있다. 시간 압박 때문일 수도 있고, 내가 모르는 비즈니스 요구사항 때문일 수도 있고, 내가 생각하지 못한 엣지 케이스를 고려한 결과일 수도 있다.

리팩토링하고 싶으면, 먼저 묻는다. "이 부분을 이렇게 바꾸면 어떨까요?" 대화를 먼저 하고, 동의를 얻고, 같이 하든 분담을 하든 결정하고 나서 진행한다. 이게 시간이 좀 더 걸리지만, 팀에서 일하는 방식이다. 혼자 밤에 몰래 코드를 바꿔놓는 건 — 설사 더 나은 코드가 되더라도 — 좋은 방법이 아니다.

그리고 솔직히, 내가 "더 낫다"고 생각한 코드가 실제로 더 나은지도 확실하지 않다. 내 버그 3개 경험이 증명하듯.

참을 수 있는 용기#

요즘 지저분한 코드를 보면 손이 근질거리는 건 여전하다. 다만 이제는 한 박자 멈추고 생각한다.

이 코드가 지금 문제를 일으키고 있나? 아니면 그냥 보기 불편한 건가?

이 리팩토링이 지금 필요한가? 아니면 내 만족을 위한 건가?

고치면 뭐가 좋아지나? 안 고치면 뭐가 나빠지나?

대부분의 경우, 지저분한 코드는 그냥 지저분한 코드일 뿐이다. 지금 당장 고치지 않아도 세상이 무너지지 않는다.

리팩토링을 참는 것도 실력이다. 완벽하지 않은 코드와 공존하는 것도 프로의 자세다. 모든 코드를 깨끗하게 만들겠다는 건 모든 방을 깨끗하게 유지하겠다는 것과 비슷하다. 이상적이지만 현실적이지 않다. 자주 쓰는 방은 깨끗하게 유지하고, 가끔 쓰는 창고는 좀 지저분해도 괜찮다.

물론 창고에 불이 나면 정리해야 한다. 그건 그때 가서 해도 된다.