워크스페이스 구조의 선순환 | Nx 블로그
저희는 Nx Enterprise 프로그램을 통해 많은 기업들과 협력하고 있으며, 많은 고객으로부터 다음과 같은 질문을 받습니다.
워크스페이스를 올바르게 구성하는 방법은 무엇인가요?
독자 여러분께 말씀드리자면, 여기서 “올바른 방법”이라는 표현은 꽤 무거운 의미를 담고 있습니다. 아쉽게도 단 하나의 정답은 없습니다. 여러분의 워크스페이스에 “올바른” 구조는 여러분과 여러분이 속한 조직에 따라 달라집니다.
그렇다고 낙담하실 필요는 없습니다. 이번 글에서는 워크스페이스를 구성하는 과정을 살펴보고, 몇 가지 질문을 검토하며 의사결정을 내리는 데 도움을 드리고자 합니다. 특히 애플리케이션을 포함하는 워크스페이스와 이러한 애플리케이션을 라이브러리로 분할하는 효율적인 방법에 초점을 맞추겠습니다.
애플리케이션에 대한 기본 권장 사항: “모듈리스(modulith)”
Nx 워크스페이스 구성을 위해 흔히 권장하는 방법은 “모듈리스(modulith)” 아키텍처를 따르는 것입니다. 모듈리스 아키텍처는 특히 Spring 커뮤니티에서 인기를 얻고 있으며, 모놀리스와 마이크로서비스/마이크로프론트엔드의 중간 지점에 위치합니다. 저희는 이 용어가 마음에 들어 차용하고자 합니다.
모놀리스 방식에서는 단일 배포 단위를 가지며 모든 코드가 하나의 큰 덩어리로 존재합니다. 반면 마이크로서비스나 마이크로프론트엔드 방식에서는 배포 단위가 여러 개이고 각 애플리케이션이 서로 분리되어 독립적으로 존재합니다. 모듈리스에서는 단일 배포 단위를 유지하면서도, 코드를 개별 부분으로 분할한 뒤 이를 함께 빌드합니다. 단 하나의 배포 단위만 관리하면 된다는 이점과 함께, 각 코드를 별도로 관리할 수 있는 분리된 코드 조각들을 가질 수 있다는 이점을 동시에 누릴 수 있습니다. 두 방식의 장점을 고루 갖춘 셈입니다.
추가적인 개선 방법
모듈리스 아키텍처는 도메인 주도 설계(DDD), 헥사고날 아키텍처 또는 다른 아키텍처 접근 방식을 통해 더욱 개선될 수 있습니다. 저희 Nx 챔피언 중 한 명인 Younes Jaaidi가 Nx 사용에 관한 훌륭한 자료를 정리해 두었습니다. 여기에는 본 글에서 설명하는 방식에 다른 아키텍처 실천법을 적용하는 방법을 포함하여, 라이브러리 구성에 관한 전용 페이지도 포함되어 있습니다.
이것이 왜 중요한가요?
진행하면서 자연스럽게 워크스페이스 구조를 잡아가면 된다고 생각할 수도 있지만, 저희는 이러한 접근 방식이 시간이 흐르면서 실패하는 경우를 보아왔습니다. 진화할 수 있는 계획을 세우는 것은 그때그때 임기응변으로 해결하는 것과는 다릅니다. 워크스페이스 구조에 대한 계획이 없을 때 발생하는 상황은 다음과 같습니다.
체계 부족은 엔지니어의 작업 속도를 늦춥니다
엔지니어가 작업을 수행하려면 다음 단계가 필요합니다.
- 변경 사항을 적용할 위치 파악하기
- 해당 변경 사항 적용하기
- 변경 사항의 영향 확인하기
이는 소프트웨어 개발 주기를 단순화하여 바라본 관점이지만, 실제로 들어맞습니다. 워크스페이스가 제대로 정리되어 있지 않으면 변경 사항을 어디에 적용해야 할지 알기 어렵습니다. 동일한 변경 사항을 여러 곳에 적용해야 할 수도 있으며, 매번 새로운 위치를 찾느라 작업 속도가 느려집니다. 이는 엔지니어의 작업 속도를 늦출 뿐만 아니라 개발 시 인지적 부하(mental load)를 가중시킵니다. 엔지니어들은 눈앞에 명확하게 정의된 구조를 두는 대신, 머릿속으로 더 복잡한 구조 체계를 유지해야 합니다. 최악의 경우, 특정 기능을 어디에 둘지를 두고 엔지니어 간에 의견 충돌이 발생할 수도 있습니다.
변경 사항의 영향을 확인하는 일은 더욱 느려질 수 있습니다. 변경 사항이 올바른 위치에 들어갔는지 확신할 수 없기 때문입니다. 무엇이 영향을 받는지 확인하기 위해 여러 곳에서 여러 변경 사항을 테스트해야 할 수도 있습니다. 단지 올바른 위치에 와 있는지 확인하기 위해 여기저기 console.log를 찍기 시작한 적이 있으신가요? 이것이 바로 저희가 해결하고자 하는 느린 개발 프로세스의 전형적인 모습입니다.
체계 부족은 CI 속도를 늦춥니다
워크스페이스의 무질서는 엔지니어의 속도만 늦추는 것이 아니라 CI의 속도도 늦춥니다. CI는 태스크를 실행해야 하는데, 정리되지 않은 워크스페이스에서는 실행해야 할 태스크가 더 많아집니다.
정리되지 않은 워크스페이스에서는 어떤 태스크들이 실행될지 예측하기가 더 어려워집니다. 변경 사항을 여러 곳에 적용해야 한다면 더 많은 프로젝트가 영향을 받게 되고, 결과적으로 더 많은 affected 태스크가 실행됩니다.
또한 정리되지 않은 워크스페이스에는 이른바 “덤프 프로젝트(dump projects)”, 즉 코드를 어디에 두어야 할지 모를 때 무작정 집어넣는 프로젝트가 생기기 쉽습니다. 생각했던 것보다 규모가 커져 버린 util이나 components라는 이름의 프로젝트를 가지고 계신다면 손을 들어보세요. 이러한 “덤프 프로젝트”에는 기술 부채가 누적되며, 결국 이곳의 변경 사항으로 인해 점점 더 많은 프로젝트가 영향을 받는 결과를 초래합니다.
잘 구성된 모듈리스를 통해 얻을 수 있는 CI 개선 효과의 실제 사례 자세히 읽어보기
목표 설정하기
그렇다면 좋은 워크스페이스 구조는 어떻게 만들 수 있을까요? 디렉터리 구조나 기술적 구현을 먼저 떠올릴 수도 있지만, 본질적으로 중요한 것부터 시작해야 합니다. 워크스페이스 구조는 조직의 목표를 달성할 수 있도록 설계되어야 합니다.
분명히 말씀드리자면, 여러분의 목표가 단지 “Nx 방식을 따르는 것”이 되어서는 안 됩니다. 저희는 포춘 500대 기업들과 다년간 협력하며 쌓은 경험을 바탕으로 일반적인 권장 사항을 제안합니다. 하지만 저희가 협력했던 모든 기업의 경우, 각자의 고유한 상황에 맞춰 이러한 일반적인 권장 사항을 적용함으로써 최적의 방안을 찾아내었습니다.
워크스페이스 구조를 설계할 때 먼저 고려하는 목표는 다음과 같습니다.
- 프로젝트는 명확하게 정의된 목적을 가져야 합니다.
- 엔지니어가 코드를 빠르게 찾을 수 있어야 합니다.
- 자주 함께 업데이트되는 코드는 서로 가까이 있어야 합니다.
여러분의 워크스페이스에는 이 외에도 더 많은 목표가 존재할 것입니다. 또한 시간이 흐름에 따라 이러한 목표가 여전히 조직의 방향과 일치하는지 지속적으로 재평가할 준비가 되어 있어야 합니다. 새로운 팀, 새로운 제품, 새로운 도구가 도입됨에 따라 목표는 변하기 마련입니다. 이러한 변화에 맞추어 함께 진화할 준비를 하세요. 항상 가고자 하는 방향에 대한 명확한 그림을 가지고 시작해야 하지만, 그것이 결코 변하지 않는 고정된 그림이라고 생각해서는 안 됩니다.
앞서 임기응변식으로 해결하면 안 된다고 말씀드린 것을 기억하시나요? 비즈니스 도메인 영역에 따라 구조가 진화하도록 계획해야 하며, 이는 다시 팀 구조에 반영됩니다. 팀은 대개 특정 비즈니스 도메인을 책임지므로, 해당 팀의 작업이 가깝게 유지되도록 코드를 구성해야 합니다. 이는 다음과 같은 이점을 줍니다.
- 발견 가능성(Discoverability) - 팀이 주로 동일한 위치의 코드를 수정하게 되므로, 변경 사항을 적용할 위치를 쉽게 찾을 수 있습니다.
- 과도한 병합 충돌 방지(Avoid excessive merge conflicts) - 서로 다른 팀이 동일한 영역을 변경할 가능성이 낮아져 병합 충돌이 줄어듭니다.
- 팀 간 의존성 방지(Avoiding cross-team dependencies) - 팀이 워크스페이스의 다른 곳에서 임의로 가져다 쓰기(import)보다 자신의 도메인 내에서 작업하는 법을 파악하게 됩니다. 팀 간 의존성이 필요한 경우에는 워크스페이스의 다른 영역에서 가져오기 때문에 그 필요성이 명확히 드러납니다.
규칙 정의하기
규칙은 워크스페이스 구조가 목표를 어떻게 달성할지 결정합니다. 앞서 제시한 각 목표를 달성하기 위해 어떤 규칙이 필요한지 생각해 보겠습니다.
프로젝트는 명확하게 정의된 목적을 가져야 합니다
이는 가장 중요한 목표이며, 이를 달성하면 다른 두 가지 목표도 자연스럽게 지원하게 됩니다. 목적이 명확히 정의되면 프로젝트 내부에 무엇이 존재해야 하고 무엇이 존재하지 않아야 하는지가 결정됩니다. 코드가 어디에 위치해야 하는지 엔지니어에게 확신을 주면 코드를 더 빠르게 찾는 데 도움이 됩니다.
프로젝트 유형
이를 돕기 위해 저희는 프로젝트의 유형을 구분하여 생각하는 것을 선호합니다. 프로젝트 유형을 제한적으로 정의하면, 프로젝트를 더 잘 분류하고 그 목적을 더 명확히 파악할 수 있습니다.
- 피처 라이브러리 (Feature libraries): 개발자는 피처 라이브러리를 특정 비즈니스 유스케이스나 애플리케이션 페이지를 위해 컨테이너 컴포넌트(데이터 소스에 접근 가능한 컴포넌트)를 구현하는 라이브러리로 생각해야 합니다.
- UI 라이브러리 (UI libraries): UI 라이브러리는 오직 프레젠테이션용(presentational) 컴포넌트만 포함합니다.
- 데이터 액세스 라이브러리 (Data-access libraries): 데이터 액세스 라이브러리는 백엔드 시스템과의 상호작용을 위한 코드를 포함합니다.
- 유틸리티 라이브러리 (Utility libraries): 유틸리티 라이브러리는 여러 라이브러리와 애플리케이션에서 공통으로 사용하는 저수준(low-level) 유틸리티를 포함합니다.
이러한 구분은 좋은 엔지니어링 관행을 장려하기 때문에 훌륭한 시작점이 됩니다. UI 라이브러리라는 개념이 존재하면 엔지니어는 프레젠테이션용 컴포넌트를 구조, 입력(inputs), 출력(outputs) 관점에서 생각하게 됩니다. 데이터 액세스 라이브러리는 엔지니어가 API 상호작용을 독자적인 로직으로 분리하도록 유도합니다. 피처 라이브러리는 이 두 가지(UI 및 데이터 액세스) 프로젝트를 가져와 함께 결합함으로써 컨테이너 컴포넌트가 포함된 단일 기능을 구현합니다. 또 다른 피처 라이브러리는 서로 다른 UI 및 데이터 액세스 프로젝트를 결합하여 다른 기능을 구현할 수 있습니다. 이는 관심사 분리(separation of concerns)와 중복 방지(DRY) 코드를 강조하는 구조의 시작입니다.
이는 시작점일 뿐 끝이 아닙니다. 이 유형들이 저희가 정의할 수 있는 가장 일반적인 프로젝트 유형이지만, 여러분의 조직에는 더 많은 유형이 필요할 수도 있습니다. 흔히 볼 수 있는 다른 예시는 다음과 같습니다.
- 백엔드와 프론트엔드 간의 인터페이스 공유를 위한 모델(Model) 프로젝트
- 프론트엔드, 백엔드, 모바일 등 특정 **플랫폼(Platform)**이 지정된 프로젝트
- 자체적인 라이브러리 유형이 필요하거나 데이터 액세스 라이브러리에 통합될 수 있는 상태 관리(State management) 라이브러리
저희의 네이밍 규칙은 프로젝트를 스코프(scope)별로 분리하도록 권장합니다. 스코프를 어떻게 정의할지는 여러분만 답할 수 있는 큰 질문입니다. 스코프는 개별 앱이 될 수도 있고, 서로 다른 사업 부문이나 조직의 다른 부서가 될 수도 있습니다. 일반적으로 프로젝트는 자체 스코프(또는 shared 스코프) 내의 프로젝트에만 의존해야 합니다. 아키텍처 내에서 스코프를 정의하는 방법에 관한 유용한 자료가 많으니, 이 글 끝에 있는 참고 자료 목록을 확인해 보시기 바랍니다.
두 개 이상의 스코프 간에 코드를 공유할 때는 보통 shared 스코프를 통해 이루어집니다. 이는 여러 스코프에서 사용할 수 있는 코드에 대해 따르기 쉬운 모델입니다.
엔지니어가 코드를 빠르게 찾을 수 있어야 합니다
엔지니어는 대개 디렉터리 트리를 통해 워크스페이스를 탐색합니다. 따라서 각 디렉터리가 표준 규칙을 따르는 의미 있는 이름을 갖도록 해야 합니다. 이 네이밍 규칙은 워크스페이스 전체에 표준으로 적용되어, 모든 기여자가 프로젝트의 이름과 위치만 보고도 해당 프로젝트가 어떤 역할을 하는지 인지할 수 있어야 합니다. 저희의 네이밍 규칙에서는 디렉터리 경로에 프로젝트의 **유형(type)**과 **스코프(scope)**가 포함되도록 보장하고자 합니다.
깊게 중첩된 구조를 탐색하는 것은 작업 속도를 떨어뜨립니다. 따라서 프로젝트의 중첩은 2~3단계 깊이를 넘지 않아야 합니다. 이는 느슨한 규칙이지만 의사결정에 중요한 기준을 제공합니다. 모노레포에 무언가를 추가할 때, 새로 중첩 구조를 만들기보다는 기존 구조의 단계 내에 배치하는 편이 좋습니다.
자주 함께 업데이트되는 코드는 서로 가까이 있어야 합니다
애플리케이션의 특정 부분을 수정할 때, 코드가 저장소 전체에 흩어져 있는 것보다 하나의 일반적인 영역에 모여 있는 편이 탐색하기 더 빠릅니다. 이는 PR(풀 리퀘스트) 검토 시 코드 소유권(code ownership)을 할당하는 데도 유리합니다. 이를 실현하기 위해, 저희는 스코프를 프로젝트의 그룹화 디렉터리로 사용할 것입니다.
선순환 (The virtuous cycle)
이러한 목표와 규칙은 워크스페이스 구조를 위한 선순환(또는 긍정적 피드백 루프)으로 이어집니다. 프로젝트의 목적이 명확히 정의되어 있으므로 스코프와 이름을 지정하기 쉽습니다. 스코프와 이름이 잘 정해져 있으므로 엔지니어가 코드를 쉽게 찾을 수 있습니다. 엔지니어가 코드를 쉽게 찾을 수 있으므로 올바른 위치에 변경 사항을 적용합니다. 그리고 올바른 위치에 변경 사항이 반영되므로 프로젝트는 명확히 정의된 목적을 계속 유지할 수 있습니다. 이 과정이 꼬리를 물고 반복되면서, 워크스페이스 구조 자체가 구조의 일관성을 스스로 유지하게 만듭니다.

프로젝트 이름 정의하기
목표와 규칙이 정의되면 이제 구조를 구현하기 시작할 수 있습니다. 밑바닥부터 시작해 위로 올라가 보겠습니다. 먼저, 프로젝트에 좋은 이름을 부여해야 합니다. 프로젝트는 워크스페이스의 가장 작은 단위이므로 엔지니어는 변경 사항을 적용할 특정 프로젝트를 찾게 됩니다. 프로젝트 전반에 걸쳐 일관된 네이밍 규칙을 유지하면, 엔지니어는 프로젝트의 이름과 위치만 보고도 해당 프로젝트가 무엇을 하는지 파악할 수 있습니다.
저희의 네이밍 규칙은 스코프와 프로젝트 유형을 결합하고, 마지막에 프로젝트 식별자(identifier)를 붙이는 방식입니다. 이러한 scope-type-identifier 패턴을 적용하면 다음과 같은 프로젝트 이름을 얻을 수 있습니다.
products-feature-detailscheckout-feature-cartcheckout-data-access-taxesshared-ui-formsshared-util-dates
이 예시 프로젝트들의 가져오기(import) 경로는 다음과 같습니다.
@org/products/feature-details@org/checkout/feature-cart@org/checkout/data-access-taxes@org/shared/ui-forms@org/shared/util-dates
새로운 TypeScript 경험
최근 저희는 TypeScript 모노레포를 위한 새로운 경험을 발표했습니다. 한 가지 작은 예외를 제외하고는 기존의 워크스페이스 구조 접근 방식이 새로운 경험에서도 여전히 유효합니다. 그 예외는 package.json 파일에 정의된 프로젝트 이름에 슬래시(/)를 단 하나만 포함할 수 있다는 점입니다. 따라서 이 경우 위에서 보여드린 가져오기 경로는 다음과 같이 구성되어야 합니다.
@org/products-feature-details@org/checkout-feature-cart@org/checkout-data-access-taxes@org/shared-ui-forms@org/shared-util-dates
구조 정의하기
프로젝트 이름에 이미 스코프를 통합해 두었기 때문에 네이밍 규칙으로부터 구조를 매우 빠르게 도출할 수 있습니다. 저희가 제안하는 구조는 스코프별로 프로젝트를 그룹화하여 정의됩니다.
결과적으로 다음과 같은 워크스페이스 구조가 만들어집니다.
libs/
products/ <---- 스코프로 그룹화됨
feature-details/ <---- 프로젝트 이름 앞에 유형이 붙음
check-out/ <---- 스코프로 그룹화됨
feature-cart/ <---- 프로젝트 이름 앞에 유형이 붙음
data-access-taxes/ <---- 프로젝트 이름 앞에 유형이 붙음
shared/ <---- 스코프로 그룹화됨
ui-forms/ <---- 프로젝트 이름 앞에 유형이 붙음
util-dates <---- 프로젝트 이름 앞에 유형이 붙음
구조 유지하기
저희는 모노레포 개발 방식에 전문적인 도구 세트가 필수적이라는 점을 항상 강조해 왔습니다. 이 도구 세트에는 향후 워크스페이스 구조를 유지하기 위한 도구들도 포함됩니다. 좋은 구조는 팀이 의도된 패턴에서 벗어나지 않도록 보장하는 자동화 도구를 통해 스스로를 더욱 견고하게 만듭니다.
태그 정의하기
구조가 완성되면, 태그(tags)를 사용하여 구조의 일관성을 더욱 강력하게 유지할 수 있습니다. 태그 세트는 가능한 한 최소한으로 유지하는 것을 권장합니다. 각 프로젝트는 스코프와 유형이라는 두 가지 태그를 가져야 합니다. 저희는 이를 앞서 정의한 프로젝트 유형인 feature, ui, data-access, util로 압축했습니다. 다른 태그가 필요할 수도 있지만, 태그가 너무 많아지면 관리가 번거로워질 수 있습니다.
모듈 경계 규칙 정의하기
이러한 규칙을 통해 프로젝트 간에 올바른 경계를 적용할 수 있습니다.
- 특정 스코프 내의 프로젝트는 해당 스코프 내부 또는
shared스코프 내의 프로젝트에만 의존할 수 있습니다. feature라이브러리는 모든 유형의 프로젝트에 의존할 수 있습니다.ui라이브러리는 유틸리티 프로젝트에 의존할 수 있습니다.data-access라이브러리는 데이터 액세스 및 유틸리티 라이브러리에 의존할 수 있습니다.util라이브러리는 다른 유틸리티 라이브러리에만 의존할 수 있습니다.
이러한 규칙이 필요한 이유는 무엇일까요? 각 라이브러리 유형이 가져야 할 책임 범위를 명확하게 정의했기 때문입니다. 개발자는 실제 코드를 들여다볼 필요 없이, 프로젝트 이름과 디렉터리 위치만으로 원하는 것을 빠르게 찾을 수 있습니다. 올바른 코드를 첫 단번에 찾을 수 있게 됨으로써 빠르고 효율적인 작업이 가능해집니다. 라이브러리에 명확하게 정의된 책임이 부여되면 기능에 대해 추론하기가 더 쉬워지며, 이는 우리를 빠르고 효율적으로 만듭니다.
구조화 및 태깅을 쉽게 만드는 도구 제공하기
구조를 설정하고 나면, 이 구조가 계속 작동하도록 강제하는 것이 지속적인 과제가 됩니다. 모듈 경계는 대부분 태그에 의해 유지될 것입니다. 그렇다면 프로젝트가 스코프, 유형, 프로젝트 이름으로 제대로 이름 붙여졌는지는 어떻게 보장할 수 있을까요? 그리고 올바른 태그가 포함되었는지는 어떻게 확인할까요? 이러한 작업을 대신해 주는 워크스페이스 제네레이터(workspace generator)를 제작하여, 처음부터 올바르게 구성하기 쉬운 환경을 만드세요.
워크스페이스 유지를 위한 도구 제작에 대해 자세히 알아보기
앞으로 나아갈 방향
이제 여러분의 워크스페이스 구조가 스스로를 견고하게 유지하는 선순환에 진입하기를 바랍니다. 좋은 워크스페이스 구조는 더 좋은 구조로 이어집니다. 하지만 조직의 변화에 발맞추어 구조를 발전시킬 준비를 하시기 바랍니다. 좋은 목표와 규칙을 지속적으로 수립해 나간다면, 그에 걸맞은 훌륭한 워크스페이스 구조가 자연스럽게 뒤따를 것입니다.
이 주제에 대해 더 깊은 통찰력을 얻고 싶으시다면 다음 권장 리소스를 참고해 보세요.