Skip to content

Commit aeb7b3d

Browse files
committed
docs(kr): guides cross-import docs add
1 parent 169feed commit aeb7b3d

1 file changed

Lines changed: 293 additions & 6 deletions

File tree

Lines changed: 293 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,308 @@
11
---
22
sidebar_position: 4
3-
sidebar_class_name: sidebar-item--wip
43
pagination_next: reference/layers
54
---
65

7-
import WIP from '@site/src/shared/ui/wip/tmpl.mdx'
8-
96
# Cross-import
107

11-
<WIP ticket="220" />
8+
Cross-import는 **같은 `layer` 내부에 있는 서로 다른 `slice` 간의 import**를 의미합니다.
9+
10+
예를 들어 `features/cart`에서 `features/product`를 import하거나,
11+
`widgets/header`에서 `widgets/sidebar`를 import하는 경우입니다.
12+
13+
Cross-import는 code smell이며, slice 간의 결합도가 높아지고 있다는 경고 신호입니다.
14+
불가피하게 사용해야 한다면, 단순히 가져다 쓰는 게 아닌 왜 필요한지 명확히 판단하고 사용해야 합니다.
15+
그리고 이런 예외적인 의존 관계는 반드시 팀 내에서 서로 합의된 내용이어야 합니다.
16+
17+
:::note
18+
`shared``app` layer는 `slice` 개념이 없으므로, 해당 layer 내부 import는 cross-import로 보지 않습니다.
19+
:::
20+
21+
22+
## Cross-import는 왜 code smell 인가요?
23+
24+
Cross-import는 단순히 코딩 스타일의 문제가 아닙니다.
25+
도메인 간의 경계를 흐리고, 변경 사항이 어디까지 영향을 줄지 알 수 없게 만드는 위험 신호로 간주됩니다.
26+
27+
예를 들어 `cart` slice가 `product`의 비즈니스 로직에 직접 의존하는 상황을 생각해 봅시다.
28+
당장은 구현이 편해 보일 수 있지만, 시간이 지나면 다음과 같은 문제들이 발생합니다.
29+
30+
1. **코드의 책임과 관리 주체가 모호해집니다**
31+
`cart``product` 내부 로직을 직접 가져다 쓰기 시작하면, 해당 로직의 '적절한 위치'가 어디인지 판단하기 어려워집니다.
32+
`product` 내부 코드를 리팩토링하거나 로직을 바꿀 때, 그 로직을 `cart`도 함께 사용한다는 점을 놓치면 `cart`에서 런타임 오류나 동작 변경이 발생할 수 있습니다.
33+
이런 숨은 의존성은 코드 탐색과 수정 난도를 올리고, 문제 발생 시 어느 `slice`에서 고쳐야 하는지 판단을 어렵게 만들어 결과적으로 리뷰/커뮤니케이션 비용을 키울 수 있습니다.
34+
35+
2. **독립적인 실행과 테스트가 어려워집니다**
36+
Sliced Architecture의 가장 큰 장점은 각 Slice를 독립적으로 개발하고 테스트하며 배포할 수 있다는 점입니다.
37+
하지만 Cross-import가 늘어나면 이 격리 상태가 깨집니다. `cart`만 테스트하고 싶어도 `product`의 정보를 불러와야 하며,
38+
한 Slice에서의 수정이 다른 Slice의 기능이 예기치 않게 오작동하는 부작용이 발생할 수 있습니다
39+
40+
3. **코드 탐색 비용이 증가합니다.**
41+
`cart`를 수정할 때 의존 중인 `product`의 설계와 동작 방식까지 모두 파악해야 합니다.
42+
하나의 Slice 안에서 작업을 끝내지 못하고, 연관된 여러 Slice 파일을 오가며 로직의 흐름을 쫓아야 합니다.
43+
작은 부분을 수정할 때도 여러 Slice의 맥락을 동시에 고려해야 하므로 실수가 잦아집니다.
44+
45+
4. **순환 의존성의 원인이 됩니다**
46+
cross-import는 처음엔 A→B 단방향으로 시작하더라도 시간이 지나면서 B→A가 생겨 **양방향(순환) 의존성**이 되기 쉽습니다.
47+
이렇게 되면 slice들이 사실상 하나로 묶여버리고, 의존성을 풀거나 리팩터링하는 비용이 크게 올라갑니다.
48+
49+
도메인 경계를 나누는 목적은 각 Slice가 자신의 책임에 집중하고 독립적으로 변화하게 만들기 위함입니다.
50+
의존 관계가 느슨할수록 변경 영향을 예측하기 쉽고, 리뷰와 테스트 범위도 좁게 유지할 수 있습니다.
51+
cross-import는 이 분리를 약화시키기 때문에, 일반적으로는 피하는 것이 좋은 의존성으로 다루는 편이 안전합니다.
52+
53+
54+
## Entities Layer cross-imports
55+
56+
`entities`에서 cross-import가 자주 보인다면, 우선 `entity` 경계를 너무 잘게 쪼갠 것은 아닌지 점검해야 합니다.
57+
`@x` 패턴을 도입하기 전에, 분리된 Slice들을 하나로 병합하는 것이 설계상 더 자연스럽지 않은지 먼저 검토하세요.
58+
59+
일부 팀에서는 `entities` 간의 불가피한 교차 참조를 해결하기 위해 `@x`를 별도의 entry point로 운영하기도 합니다.
60+
하지만 `@x`는 권장되는 패턴이라기보다 불가피한 타협에 가깝습니다.
61+
따라서 다른 대안이 없을 때 선택하는 마지막 수단으로 취급하는 것이 좋습니다.
62+
63+
`@x`는 도메인 간의 피할 수 없는 의존 관계를 감추지 않고 명확히 드러내기 위한 공식 통로에 가깝습니다.
64+
이를 남발하면 `entity` 경계가 서로 강하게 엮이고, 시간이 지날수록 리팩터링 비용이 커지기 쉽습니다.
65+
66+
`@x`에 대한 자세한 내용은 [Public API 문서](/docs/reference/public-api)를 참고하세요.
67+
68+
비즈니스 `entity` 간 상호 참조(타입/관계)의 구체 예시는 아래 문서를 참고하세요:
69+
- [Types 가이드 — 비즈니스 엔티티와 상호 참조](/docs/guides/examples/types#business-entities-and-their-cross-references)
70+
- [Layers 레퍼런스 — Entities](/docs/reference/layers#entities)
71+
72+
73+
## Features와 Widgets: 다양한 설계 전략
74+
75+
`features``widgets`에서는 cross-import를 항상 금지라고 선언하기보다,
76+
실제로는 팀/도메인/제품 상황에 따라 선택 가능한 여러 전략이 있다고 보는 편이 현실적입니다.
77+
이 섹션은 코드 자체보다, 상황에 따라 고를 수 있는 패턴 중심으로 설명합니다.
78+
79+
80+
### Strategy A: Slice merge
81+
82+
두 slice가 실제로 독립적이지 않고 항상 같이 변경된다면, 둘을 하나의 더 큰 slice로 합치는 방법이 있습니다.
83+
84+
예시 (before):
85+
86+
- `features/profile`
87+
- `features/profileSettings`
88+
89+
서로 계속 cross-import하고 사실상 한 단위로 움직인다면, 사실상 하나의 기능에 가까울 가능성이 큽니다.
90+
그 경우 `features/profile`로 합치는 편이 구조적으로 더 단순하고 유지보수도 쉬워지는 경우가 많습니다.
91+
92+
93+
### Strategy B: 여러 `features`에서 공유하는 도메인 로직을 `entities`로 내리기
94+
95+
여러 `feature`가 같은 도메인 로직(예: 세션 검증)을 반복해서 사용한다면,
96+
그 로직을 `entities` 내부의 도메인 slice(예: `entities/session`)로 옮길 수 있습니다.
97+
98+
**핵심 원칙**:
99+
- `entities`에는 도메인 타입과 로직만 둡니다. (예: createSessionFromToken(), isSessionExpired())
100+
- UI는 `features` / `widgets`에 유지합니다.
101+
-`feature`들은 `entities`의 도메인 로직을 import하여 재사용합니다.
102+
103+
예를 들어 `features/auth``features/profile`이 모두 세션 검증이 필요하다면,
104+
세션 관련 도메인 함수들을 `entities/session`에 두고 두 feature에서 공통으로 사용합니다.
105+
106+
자세한 예시는 [Layers reference — Entities](/docs/reference/layers#entities)를 참고하세요.
107+
108+
109+
### Strategy C: 상위 레이어(pages / app)에서 조립하기
110+
111+
같은 layer 내부에서 slice끼리 cross-import 하는 대신, 상위 `pages` / `app`에서 필요한 것들을 조립하는 방식입니다.
112+
Slice들이 서로를 직접 참조하지 않도록 하고, 상위 layer가 화면/플로우를 구성하면서 연결을 담당하는 방식으로 **Inversion of Control (IoC)** 패턴을 적용합니다.
113+
114+
대표적인 IoC 기법은 아래와 같습니다.
115+
- **Render props (React)**: Component 또는 render 함수를 props로 전달해 조립합니다
116+
- **Slots (Vue)**: named slot을 사용해 부모가 콘텐츠를 주입합니다.
117+
- **Dependency injection**: props 또는 Context로 의존성을 전달합니다.
118+
119+
#### Basic composition example (React)
120+
121+
```tsx title="features/userProfile/index.ts"
122+
export { UserProfilePanel } from './ui/UserProfilePanel';
123+
```
124+
125+
```tsx title="features/activityFeed/index.ts"
126+
export { ActivityFeed } from './ui/ActivityFeed';
127+
```
128+
129+
```tsx title="pages/UserDashboardPage.tsx"
130+
import React from 'react';
131+
import { UserProfilePanel } from '@/features/userProfile';
132+
import { ActivityFeed } from '@/features/activityFeed';
133+
134+
export function UserDashboardPage() {
135+
return (
136+
<div>
137+
<UserProfilePanel />
138+
<ActivityFeed />
139+
</div>
140+
);
141+
}
142+
```
143+
144+
이 구조에서는 `features/userProfile``features/activityFeed`가 서로를 import하지 않습니다.
145+
대신 `pages/UserDashboardPage`가 두 `feature`를 조합해 화면을 구성합니다.
146+
147+
#### Render props example (React)
148+
149+
특정 feature가 다른 feature의 UI를 렌더링해야 하는 경우,
150+
render props를 통해 렌더링을 주입함으로써 의존성을 상위 layer로 이동시킬 수 있습니다.
151+
152+
```tsx title="features/commentList/ui/CommentList.tsx"
153+
interface CommentListProps {
154+
comments: Comment[];
155+
renderUserAvatar?: (userId: string) => React.ReactNode;
156+
}
157+
158+
export function CommentList({ comments, renderUserAvatar }: CommentListProps) {
159+
return (
160+
<ul>
161+
{comments.map(comment => (
162+
<li key={comment.id}>
163+
{renderUserAvatar?.(comment.userId)}
164+
<span>{comment.text}</span>
165+
</li>
166+
))}
167+
</ul>
168+
);
169+
}
170+
```
171+
172+
```tsx title="pages/PostPage.tsx"
173+
import { CommentList } from '@/features/commentList';
174+
import { UserAvatar } from '@/features/userProfile';
175+
176+
export function PostPage() {
177+
return (
178+
<CommentList
179+
comments={comments}
180+
renderUserAvatar={(userId) => <UserAvatar userId={userId} />}
181+
/>
182+
);
183+
}
184+
```
185+
186+
이렇게 하면 `CommentList``UserAvatar`를 직접 import하지 않습니다.
187+
대신 `pages/PostPage`에서 `UserAvatar`를 import한 뒤, `renderUserAvatar` prop으로 전달합니다.
188+
189+
#### Slots example (Vue)
190+
191+
Vue에서는 named slot으로 부모(pages)가 콘텐츠를 주입해 cross-import 없이 조립할 수 있습니다.
192+
193+
```vue title="features/commentList/ui/CommentList.vue"
194+
<template>
195+
<ul>
196+
<li v-for="comment in comments" :key="comment.id">
197+
<slot name="avatar" :userId="comment.userId" />
198+
<span>{{ comment.text }}</span>
199+
</li>
200+
</ul>
201+
</template>
202+
203+
<script setup lang="ts">
204+
defineProps<{
205+
comments: Comment[];
206+
}>();
207+
</script>
208+
```
209+
210+
```vue title="pages/PostPage.vue"
211+
<template>
212+
<CommentList :comments="comments">
213+
<template #avatar="{ userId }">
214+
<UserAvatar :userId="userId" />
215+
</template>
216+
</CommentList>
217+
</template>
218+
219+
<script setup lang="ts">
220+
import { CommentList } from '@/features/commentList';
221+
import { UserAvatar } from '@/features/userProfile';
222+
</script>
223+
```
224+
225+
`CommentList``userProfile`을 import하지 않고, `PostPage``slot`으로 조립합니다.
226+
227+
### Strategy D: Cross-feature 재사용은 Public API를 통해서만
228+
229+
A–C는 cross-import 자체를 없애려는 전략입니다.
230+
반면 D는 cross-import가 불가피한 경우, 공개된 Public API만 사용하게 제한해서 결합과 변경 영향을 관리하는 전략입니다.
231+
232+
cross-feature 재사용이 필요한 상황이라면, 다른 slice의 내부 구현(예: store/model/internal)에 직접 접근하지 말고,
233+
Public API를 통해서만 사용하도록 제한합니다.
234+
235+
#### Example code:
236+
237+
```tsx title="features/auth/index.ts"
238+
export { useAuth } from './model/useAuth';
239+
export { AuthButton } from './ui/AuthButton';
240+
```
241+
242+
```tsx title="features/profile/ui/ProfileMenu.tsx"
243+
244+
import React from 'react';
245+
import { useAuth, AuthButton } from '@/features/auth';
246+
247+
export function ProfileMenu() {
248+
const { user } = useAuth();
249+
250+
if (!user) {
251+
return <AuthButton />;
252+
}
253+
254+
return <div>{user.name}</div>;
255+
}
256+
```
257+
258+
예를 들어 `features/profile``features/auth/model/internal/*` 같은 경로에 직접 접근하지 않도록 합니다.
259+
`features/auth`가 공식적으로 공개한 Public API만 사용하도록 제한합니다.
260+
261+
## cross-import를 문제로 간주해야 하는 경우
262+
263+
위에서 여러 전략을 살펴봤다면, 이제 다음 질문이 생길 수 있습니다.
264+
265+
> cross-import를 그냥 두어도 되는 상황은 언제일까?
266+
> 그리고 언제는 “이건 code smell이다”라고 보고 리팩터링을 검토해야 할까?
267+
268+
대표적인 경고 신호는 다음과 같습니다.
269+
270+
- 다른 slice의 store/model/비즈니스 로직에 직접 의존하는 경우
271+
- slice들 간에 서로 직접 의존하는 **양방향 의존성**이 생긴 경우
272+
- 하나의 slice 변경이 거의 항상 다른 slice를 함께 깨뜨리는 경우
273+
- 원래는 상위 layer(`pages` / `app`)에서 조립해야 할 플로우를,
274+
같은 layer의 cross-import로 억지로 구현하고 있는 경우
275+
276+
이런 신호들이 보인다면,
277+
그 cross-import는 **code smell**로 보고 위에서 소개한 전략들 중 최소 하나는 적용할 수 있는지 검토해야 합니다.
278+
279+
## 적용 범위와 기준은 팀 및 프로젝트의 결정 사항입니다.
280+
281+
마지막으로, 다음 내용에서는 이 규칙들을 얼마나 엄격하게 적용할지는 팀과 프로젝트에 따라 달라진다는 점을 강조합니다.
282+
283+
예를 들어:
284+
285+
- **초기 단계 제품**처럼 요구사항 변동이 크고 빠른 반복이 잦은 서비스라면,
286+
단기적인 개발 속도를 위해 어느 정도의 cross-import를 허용할 수도 있습니다.
287+
- 반대로, **장기 운영이 전제되거나 규제 요구사항이 높은 시스템**
288+
더 엄격한 경계와 layer 설계를 통해 장기적인 안정성과 유지보수성을 얻는 편이 낫습니다.
289+
290+
우리는 cross-import를 **절대 금지 규칙**으로 보지 않습니다.
291+
대신, **일반적으로는 피해야 하는 의존성**으로 취급합니다.
292+
293+
cross-import를 도입할 때에는, 그것이 **의도적이고 의식적인 설계 선택**인지 인지해야 합니다.
294+
또한 이 선택을 문서화하고, 시스템이 진화함에 따라 주기적으로 다시 검토하는 것이 좋습니다.
295+
296+
팀은 다음과 같은 점들에 대해 서로 합의를 맞출 필요가 있습니다.
12297

13-
> Cross-import는 Layer나 추상화가 원래의 책임 범위를 넘어설 때 발생합니다. 방법론에서는 이러한 Cross-import를 해결하기 위한 별도의 Layer를 정의합니다.
298+
- 우리 팀/프로젝트에서 원하는 엄격함 수준은 어느 정도인지
299+
- 그 엄격함을 lint, 코드 리뷰, 문서 등에 어떻게 반영할지
300+
- 도메인과 아키텍처가 성숙해지면서 cross-import를 어떤 주기와 기준으로 다시 점검할지
14301

15302
## 참고 자료
16303

17304
- [(스레드) Cross-import가 불가피한 상황 논의](https://t.me/feature_sliced/4515)
18305
- [(스레드) Entity에서 Cross-import 해결 방법](https://t.me/feature_sliced/3678)
19306
- [(스레드) Cross-import와 책임 범위 관계](https://t.me/feature_sliced/3287)
20307
- [(스레드) Segment 간 import 이슈 해결](https://t.me/feature_sliced/4021)
21-
- [(스레드) Shared 내부 구조의 Cross-import 해결](https://t.me/feature_sliced/3618)
308+
- [(스레드) Shared 내부 구조의 Cross-import 해결](https://t.me/feature_sliced/3618)

0 commit comments

Comments
 (0)