모노레포나 레이어가 분리된 프로젝트를 운영하다 보면, 어느 순간부터 타입 import가 서로를 가리키기 시작합니다. 처음에는 단순한 참조처럼 보이지만, 시간이 지나면 빌드 순서가 꼬이고, 타입 에러가 예측하기 어려운 형태로 나타납니다.
순환 의존은 런타임 코드보다 타입 레벨에서 먼저 드러나는 경우가 많습니다. 특히 shared 타입이 늘어나고, 레이어 간 경계가 흐려질수록 타입 참조가 서로 얽히는 속도가 빨라집니다.
순환 의존이 만들어지는 전형적인 흐름
대부분의 순환 의존은 의도해서 만들지 않습니다. 작은 편의를 반복하다 보면 자연스럽게 생깁니다.
- 공통 타입을 shared로 이동
- shared 타입이 특정 서비스 타입을 참조
- 해당 서비스가 다시 shared를 참조
처음에는 문제가 없어 보이지만, 타입 그래프가 커질수록 빌드와 분석 단계에서 불안정성이 나타납니다.
초기에 보이는 징후
순환 의존은 갑자기 폭발하기보다, 몇 가지 신호가 먼저 나타나는 경우가 많습니다.
- 타입만 import했는데 예상치 못한 파일이 함께 로드됨
- 빌드 캐시를 지우면 에러가 사라졌다가 다시 나타남
- IDE에서 타입 추론이 불안정해짐
- 패키지 빌드 순서에 민감해짐
이 단계에서 구조를 한 번 점검해 두면, 나중에 큰 리팩터링을 피할 수 있습니다.
타입 순환이 특히 잘 생기는 위치
1. DTO와 도메인 모델을 서로 참조하는 경우
// api/dto.ts
import { User } from '../domain/user';
export type UserResponseDto = {
id: User['id'];
};
// domain/user.ts
import { UserResponseDto } from '../api/dto';
export type User = {
id: string;
toDto(): UserResponseDto;
};
서로 자연스럽게 보이지만, 이 구조는 타입 그래프를 바로 순환으로 만듭니다.
2. shared가 상위 레이어 타입을 참조하기 시작한 경우
// shared/common.ts
import { OrderDto } from '../api/order/dto';
export type ApiEnvelope<T> = {
data: T;
meta?: OrderDto['meta'];
};
shared는 보통 가장 아래 레이어에 위치합니다. 여기서 상위 레이어를 참조하기 시작하면, 의존 방향이 뒤집히면서 순환 가능성이 크게 올라갑니다.
구조를 크게 흔들지 않고 끊는 방법
방법 1: 참조 대신 “값 타입”으로 끊기
많은 순환은 “편하게 타입을 끌어다 쓰려는” 순간에 생깁니다. 이럴 때는 필요한 필드만 별도 타입으로 분리하는 쪽이 덜 흔들립니다.
// before
type UserResponseDto = {
id: User['id'];
};
// after
type UserId = string;
type UserResponseDto = {
id: UserId;
};
타입 의존을 직접 연결하지 않고, 의미 단위로 분리하는 방식입니다.
방법 2: 인터페이스 분리로 의존 방향 고정
도메인이 상위 레이어를 참조하는 경우에는, 계약 인터페이스를 중간에 두는 방식이 자주 사용됩니다.
// shared/contracts.ts
export interface UserPresenter {
toDto(): {
id: string;
};
}
// domain/user.ts
import { UserPresenter } from '@shared/contracts';
export class User implements UserPresenter {
constructor(public readonly id: string) {}
toDto() {
return { id: this.id };
}
}
도메인은 계약만 알고, API 레이어 구현과 직접 연결되지 않습니다.
방법 3: type-only import로 런타임 결합 줄이기
완전히 구조를 바꾸기 어려운 상황에서는, type import로 런타임 결합을 줄이는 접근도 사용됩니다.
import type { User } from '../domain/user';
다만 이 방법은 타입 그래프 자체를 끊어주지는 않습니다. 구조 개선 전의 완충 장치 정도로 보는 편이 맞습니다.
모노레포에서 특히 주의할 지점
패키지가 분리된 환경에서는 다음 상황에서 순환이 빨리 드러납니다.
- shared → service → shared 재참조
- web ↔ api 타입 상호 참조
- generated 타입을 여러 레이어에서 직접 참조
의존 그래프를 한 번 시각화해 보면, 문제가 되는 지점이 비교적 명확하게 보이는 경우가 많습니다.
운영 중 나타나는 실제 문제들
타입 순환은 단순한 구조 문제를 넘어서, 다음과 같은 형태로 운영에 영향을 줍니다.
- 빌드 캐시가 불안정해짐
- 증분 컴파일 시간이 길어짐
- 테스트 환경에서만 간헐적 실패
- IDE 타입 추론 지연
특히 증상이 간헐적으로 나타나는 경우가 많아서, 초기에 원인을 찾기 어려운 편입니다.
점검할 때 보게 되는 항목들
- shared가 상위 레이어 타입을 참조하고 있지 않은지
- DTO와 도메인 모델이 서로를 import하고 있지 않은지
- 타입 하나 변경 시 예상보다 넓은 패키지가 다시 빌드되는지
- type import로 임시 우회가 늘어나고 있지 않은지
- 패키지 의존 그래프에 순환 경로가 존재하는지
타입 순환 의존은 코드가 커질수록 자연스럽게 생길 수 있습니다. 문제는 존재 자체보다, 어느 지점에서 경계를 다시 세우느냐에 가까운 경우가 많습니다.
참조를 줄이고, 의존 방향을 한쪽으로 고정하고, 의미 단위로 타입을 끊어두면 대부분의 순환은 구조를 크게 흔들지 않고도 정리할 수 있습니다.
'개발 > Typescript' 카테고리의 다른 글
| [TYPESCRIPT] 타입 안정성과 런타임 검증을 나누는 기준 (0) | 2026.03.05 |
|---|---|
| [TYPESCRIPT] 타입 추론이 과하게 깊어질 때 생기는 성능 문제와 완화 방법 (0) | 2026.03.04 |
| [TYPESCRIPT] 공통 타입을 분리하기 시작했을 때 생기는 결합과 관리 전략 (0) | 2026.03.02 |
| [TYPESCRIPT] 대규모 TypeScript 프로젝트 타입 설계 전략: 운영에서 흔들리지 않게 만드는 기준 (0) | 2026.03.01 |
| [TYPESCRIPT] TypeScript 프로젝트에서 절대 경로 설정을 운영 기준으로 잡는 방법 (0) | 2026.02.28 |
