|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +--- |
| 4 | + |
| 5 | +# Public API |
| 6 | + |
| 7 | +Public API는 Slice와 같은 모듈 그룹과 이를 사용하는 코드 사이의 Contract 역할을 합니다. 또한 Gate 역할을 하여 특정 Object에 접근할 수 있는 유일한 경로를 제공합니다. |
| 8 | + |
| 9 | +일반적으로 Public API는 Re-export가 포함된 Index File로 구현됩니다: |
| 10 | + |
| 11 | +```js title="pages/auth/index.js" |
| 12 | +export { LoginPage } from "./ui/LoginPage"; |
| 13 | +export { RegisterPage } from "./ui/RegisterPage"; |
| 14 | +``` |
| 15 | + |
| 16 | +## 좋은 Public API의 조건 |
| 17 | + |
| 18 | +좋은 Public API는 Slice 사용과 통합을 용이하게 합니다. 세 가지 주요 목표: |
| 19 | + |
| 20 | +1. Application은 Slice 내부 구조의 Refactoring에 영향받지 않아야 함 |
| 21 | +2. Slice 동작의 중요한 변경은 Public API 변경으로 이어져야 함 |
| 22 | +3. Slice의 필요한 부분만 외부에 노출되어야 함 |
| 23 | + |
| 24 | +마지막 목표는 실용적 고려사항을 포함합니다. 초기 개발 시 모든 Export를 자동으로 노출하고 싶은 유혹이 있어 Wildcard(*) Re-export를 사용하려 할 수 있습니다: |
| 25 | + |
| 26 | +```js title="Bad practice, features/comments/index.js" |
| 27 | +// ❌ 잘못된 예시 |
| 28 | +export * from "./ui/Comment"; // 👎 사용하지 마세요 |
| 29 | +export * from "./model/comments"; // 💩 나쁜 관행 |
| 30 | +``` |
| 31 | + |
| 32 | +이는 Slice Interface를 모호하게 만들어 발견성과 이해도를 낮춥니다. Interface가 불명확하면 Application 통합을 위해 코드를 깊이 분석해야 합니다. |
| 33 | + |
| 34 | +또한 모듈 내부 구현이 의도치 않게 노출될 수 있어, 외부 코드가 이에 의존하게 되면 Refactoring이 어려워집니다. |
| 35 | + |
| 36 | +## Cross-Import를 위한 Public API {#public-api-for-cross-imports} |
| 37 | + |
| 38 | +Cross-import는 같은 Layer의 한 Slice가 다른 Slice를 Import하는 것입니다. [Layer Import Rule][import-rule-on-layers]으로 금지되지만, 때로는 필요한 경우가 있습니다. |
| 39 | + |
| 40 | +예를 들어, Business Entity들은 실제로 서로 참조하는 경우가 많습니다. 이런 관계를 우회하기보다 코드에 자연스럽게 반영하는 것이 더 적절할 수 있습니다. |
| 41 | + |
| 42 | +이를 위해 `@x-` 표기법의 특별한 Public API를 사용할 수 있습니다. Entity A와 B가 있고 B가 A의 일부를 Import해야 한다면, A는 B를 위한 전용 Public API를 선언할 수 있습니다: |
| 43 | + |
| 44 | +- `📂 entities` |
| 45 | + - `📂 A` |
| 46 | + - `📂 @x` |
| 47 | + - `📄 B.ts` — `entities/B/` 전용 Public API |
| 48 | + - `📄 index.ts` — 일반 Public API |
| 49 | + |
| 50 | +이제 `entities/B/` 코드는 `entities/A/@x/B`에서 필요한 부분을 Import할 수 있습니다: |
| 51 | + |
| 52 | +```ts |
| 53 | +import type { EntityA } from "entities/A/@x/B"; |
| 54 | +``` |
| 55 | + |
| 56 | +`A/@x/B`는 'A와 B의 교차'를 의미합니다. |
| 57 | + |
| 58 | +:::note |
| 59 | + |
| 60 | +Cross-import는 최소화해야 하며, **이 표기법은 Entity Layer에서만 사용**하세요. Cross-import 제거가 비효율적이거나 비현실적일 수 있기 때문입니다. |
| 61 | + |
| 62 | +::: |
| 63 | + |
| 64 | +## Index File의 문제점 |
| 65 | + |
| 66 | +Index File(Barrel File)은 Public API 정의의 일반적 방법이지만, 특정 Bundler나 Framework에서 문제를 일으킬 수 있습니다. |
| 67 | + |
| 68 | +### Circular Import |
| 69 | + |
| 70 | +Circular Import는 파일들이 서로를 순환적으로 Import하는 경우입니다. |
| 71 | + |
| 72 | +<!-- TODO: add backgrounds to the images below, check on mobile --> |
| 73 | + |
| 74 | +<figure> |
| 75 | + <img src="/img/circular-import-light.svg#light-mode-only" width="60%" alt="세 파일이 서로 원형으로 import하는 모습" /> |
| 76 | + <img src="/img/circular-import-dark.svg#dark-mode-only" width="60%" alt="세 파일이 서로를 원형으로 import하고 있는 예시입니다." /> |
| 77 | + <figcaption> |
| 78 | + 위 그림: `fileA.js`, `fileB.js`, `fileC.js` 파일의 Circular Import 예시 |
| 79 | + </figcaption> |
| 80 | +</figure> |
| 81 | + |
| 82 | +이는 Bundler가 처리하기 어렵고 Runtime Error의 원인이 될 수 있습니다. |
| 83 | + |
| 84 | +Index File 사용 시 Circular Import가 발생하기 쉽습니다. 특히 Slice의 Public API에서 여러 Object를 노출할 때 자주 발생합니다. |
| 85 | + |
| 86 | +예시) `HomePage`와 `loadUserStatistics`가 Public API로 노출되고 `HomePage`가 `loadUserStatistics`에 접근해야 할 때: |
| 87 | + |
| 88 | +```jsx title="pages/home/ui/HomePage.jsx" |
| 89 | +import { loadUserStatistics } from "../"; // pages/home/index.js에서 import |
| 90 | + |
| 91 | +export function HomePage() { /* … */ } |
| 92 | +``` |
| 93 | + |
| 94 | +```js title="pages/home/index.js" |
| 95 | +export { HomePage } from "./ui/HomePage"; |
| 96 | +export { loadUserStatistics } from "./api/loadUserStatistics"; |
| 97 | +``` |
| 98 | + |
| 99 | +이는 Circular Import를 생성합니다: `index.js`가 `ui/HomePage.jsx`를 Import하고, `ui/HomePage.jsx`가 다시 `index.js`를 Import합니다. |
| 100 | + |
| 101 | +해결을 위한 두 가지 원칙: |
| 102 | +- 같은 Slice 내: 항상 Relative Path Import 사용, 전체 경로 명시 |
| 103 | +- 다른 Slice Import: 항상 Alias 등의 Absolute Import 사용 |
| 104 | + |
| 105 | +### Shared의 Large Bundle과 Tree-shaking 문제 {#large-bundles} |
| 106 | + |
| 107 | +일부 Bundler는 모든 것을 Re-export하는 Index File이 있을 때 Tree-shaking(미사용 코드 제거)을 제대로 수행하지 못할 수 있습니다. |
| 108 | + |
| 109 | +일반적으로 Public API에서는 큰 문제가 되지 않습니다. Module 내용이 밀접하게 연관되어 있어 하나를 Import하면 다른 것들도 필요한 경우가 많기 때문입니다. 하지만 FSD의 Public API Rule은 `shared/ui`와 `shared/lib`에서 문제가 될 수 있습니다. |
| 110 | + |
| 111 | +이 두 폴더는 보통 연관성이 적은 Component들의 집합입니다. 예를 들어, `shared/ui`는 UI Library의 모든 Component를 포함할 수 있습니다: |
| 112 | + |
| 113 | + |
| 114 | +- `📂 shared/ui/` |
| 115 | + - `📁 button` |
| 116 | + - `📁 text-field` |
| 117 | + - `📁 carousel` |
| 118 | + - `📁 accordion` |
| 119 | + |
| 120 | +Syntax Highlighter나 Drag-and-Drop Library 같은 Heavy Dependency가 있을 때 문제가 더 심각해집니다. `shared/ui`에서 Button 같은 간단한 Component를 사용하는 모든 Page에 이런 Heavy Dependency가 포함되는 것은 피해야 합니다. |
| 121 | + |
| 122 | +`shared/ui`나 `shared/lib`의 단일 Public API로 인해 Bundle Size가 커진다면, 각 Component나 Library에 대해 별도의 Index File을 만드는 것이 좋습니다: |
| 123 | + |
| 124 | +- `📂 shared/ui/` |
| 125 | + - `📂 button` |
| 126 | + - `📄 index.js` |
| 127 | + - `📂 text-field` |
| 128 | + - `📄 index.js` |
| 129 | + |
| 130 | +이렇게 하면 다음과 같이 직접 Import가 가능합니다: |
| 131 | + |
| 132 | +```js title="pages/sign-in/ui/SignInPage.jsx" |
| 133 | +import { Button } from '@/shared/ui/button'; |
| 134 | +import { TextField } from '@/shared/ui/text-field'; |
| 135 | +``` |
| 136 | + |
| 137 | +### Public API 우회 방지의 한계 |
| 138 | + |
| 139 | +Slice에 Index File을 추가해도 직접 Import를 막을 수는 없습니다. 특히 IDE의 Auto Import 기능에서 문제가 됩니다. Import 가능한 여러 경로 중 IDE가 직접 Import를 선택하여 Slice의 Public API Rule을 위반할 수 있습니다. |
| 140 | + |
| 141 | +이 문제를 자동으로 감지하고 방지하려면 FSD용 Architecture Linter인 [Steiger][ext-steiger]를 사용하세요. |
| 142 | + |
| 143 | +### Large Project에서의 Bundler 성능 문제 |
| 144 | + |
| 145 | +TkDodo의 ["Please Stop Using Barrel Files"][ext-please-stop-using-barrel-files] 글처럼, 많은 Index File은 Development Server 속도를 저하시킬 수 있습니다. |
| 146 | + |
| 147 | +해결 방안: |
| 148 | +1. ["Shared의 Large Bundle 문제"](#large-bundles) 조언을 따르세요. `shared/ui`와 `shared/lib`에 하나의 큰 Index File 대신 각 Component/Library별 Index File을 사용하세요. |
| 149 | +2. Slice Layer의 Segment에서 Index File 생성을 피하세요. |
| 150 | + 예) "comments" Feature의 `📄 features/comments/index.js`가 있다면, `📄 features/comments/ui/index.js` 같은 추가 Index File은 불필요 |
| 151 | + |
| 152 | +3. 대규모 프로젝트는 여러 큰 Chunk로 분할을 고려하세요. |
| 153 | + 예) Google Docs처럼 Document Editor와 File Browser를 분리. Monorepo로 각 Package가 독립적 Layer 구조를 가진 FSD Root가 되도록 구성: |
| 154 | + - 일부 Package는 Shared와 Entity Layer만 포함 |
| 155 | + - 다른 Package는 Page와 App Layer만 포함 |
| 156 | + - 또 다른 Package는 자체 작은 Shared와 다른 Package의 큰 Shared 활용 가능 |
| 157 | + |
| 158 | +<!-- TODO: add a link to a page that explains this in more detail (when one will exist) --> |
| 159 | + |
| 160 | +<!-- TODO: discuss issues with mixing server/client code in Next/Remix --> |
| 161 | + |
| 162 | +[import-rule-on-layers]: /docs/reference/layers#import-rule-on-layers |
| 163 | +[ext-steiger]: https://github.com/feature-sliced/steiger |
| 164 | +[ext-please-stop-using-barrel-files]: https://tkdodo.eu/blog/please-stop-using-barrel-files |
0 commit comments