모든 개발자에게는 그런 폴더가 하나씩 있습니다. 저장소(repo)에서 저장소로 복사해 다니는 utils나 helpers라고 불리는 폴더 말이죠. 그걸 붙여넣고 나서, 오래된 데이터베이스 스키마에 대한 참조를 삭제하고, 적용되지 않는 인증(auth) 체크 로직을 뜯어내고, 새로운 린터(linter)가 경고를 멈추도록 변수 이름을 바꾸는 데 20분을 허비하곤 합니다. 저도 제가 만든 테마 시스템인 Dynamic Theme Kit으로 이런 일을 하곤 했습니다. 처음에는 한 애플리케이션 내부의 기능으로 시작했고, 몇 달 동안은 이를 휴대 가능한 도구처럼 취급했습니다. 하지만 제 생각이 틀렸었습니다. 코드를 복사하는 것은 재사용이 아닙니다. 그것은 단지 단계가 더 추가된 중복일 뿐입니다.
단일 프로젝트 사고방식의 함정
프로젝트 내부에서 기능을 만들 때, 여러분은 수백 가지의 보이지 않는 가정을 하게 됩니다. 컬러 팔레트는 특정 CSS-in-JS 설정을 가정할 수 있습니다. 간격(spacing) 스케일은 회사의 브랜드 가이드에 있는 디자인 토큰을 참조할 수도 있습니다. 라이트 모드와 다크 모드 전환 기능은 해당 앱의 백엔드에만 존재하는 사용자 설정 엔드포인트를 호출할 수도 있습니다. 이러한 의존성들은 프로젝트 내부에서는 아무런 문제가 없기 때문에 무해하게 느껴집니다. 그 자리에 있어야 할 것들이니까요.
문제는 그 코드를 밖으로 떼어내려 할 때 시작됩니다. "재사용 가능한" 컴포넌트가 실제로는 해당 코드베이스와 연결된 숨겨진 문자열의 거미줄이라는 사실을 깨닫게 됩니다. 저는 DTK를 통해 이를 배웠습니다. DTK는 테마 변수를 생성해 주긴 했습니다. 하지만 특정 폴더 구조를 요구하기도 했습니다. 원래 앱의 types 디렉토리 깊숙한 곳에서 타입 정의를 가져오기도 했습니다. 오직 그 저장소에만 존재하는 글로벌 설정(config) 객체가 있을 것이라고 가정하기도 했습니다. 프로젝트 내부에서는 모든 것이 항상 존재했기에 전혀 눈치채지 못했습니다.
DTK를 독립적인 패키지로 만드는 것은 확장이 아니라 수술을 의미했습니다. 저에게 필요했던 것은 더 많은 기능이 아니라, 더 적은 연결이었습니다.
Dynamic Theme Kit 추출하기
가장 힘든 작업은 코드베이스를 앞에 두고 모든 함수와 모든 export에 대해 질문을 던지는 것이었습니다. "이것이 테마 로직을 위한 것인가, 아니면 프로젝트를 위한 것인가?" 저는 스타일링 프리셋을 제거했습니다. 사용하는 쪽이 React 애플리케이션일 것이라는 가정을 없앴습니다. 기본 컬러 팔레트를 완전히 삭제했습니다. 원래 프로젝트의 기본값에는 네이비와 슬레이트 색상의 기업용 미학이 녹아 있었습니다. 그것은 제거해야 했습니다. 패키지가 사용자의 브랜드 컬러를 함께 배포할 수는 없으니까요.
새로운 키트는 정확히 한 가지 일만 수행합니다. 설정 객체(색상 값, 간격 수치, 타이포그래피 스케일 등)를 받아 CSS custom properties를 생성하는 것입니다. 그게 전부입니다. 이를 직접 적용하지도 않고, DOM의 어디에 위치할지도 결정하지 않습니다. Tailwind를 쓰든, Styled Components를 쓰든, 아니면 순수 HTML을 쓰든 상관하지 않습니다. 애플리케이션에 변수를 제공할 뿐이며, 그 변수를 어떻게 사용할지는 프로젝트가 결정합니다.
처음에는 그 제약이 답답하게 느껴졌습니다. 하지만 결과적으로는 해방감을 주었습니다.
실제로 재사용하려고 할 때 무엇이 망가지는가
무엇인가를 배포하기 전에, 이 추상화가 실제로 유효하다는 증거가 필요했습니다. 저는 아카이브에서 세 개의 작은 개인 프로젝트를 꺼냈습니다. 마크다운 미리보기 도구, 습관 추적기, 그리고 이벤트용 랜딩 페이지였습니다. 이 프로젝트들은 프레임워크나 폴더 구조를 전혀 공유하지 않았습니다. 저는 각 프로젝트에 DTK를 로컬로 설치하고 테마를 적용해 보았습니다.
첫 번째 시도는 즉시 실패했습니다. DTK가 생성한 변수 이름이 너무 구체적이었습니다. --primary-action이나 --background-overlay와 같이 특정 UI 레이아웃을 암시하는 토큰을 출력하고 있었습니다. 마크다운 미리보기 도구에서는 그런 이름들이 전혀 맞지 않았습니다. 액션 버튼도 없었고, 오버레이도 없었으니까요. 저는 위젯이 아닌 값을 설명하는 중립적이고 구조적인 이름을 생성하도록 로직을 변경했습니다.
또한 기본값이 너무 공격적이라는 사실도 발견했습니다. 사용자가 불완전한 설정을 전달하면, DTK는 빽빽한 대시보드에서는 괜찮아 보이지만 텅 빈 랜딩 페이지에서는 어색해 보이는 값들로 빈틈을 채워버렸습니다. 저는 누락된 토큰이 아예 렌더링되지 않는 투명한 기본값 방식으로 전환하여, 사용하는 프로젝트가 자체적인 폴백(fallback)을 정의할 수 있도록 했습니다.
그다음은 문서화였습니다. 저에게는 "그냥 설정 객체를 전달하면 됩니다"처럼 당연해 보이는 내용이, 한밤중에 README를 읽는 사람에게는 모호하게 느껴질 수 있었습니다. 저는 실제 객체와 실제 파일 경로를 사용하고, 함수를 호출할 때 일어나는 일과 그 이후에 애플리케이션이 해야 할 일을 명확하게 설명하며 문서를 다시 작성했습니다.
이 작은 개인 프로젝트들은 테스트 베드 역할을 했습니다. 위험 부담은 적었지만, 소스 코드만 따로 들여다봤다면 절대 발견하지 못했을 실제 결함들을 드러내 주었습니다.
진짜 테스트: Web Weavers World에서의 프로덕션 적용
개인 프로젝트는 샌드박스와 같습니다. 마감 기한도, 이해관계자도, 패키지보다 오래된 레거시 CSS도 없죠. 진짜 시험대는 제 비즈니스 사이트인 Web Weavers World에 DTK를 통합했을 때였습니다. 이곳은 기존 스타일, 고객의 기대치, 고려해야 할 분석 데이터가 존재하는 실제 운영 중인 사이트였습니다. 만약 패키지가 무언가를 망가뜨린다면, 단순히 저장소를 삭제하고 처음부터 다시 시작할 수도 없는 상황이었습니다.
저는 빌드 파이프라인에 DTK를 추가하고, 새로운 컬러 설정(color configuration)을 지정한 뒤, 새로운 CSS 변수 세트를 생성하도록 했습니다. 통합 작업은 일주일이 아니라 단 한 오후 만에 끝났습니다. 그것이 바로 신호였습니다. 이전에는 새로운 테마를 추가하려면 새로운 CSS를 작성하고, 20개의 파일에서 하드코딩된 헥스(hex) 값을 찾아다니며, 예외 케이스를 놓치지 않기를 기도해야 했습니다. 이제는 설정 파일에 팔레트를 추가하기만 하면 DTK가 변수를 생성하고, 사이트의 나머지 부분에서 이를 사용합니다. 테마 로직은 취약한 수동 프로세스에서 협업자에게 믿고 맡길 수 있는 수준으로 진화했습니다.
개발 방식을 바꾼 세 가지 질문
이 과정을 거치며 저는 무언가를 추상화하기 전에 사용하는 정신적 체크리스트를 공식화하게 되었습니다.
- 이 변수는 진정으로 범용적인가? 변수 이름이나 로직이 원래 프로젝트의 도메인 개념을 참조하고 있다면, 그것은 추상화 대상에서 제외해야 합니다.
- 이것은 패키지에 속해야 하는가, 애플리케이션에 속해야 하는가? 비즈니스 규칙, 브랜드 정체성, 레이아웃 가설은 애플리케이션에 존재해야 합니다. 표준화된 출력을 생성하는 기반 구조(plumbing)는 패키지에 존재해야 합니다.
- 재사용 가능한 문제를 해결하고 있는가, 아니면 특정 프로젝트에 국한된 문제를 해결하고 있는가? 이 질문에 솔직하게 답하는 것이 가장 어렵습니다. 우리는 자신의 솔루션이 보편적이라고 생각하고 싶어 하지만, 대개는 국지적입니다.
이 질문들에 답하면서 저는 코드를 추가하기보다 오히려 제거함으로써 설계를 단순화할 수 있었습니다. DTK는 재사용이 자신에게 주는 선물이 아니라, 편리함을 거부함으로써 실천하는 규율이라는 것을 가르쳐 주었습니다.
리팩터링을 바라보는 다른 관점
예전에는 리팩터링의 척도를 코드가 얼마나 짧아졌느냐로 판단했습니다. 코드 줄 수가 줄어드는 것이 진보라고 느꼈죠. 이제 저는 리팩터링이 얼마나 많은 가능성을 열어주느냐로 측정합니다. Dynamic Theme Kit이 우아한 이유는 간결하기 때문이 아닙니다. 내부 구조를 변경할 필요 없이 세 개의 서로 다른 개인 프로젝트와 실제 운영 중인 비즈니스 사이트에서 살아남았기 때문에 유용한 것입니다.
그것이 바로 중요한 지표입니다. 한 번만 작동하는 코드는 비용입니다. 반복해서 작동하는 코드는 자산입니다. 이제 저는 어떤 기능을 시작하기 전에 잠시 멈춥니다. 그리고 이것이 나중에 다시 필요할 만한 것인지 스스로에게 묻습니다. 만약 그렇다면, 첫 줄부터 다르게 설계합니다. 입력을 격리하고, 출력을 정의하며, 가설(assumptions)을 제거합니다.
최고의 리팩터링은 코드를 짧게 만드는 것이 아닙니다. 아직 상상하지 못한 곳에서도 코드가 작동하게 만드는 것입니다.
