원문 캡처 · tkdodo.eu
원문 캡처 · tkdodo.eu

코드베이스를 컴포넌트·훅·유틸리티처럼 종류별로 나누기보다 대시보드·결제처럼 기능 영역별로 묶으면 관련 코드를 찾고 함께 바꾸기 쉬워진다. 도미니크는 이런 수직적 구조가 응집도를 높이고 코드 간 의존 관계를 명확하게 하지만, 적절한 영역을 나누고 경계를 관리하려면 팀 간 소통이 필요하다고 말한다.

종류별 구조의 문제

도미니크는 컴포넌트·훅·타입·유틸리티를 각각 별도 폴더에 두는 방식이 코드의 역할만 기준으로 삼고, 어떤 기능에 속하는지는 드러내지 않는다고 지적한다. 예를 들어 useTheme은 ThemeProvider가 아니라 useTodo 옆에 놓일 수 있다. 처음에는 편리해 보여도 코드가 늘면 관련 파일을 찾기 어려워진다.

그는 10년 넘게 제품을 개발해 온 Sentry 코드베이스를 사례로 든다. 최상위 components 폴더에 200개가 넘는 파일이 있지만, 이 파일들이 공유하는 것은 컴포넌트라는 점뿐이다. 분석 기능과 워크플로 엔진처럼 서로 다른 영역의 코드가 한곳에 모여 있어 어디서 찾아야 할지 알기 어렵다는 설명이다. Sentry의 최상위 components 폴더

AI 에이전트가 코드를 작성·검토·수정하더라도 프로젝트 구조와 규칙은 여전히 중요하다고 도미니크는 주장한다. 에이전트도 사람이 효율적으로 일할 때 필요한 경계와 제약, 빠른 피드백을 필요로 한다는 것이다. 탐색하기 쉬운 프로젝트 구조와 린트 규칙, TypeScript 설정, 빠르고 신뢰할 수 있는 테스트가 여기에 포함된다. 그는 이런 조건이 새 코드베이스에서는 에이전트가 잘 작동하지만, 오랜 시간 자연스럽게 커진 코드베이스에서는 효과가 떨어지는 이유라고 본다.

기능 영역을 기준으로 묶기

도미니크가 제안하는 대안은 함께 바뀌는 코드를 함께 두는 것이다. 컴포넌트의 props를 별도 파일로 옮기지 않고 컴포넌트와 함께 두듯, 데이터 조회 코드도 해당 기능과 가까이 배치하자는 취지다. 코드가 기술적으로 컴포넌트인지 훅인지 유틸리티인지보다 무엇을 하는지가 기준이 된다.

예를 들어 위젯과 관련된 컴포넌트·훅·타입·유틸리티·상수는 모두 src/widgets/에 둘 수 있다. 이런 구분은 팀이 맡은 제품 영역이나 codeowners 설정과도 맞아떨어질 수 있다. 프로파일링 팀이 관리하는 코드를 src/profiling/에 두는 식이다.

기능 영역을 어디서 나눌지는 정답이 정해져 있지 않다. 도미니크는 페이지나 경로를 출발점으로 삼을 수 있다고 제안한다. /dashboard에 속한 코드를 먼저 묶고, 여러 페이지에서 쓰이거나 규모가 커진 위젯은 별도 영역으로 분리할 수 있다.

여러 곳에서 공유하는 코드도 독립된 영역으로 만들 수 있다. Sentry의 PageFilters는 여러 페이지에서 쓰이는 컴포넌트이므로 그 자체로 하나의 영역이 될 수 있다. 현재는 컴포넌트가 components/pageFilters에, 타입이 types/core에 나뉘어 있다. 관련 유틸리티도 별도 경로에 있어, 페이지 필터 관련 코드를 찾거나 소유 관계를 파악하려면 여러 곳을 살펴봐야 한다고 설명한다.

경계와 감수할 점

관련 코드를 한데 모으면 응집도는 높아지지만, 의존성이 저절로 줄어들지는 않는다. 한 기능을 위해 만든 유틸리티가 예상치 못한 곳에서 재사용되면 작은 수정도 위험해질 수 있다. 이를 막으려면 각 영역에서 다른 영역이 사용할 수 있는 공개 인터페이스와 내부 전용 코드를 구분해야 한다.

도미니크는 모노레포에서 각 영역을 별도 패키지로 만들고, package.json의 exports 필드로 공개 인터페이스를 정하는 방법을 제시한다. 그러면 영역 간 의존성을 명시적으로 관리할 수 있다. 그는 이를 위해 pnpm workspaces를 사용할 수 있다고 덧붙인다.

모노레포 없이도 의존 규칙을 둘 수 있다. Nx는 프로젝트 의존성 규칙을 기본 제공하며, Sentry는 어떤 영역이 다른 영역에 의존할 수 있는지 관리하기 위해 eslint-plugin-boundaries를 사용하기 시작했다. 예를 들어 Sentry의 scraps Design System에서 내부 유틸리티를 깊은 경로로 직접 가져오면 ESLint 오류가 발생하도록 할 수 있다. Nx의 프로젝트 의존성 규칙과 Sentry의 ESLint 설정에서 관련 내용을 확인할 수 있다.

이 방식에도 비용은 있다. 코드마다 적절한 기능 영역을 정하기 어렵고, 내부 전용 코드를 여러 팀이 각자 다시 구현할 위험도 있다. 도미니크는 두 문제 모두 팀 간 소통이 더 필요하다는 점에서 쉽지 않다고 인정하면서도, 코드베이스를 기능 영역별로 구성하는 것이 출발점이라고 권한다.