# Feature-Sliced Design ## documentation - [예제](/examples.md): Feature‑Sliced Design으로 제작된 웹사이트 모음 - [🧭 내비게이션](/nav.md): Feature-Sliced Design Navigation help page - [Search the documentation](/search.md) - [Feature‑Sliced Design 버전](/versions.md): Feature-Sliced Design Versions page listing all documented site versions - [💫 Community](/community.md): Community resources, additional materials - [Team](/community/team.md): Core-team - [Alternatives](/docs/about/alternatives.md): History of architecture approaches - [Mission](/docs/about/mission.md): 이 문서는 우리가 방법론을 개발할 때 따르는 목표와 적용 가능성의 한계를 설명합니다. - [Motivation](/docs/about/motivation.md): Feature-Sliced Design은 여러 개발자들의 연구와 경험을 결합하여 - [Promote in company](/docs/about/promote/for-company.md): Do the project and the company need a methodology? - [Promote in team](/docs/about/promote/for-team.md): - Onboard newcomers - [Integration aspects](/docs/about/promote/integration.md): Summary - [Partial Application](/docs/about/promote/partial-application.md): How to partially apply the methodology? Does it make sense? What if I ignore it? - [Abstractions](/docs/about/understanding/abstractions.md): The law of leaky abstractions - [About architecture](/docs/about/understanding/architecture.md): 문제점들 - [Knowledge types](/docs/about/understanding/knowledge-types.md): 소프트웨어 프로젝트를 개발할 때 다루게 되는 지식은 크게 세 가지로 나눌 수 있습니다: - [Naming](/docs/about/understanding/naming.md): 개발자들은 각자의 경험과 관점에 따라 같은 대상을 다르게 부르는 경우가 많습니다. 이는 팀 내에서 혼동을 유발할 수 있습니다. 예를 들어: - [Needs driven](/docs/about/understanding/needs-driven.md): 새로운 Feature의 목표가 불분명하거나 작업 정의가 모호한가요? **이 방법론의 핵심은 작업과 목표를 명확히 정의하는 데 있습니다.** - [Signals of architecture](/docs/about/understanding/signals.md): If there is a limitation on the part of the architecture, then there are obvious reasons for this, and consequences if they are ignored - [Branding Guidelines](/docs/branding.md): FSD's visual identity is based on its core-concepts: Layered, Sliced self-contained parts, Parts & Compose, Segmented. - [Decomposition cheatsheet](/docs/get-started/cheatsheet.md): Use this as a quick reference when you're deciding how to decompose your UI. PDF versions are also available below, so you can print it out and keep one under your pillow. - [FAQ](/docs/get-started/faq.md): 질문은 언제든 Telegram, Discord, GitHub Discussions에서 남겨 주세요. - [개요](/docs/get-started/overview.md): Feature-Sliced Design (FSD) 는 프론트엔드 애플리케이션 구조를 위한 아키텍처 방법론입니다. - [튜토리얼](/docs/get-started/tutorial.md): Part 1. 설계 - [Handling API Requests](/docs/guides/examples/api-requests.md): Shared API Requests - [Authentication](/docs/guides/examples/auth.md): 일반적으로 인증(Authentication) 플로우는 세 단계로 구성됩니다. - [Autocomplete](/docs/guides/examples/autocompleted.md): About decomposition by layers - [Browser API](/docs/guides/examples/browser-api.md): About working with the Browser API: localStorage, audio Api, bluetooth API, etc. - [CMS](/docs/guides/examples/cms.md): Features may be different - [Feedback](/docs/guides/examples/feedback.md): Errors, Alerts, Notifications, ... - [i18n](/docs/guides/examples/i18n.md): Where to place it? How to work with this? - [Metric](/docs/guides/examples/metric.md): About ways to initialize metrics in the application - [Monorepositories](/docs/guides/examples/monorepo.md): About applicability for mono repositories, about bff, about microapps - [Page layouts](/docs/guides/examples/page-layout.md): 여러 페이지에서 동일한 공통 layout(header, sidebar, footer) 을 사용하고, - [Desktop/Touch platforms](/docs/guides/examples/platforms.md): About the application of the methodology for desktop/touch - [SSR](/docs/guides/examples/ssr.md): About the implementation of SSR using the methodology - [Theme](/docs/guides/examples/theme.md): Where should I put my work with the theme and palette? - [Types](/docs/guides/examples/types.md): 이 가이드는 TypeScript 같은 정적 타입 언어에서 데이터를 정의·활용하는 방법과, FSD 구조 내에서 타입을 어디에 배치할지 설명합니다. - [White Labels](/docs/guides/examples/white-labels.md): Figma, brand uikit, templates, adaptability to brands - [Cross-import](/docs/guides/issues/cross-imports.md): Cross-import는 Layer나 추상화가 원래의 책임 범위를 넘어설 때 발생합니다. 방법론에서는 이러한 Cross-import를 해결하기 위한 별도의 Layer를 정의합니다. - [Desegmentation](/docs/guides/issues/desegmented.md): 상황 - [Routing](/docs/guides/issues/routes.md): 상황 - [기존 아키텍처에서 FSD로의 마이그레이션](/docs/guides/migration/from-custom.md): 이 가이드는 기존 아키텍처를 Feature-Sliced Design(FSD) 으로 단계별 전환하는 방법을 설명합니다. - [v1 -> v2 마이그레이션 가이드](/docs/guides/migration/from-v1.md): v2 도입 배경 - [v2.0 -> v2.1 마이그레이션 가이드](/docs/guides/migration/from-v2-0.md): v2.1의 핵심 변화는 Page 중심(Page-First) 접근 방식을 통한 인터페이스 구조화입니다. - [Electron와 함께 사용하기](/docs/guides/tech/with-electron.md): Electron 애플리케이션은 역할이 다른 여러 프로세스(Main, Renderer, Preload)로 구성됩니다. - [NextJS와 함께 사용하기](/docs/guides/tech/with-nextjs.md): NextJS 프로젝트에도 FSD 아키텍처를 적용할 수 있지만, 구조적 차이로 두 가지 충돌이 발생합니다. - [NuxtJS와 함께 사용하기](/docs/guides/tech/with-nuxtjs.md): NuxtJS 프로젝트에 FSD(Feature-Sliced Design)를 도입할 때는 기본 구조와 FSD 원칙 간에 다음과 같은 차이를 고려해야 합니다: - [React Query와 함께 사용하기](/docs/guides/tech/with-react-query.md): Query Key 배치 문제 - [SvelteKit와 함께 사용하기](/docs/guides/tech/with-sveltekit.md): SvelteKit 프로젝트에 FSD(Feature-Sliced Design)를 적용할 때는 다음 차이를 유의하세요: - [Docs for LLMs](/docs/llms.md): This page provides links and guidance for LLM crawlers. - [Layer](/docs/reference/layers.md): Layer는 Feature-Sliced Design에서 코드를 구분하는 가장 큰 범위입니다. - [Public API](/docs/reference/public-api.md): Public API는 Slice 기능을 외부에서 사용할 수 있는 공식 경로입니다. - [Slices and segments](/docs/reference/slices-segments.md): Slice - [Feature-Sliced Design](/index.md): Architectural methodology for frontend projects --- # Full Documentation Content v2 ![](/documentation/kr/assets/ideal-img/tiny-bunny.dd60f55.640.png) Tiny Bunny Mini Game Mini-game "21 points" in the universe of the visual novel "Tiny Bunny". reactredux-toolkittypescript [Website](https://sanua356.github.io/tiny-bunny/)[Source](https://github.com/sanua356/tiny-bunny) --- # 🧭 내비게이션 ## 이전 경로 문서 구조가 바뀌어 일부 경로가 변경되었습니다. 아래에서 원하는 페이지를 찾을 수 있습니다. 기존 링크는 호환성을 위해 리디렉션됩니다. ### 🚀 Get Started ⚡️ Simplified and merged [Tutorial](/documentation/kr/docs/get-started/tutorial.md) [**old**:](/documentation/kr/docs/get-started/tutorial.md) [/docs/get-started/quick-start](/documentation/kr/docs/get-started/tutorial.md) [**new**: ](/documentation/kr/docs/get-started/tutorial.md) [/docs/get-started/tutorial](/documentation/kr/docs/get-started/tutorial.md) [Basics](/documentation/kr/docs/get-started/overview.md) [**old**:](/documentation/kr/docs/get-started/overview.md) [/docs/get-started/basics](/documentation/kr/docs/get-started/overview.md) [**new**: ](/documentation/kr/docs/get-started/overview.md) [/docs/get-started/overview](/documentation/kr/docs/get-started/overview.md) [Decompose Cheatsheet](/documentation/kr/docs/get-started/cheatsheet.md) [**old**:](/documentation/kr/docs/get-started/cheatsheet.md) [/docs/get-started/tutorial/decompose; /docs/get-started/tutorial/design-mockup; /docs/get-started/onboard/cheatsheet](/documentation/kr/docs/get-started/cheatsheet.md) [**new**: ](/documentation/kr/docs/get-started/cheatsheet.md) [/docs/get-started/cheatsheet](/documentation/kr/docs/get-started/cheatsheet.md) ### 🍰 Alternatives ⚡️ Moved and merged to /about/alternatives as advanced materials [Architecture approaches alternatives](/documentation/kr/docs/about/alternatives.md) [**old**:](/documentation/kr/docs/about/alternatives.md) [/docs/about/alternatives/big-ball-of-mud; /docs/about/alternatives/design-principles; /docs/about/alternatives/ddd; /docs/about/alternatives/clean-architecture; /docs/about/alternatives/frameworks; /docs/about/alternatives/atomic-design; /docs/about/alternatives/smart-dumb-components; /docs/about/alternatives/feature-driven](/documentation/kr/docs/about/alternatives.md) [**new**: ](/documentation/kr/docs/about/alternatives.md) [/docs/about/alternatives](/documentation/kr/docs/about/alternatives.md) ### 🍰 Promote & Understanding ⚡️ Moved to /about as advanced materials [Knowledge types](/documentation/kr/docs/about/understanding/knowledge-types.md) [**old**:](/documentation/kr/docs/about/understanding/knowledge-types.md) [/docs/reference/knowledge-types](/documentation/kr/docs/about/understanding/knowledge-types.md) [**new**: ](/documentation/kr/docs/about/understanding/knowledge-types.md) [/docs/about/understanding/knowledge-types](/documentation/kr/docs/about/understanding/knowledge-types.md) [Needs driven](/documentation/kr/docs/about/understanding/needs-driven.md) [**old**:](/documentation/kr/docs/about/understanding/needs-driven.md) [/docs/concepts/needs-driven](/documentation/kr/docs/about/understanding/needs-driven.md) [**new**: ](/documentation/kr/docs/about/understanding/needs-driven.md) [/docs/about/understanding/needs-driven](/documentation/kr/docs/about/understanding/needs-driven.md) [About architecture](/documentation/kr/docs/about/understanding/architecture.md) [**old**:](/documentation/kr/docs/about/understanding/architecture.md) [/docs/concepts/architecture](/documentation/kr/docs/about/understanding/architecture.md) [**new**: ](/documentation/kr/docs/about/understanding/architecture.md) [/docs/about/understanding/architecture](/documentation/kr/docs/about/understanding/architecture.md) [Naming adaptability](/documentation/kr/docs/about/understanding/naming.md) [**old**:](/documentation/kr/docs/about/understanding/naming.md) [/docs/concepts/naming-adaptability](/documentation/kr/docs/about/understanding/naming.md) [**new**: ](/documentation/kr/docs/about/understanding/naming.md) [/docs/about/understanding/naming](/documentation/kr/docs/about/understanding/naming.md) [Signals of architecture](/documentation/kr/docs/about/understanding/signals.md) [**old**:](/documentation/kr/docs/about/understanding/signals.md) [/docs/concepts/signals](/documentation/kr/docs/about/understanding/signals.md) [**new**: ](/documentation/kr/docs/about/understanding/signals.md) [/docs/about/understanding/signals](/documentation/kr/docs/about/understanding/signals.md) [Abstractions of architecture](/documentation/kr/docs/about/understanding/abstractions.md) [**old**:](/documentation/kr/docs/about/understanding/abstractions.md) [/docs/concepts/abstractions](/documentation/kr/docs/about/understanding/abstractions.md) [**new**: ](/documentation/kr/docs/about/understanding/abstractions.md) [/docs/about/understanding/abstractions](/documentation/kr/docs/about/understanding/abstractions.md) ### 📚 Reference guidelines (isolation & units) ⚡️ Moved to /reference as theoretical materials (old concepts) [Decouple of entities](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/decouple-entities](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [Low Coupling & High Cohesion](/documentation/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**old**:](/documentation/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/concepts/low-coupling](/documentation/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**new**: ](/documentation/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/reference/slices-segments#zero-coupling-high-cohesion](/documentation/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [Cross-communication](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/cross-communication](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [App splitting](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/concepts/app-splitting](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Decomposition](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units/decomposition](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Units](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Layers](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units/layers](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Layer overview](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers/overview](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [App layer](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units/layers/app](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Processes layer](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units/layers/processes](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Pages layer](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units/layers/pages](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Widgets layer](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units/layers/widgets](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Widgets layer](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers/widgets](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Features layer](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units/layers/features](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Entities layer](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units/layers/entities](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Shared layer](/documentation/kr/docs/reference/layers.md) [**old**:](/documentation/kr/docs/reference/layers.md) [/docs/reference/units/layers/shared](/documentation/kr/docs/reference/layers.md) [**new**: ](/documentation/kr/docs/reference/layers.md) [/docs/reference/layers](/documentation/kr/docs/reference/layers.md) [Segments](/documentation/kr/docs/reference/slices-segments.md) [**old**:](/documentation/kr/docs/reference/slices-segments.md) [/docs/reference/units/segments](/documentation/kr/docs/reference/slices-segments.md) [**new**: ](/documentation/kr/docs/reference/slices-segments.md) [/docs/reference/slices-segments](/documentation/kr/docs/reference/slices-segments.md) ### 🎯 Bad Practices handbook ⚡️ Moved to /guides as practice materials [Cross-imports](/documentation/kr/docs/guides/issues/cross-imports.md) [**old**:](/documentation/kr/docs/guides/issues/cross-imports.md) [/docs/concepts/issues/cross-imports](/documentation/kr/docs/guides/issues/cross-imports.md) [**new**: ](/documentation/kr/docs/guides/issues/cross-imports.md) [/docs/guides/issues/cross-imports](/documentation/kr/docs/guides/issues/cross-imports.md) [Desegmented](/documentation/kr/docs/guides/issues/desegmented.md) [**old**:](/documentation/kr/docs/guides/issues/desegmented.md) [/docs/concepts/issues/desegmented](/documentation/kr/docs/guides/issues/desegmented.md) [**new**: ](/documentation/kr/docs/guides/issues/desegmented.md) [/docs/guides/issues/desegmented](/documentation/kr/docs/guides/issues/desegmented.md) [Routes](/documentation/kr/docs/guides/issues/routes.md) [**old**:](/documentation/kr/docs/guides/issues/routes.md) [/docs/concepts/issues/routes](/documentation/kr/docs/guides/issues/routes.md) [**new**: ](/documentation/kr/docs/guides/issues/routes.md) [/docs/guides/issues/routes](/documentation/kr/docs/guides/issues/routes.md) ### 🎯 Examples ⚡️ Grouped and simplified into /guides/examples as practical examples [Viewer logic](/documentation/kr/docs/guides/examples/auth.md) [**old**:](/documentation/kr/docs/guides/examples/auth.md) [/docs/guides/examples/viewer](/documentation/kr/docs/guides/examples/auth.md) [**new**: ](/documentation/kr/docs/guides/examples/auth.md) [/docs/guides/examples/auth](/documentation/kr/docs/guides/examples/auth.md) [Monorepo](/documentation/kr/docs/guides/examples/monorepo.md) [**old**:](/documentation/kr/docs/guides/examples/monorepo.md) [/docs/guides/monorepo](/documentation/kr/docs/guides/examples/monorepo.md) [**new**: ](/documentation/kr/docs/guides/examples/monorepo.md) [/docs/guides/examples/monorepo](/documentation/kr/docs/guides/examples/monorepo.md) [White Labels](/documentation/kr/docs/guides/examples/white-labels.md) [**old**:](/documentation/kr/docs/guides/examples/white-labels.md) [/docs/guides/white-labels](/documentation/kr/docs/guides/examples/white-labels.md) [**new**: ](/documentation/kr/docs/guides/examples/white-labels.md) [/docs/guides/examples/white-labels](/documentation/kr/docs/guides/examples/white-labels.md) ### 🎯 Migration ⚡️ Grouped and simplified into /guides/migration as migration guidelines [Migration from V1](/documentation/kr/docs/guides/migration/from-v1.md) [**old**:](/documentation/kr/docs/guides/migration/from-v1.md) [/docs/guides/migration-from-v1](/documentation/kr/docs/guides/migration/from-v1.md) [**new**: ](/documentation/kr/docs/guides/migration/from-v1.md) [/docs/guides/migration/from-v1](/documentation/kr/docs/guides/migration/from-v1.md) [Migration from Legacy](/documentation/kr/docs/guides/migration/from-custom.md) [**old**:](/documentation/kr/docs/guides/migration/from-custom.md) [/docs/guides/migration-from-legacy](/documentation/kr/docs/guides/migration/from-custom.md) [**new**: ](/documentation/kr/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/documentation/kr/docs/guides/migration/from-custom.md) ### 🎯 Tech ⚡️ Grouped into /guides/tech as tech-specific usage guidelines [Usage with NextJS](/documentation/kr/docs/guides/tech/with-nextjs.md) [**old**:](/documentation/kr/docs/guides/tech/with-nextjs.md) [/docs/guides/usage-with-nextjs](/documentation/kr/docs/guides/tech/with-nextjs.md) [**new**: ](/documentation/kr/docs/guides/tech/with-nextjs.md) [/docs/guides/tech/with-nextjs](/documentation/kr/docs/guides/tech/with-nextjs.md) ### Rename 'legacy' to 'custom' ⚡️ 'Legacy' is derogatory, we don't get to call people's projects legacy [Rename 'legacy' to custom](/documentation/kr/docs/guides/migration/from-custom.md) [**old**:](/documentation/kr/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-legacy](/documentation/kr/docs/guides/migration/from-custom.md) [**new**: ](/documentation/kr/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/documentation/kr/docs/guides/migration/from-custom.md) ### Deduplication of Reference ⚡️ Cleaned up the Reference section and deduplicated the material [Isolation of modules](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/isolation](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) --- [주요 콘텐츠로 건너뛰기](#__docusaurus_skipToContent_fallback) [![logo](/documentation/kr/img/brand/logo-primary.png)![logo](/documentation/kr/img/brand/logo-primary.png)](/documentation/kr/.md) [****](/documentation/kr/.md)[📖 Docs](/documentation/kr/docs/get-started/overview.md)[💫 Community](/documentation/kr/community.md)[📝 Blog](/documentation/kr/blog)[🛠 Examples](/documentation/kr/examples.md) [v2.1](/documentation/kr/docs/get-started/overview.md) * [v2.1](/documentation/kr/docs/get-started/overview.md) * [v1.0](https://feature-sliced.github.io/featureslices.dev/v1.0.html) * [v0.1](https://feature-sliced.github.io/featureslices.dev/v0.1.html) * [feature-driven](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) * [All versions](/documentation/kr/versions.md) [한국어](#) * [Русский](/documentation/ru/search) * [English](/documentation/search) * [O'zbekcha](/documentation/uz/search) * [한국어](/documentation/kr/search.md) * [日本語](/documentation/ja/search) * [Help Us Translate](https://github.com/feature-sliced/documentation/issues/244) [](https://discord.gg/S8MzWTUsmp)[](https://github.com/feature-sliced/documentation) Search # Search the documentation Type your search here [](https://www.algolia.com/) Specs * [Documentation](/documentation/kr/docs/get-started/overview.md) * [Community](/documentation/kr/community.md) * [Help](/documentation/kr/nav.md) * [Discussions](https://github.com/feature-sliced/documentation/discussions) Community * [Discord](https://discord.gg/S8MzWTUsmp) * [Telegram (RU)](https://t.me/feature_sliced) * [Twitter](https://twitter.com/feature_sliced) * [Open Collective](https://opencollective.com/feature-sliced) * [YouTube](https://www.youtube.com/c/FeatureSlicedDesign) More * [GitHub](https://github.com/feature-sliced) * [Contribution Guide](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md) * [License](https://github.com/feature-sliced/documentation/blob/master/LICENSE) * [Docs for LLMs](/documentation/kr/docs/llms.md) [![Feature-Sliced Design - Architectural methodology for frontend projects](/documentation/kr/img/brand/logo-primary.png)![Feature-Sliced Design - Architectural methodology for frontend projects](/documentation/kr/img/brand/logo-primary.png)](https://github.com/feature-sliced) Copyright © 2025 Feature-Sliced Design --- # Feature‑Sliced Design 버전 ### Feature-Sliced Design v2.1 (Current) 현재 배포된 문서는 여기에서 확인할 수 있습니다. | v2.1 | [Release Notes](https://github.com/feature-sliced/documentation/releases/tag/v2.1) | [Documentation](/documentation/kr/docs/get-started/overview.md) | [Migration from v1](/documentation/kr/docs/guides/migration/from-v1.md) | [Migration from v2.0](/documentation/kr/docs/guides/migration/from-v1.md) | | ---- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- | ### Feature Slices v1 (Legacy) feature-slices의 이전 문서는 여기에서 확인할 수 있습니다. | v1.0 | [Documentation](https://feature-sliced.github.io/featureslices.dev/v1.0.html) | | ---- | ----------------------------------------------------------------------------- | | v0.1 | [Documentation](https://feature-sliced.github.io/featureslices.dev/v0.1.html) | ### Feature Driven (Legacy) feature-driven의 이전 문서는 여기에서 확인할 수 있습니다. | v0.1 | [Documentation](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) | | ------------- | --------------------------------------------------------------------------------------- | | Example (kof) | [Github](https://github.com/kof/feature-driven-architecture) | --- # 💫 Community Community resources, additional materials ## Main[​](#main "해당 헤딩으로 이동") [Awesome Resources](https://github.com/feature-sliced/awesome) [A curated list of awesome FSD videos, articles, packages](https://github.com/feature-sliced/awesome) [Team](/documentation/kr/community/team.md) [Core-team, Champions, Contributors, Companies](/documentation/kr/community/team.md) [Brandbook](/documentation/kr/docs/branding.md) [Recommendations for FSD's branding usage](/documentation/kr/docs/branding.md) [Contributing](#) [HowTo, Workflow, Support](#) --- # Team WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/192) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Core-team[​](#core-team "해당 헤딩으로 이동") ### Champions[​](#champions "해당 헤딩으로 이동") ## Contributors[​](#contributors "해당 헤딩으로 이동") ## Companies[​](#companies "해당 헤딩으로 이동") --- # Alternatives WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/62) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* History of architecture approaches ## Big Ball of Mud[​](#big-ball-of-mud "해당 헤딩으로 이동") WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/258) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > What is it; Why is it so common; When it starts to bring problems; What to do and how does FSD help in this * [(Article) Oleg Isonen - Last words on UI architecture before an AI takes over](https://oleg008.medium.com/last-words-on-ui-architecture-before-an-ai-takes-over-468c78f18f0d) * [(Report) Julia Nikolaeva, iSpring - Big Ball of Mud and other problems of the monolith, we have handled](http://youtu.be/gna4Ynz1YNI) * [(Article) DD - Big Ball of mud](https://thedomaindrivendesign.io/big-ball-of-mud/) ## Smart & Dumb components[​](#smart--dumb-components "해당 헤딩으로 이동") WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/214) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About the approach; About applicability in the frontend; Methodology position About obsolescence, about a new view from the methodology Why component-containers approach is evil? * [(Article) Den Abramov-Presentation and Container Components (TLDR: deprecated)](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) ## Design Principles[​](#design-principles "해당 헤딩으로 이동") WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/59) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > What are we talking about; FSD position SOLID, GRASP, KISS, YAGNI, ... - and why they don't work well together in practice And how does it aggregate these practices * [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Design Principles)](https://youtu.be/SnzPAr_FJ7w?t=380) ## DDD[​](#ddd "해당 헤딩으로 이동") WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/1) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About the approach; Why does it work poorly in practice What is the difference, how does it improve applicability, where does it adopt practices * [(Article) DDD, Hexagonal, Onion, Clean, CQRS, ... How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) * [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Clean Architecture, DDD)](https://youtu.be/SnzPAr_FJ7w?t=528) ## Clean Architecture[​](#clean-architecture "해당 헤딩으로 이동") WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/165) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About the approach; About applicability in the frontend; FSD position How are they similar (to many), how are they different * [(Thread) About use-case/interactor in the methodology](https://t.me/feature_sliced/3897) * [(Thread) About DI in the methodology](https://t.me/feature_sliced/4592) * [(Article) Alex Bespoyasov - Clean Architecture on frontend](https://bespoyasov.me/blog/clean-architecture-on-frontend/) * [(Article) DDD, Hexagonal, Onion, Clean, CQRS, ... How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) * [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Clean Architecture, DDD)](https://youtu.be/SnzPAr_FJ7w?t=528) * [(Article) Misconceptions of Clean Architecture](http://habr.com/ru/company/mobileup/blog/335382/) ## Frameworks[​](#frameworks "해당 헤딩으로 이동") WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/58) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About applicability in the frontend; Why frameworks do not solve problems; why there is no single approach; FSD position Framework-agnostic, conventional-approach * [(Article) About the reasons for creating the methodology (fragment about frameworks)](/documentation/kr/docs/about/motivation.md) * [(Thread) About the applicability of the methodology for different frameworks](https://t.me/feature_sliced/3867) ## Atomic Design[​](#atomic-design "해당 헤딩으로 이동") ### What is it?[​](#what-is-it "해당 헤딩으로 이동") In Atomic Design, the scope of responsibility is divided into standardized layers. Atomic Design is broken down into **5 layers** (from top to bottom): 1. `pages` - Functionality similar to the `pages` layer in FSD. 2. `templates` - Components that define the structure of a page without tying to specific content. 3. `organisms` - Modules consisting of molecules that have business logic. 4. `molecules` - More complex components that generally do not contain business logic. 5. `atoms` - UI components without business logic. Modules at one layer interact only with modules in the layers below, similar to FSD. That is, molecules are built from atoms, organisms from molecules, templates from organisms, and pages from templates. Atomic Design also implies the use of Public API within modules for isolation. ### Applicability to frontend[​](#applicability-to-frontend "해당 헤딩으로 이동") Atomic Design is relatively common in projects. Atomic Design is more popular among web designers than in development. Web designers often use Atomic Design to create scalable and easily maintainable designs. In development, Atomic Design is often mixed with other architectural methodologies. However, since Atomic Design focuses on UI components and their composition, a problem arises with implementing business logic within the architecture. The problem is that Atomic Design does not provide a clear level of responsibility for business logic, leading to its distribution across various components and levels, complicating maintenance and testing. The business logic becomes blurred, making it difficult to clearly separate responsibilities and rendering the code less modular and reusable. ### How does it relate to FSD?[​](#how-does-it-relate-to-fsd "해당 헤딩으로 이동") In the context of FSD, some elements of Atomic Design can be applied to create flexible and scalable UI components. The `atoms` and `molecules` layers can be implemented in `shared/ui` in FSD, simplifying the reuse and maintenance of basic UI elements. ``` ├── shared │ ├── ui │ │ ├── atoms │ │ ├── molecules │ ... ``` A comparison of FSD and Atomic Design shows that both methodologies strive for modularity and reusability but focus on different aspects. Atomic Design is oriented towards visual components and their composition. FSD focuses on dividing the application's functionality into independent modules and their interconnections. * [Atomic Design Methodology](https://atomicdesign.bradfrost.com/table-of-contents/) * [(Thread) About applicability in shared / ui](https://t.me/feature_sliced/1653) * [(Video) Briefly about Atomic Design](https://youtu.be/Yi-A20x2dcA) * [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Atomic Design)](https://youtu.be/SnzPAr_FJ7w?t=587) ## Feature Driven[​](#feature-driven "해당 헤딩으로 이동") WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/219) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About the approach; About applicability in the frontend; FSD position About compatibility, historical development and comparison * [(Talk) Oleg Isonen - Feature Driven Architecture](https://youtu.be/BWAeYuWFHhs) * [Feature Driven-Short specification (from the point of view of FSD)](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) --- # Mission 이 문서는 우리가 방법론을 개발할 때 따르는 **목표와 적용 가능성의 한계**를 설명합니다. * 방법론의 목표는 **이념과 단순성의 균형**을 맞추는 것입니다. * 모든 사람에게 완벽하게 들어맞는 만능 해결책은 존재하지 않습니다. **그럼에도, 방법론은 다양한 개발자들이 쉽게 접근할 수 있고 실용적이어야 합니다.** ## 목표[​](#목표 "해당 헤딩으로 이동") ### 다양한 개발자에게 직관적이고 명확하게[​](#다양한-개발자에게-직관적이고-명확하게 "해당 헤딩으로 이동") 방법론은 프로젝트에 참여하는 대부분의 팀원들이 쉽게 접근하고 이해할 수 있도록 설계되어야 합니다. *향후 새로운 도구가 추가되더라도, 시니어나 리더급 개발자만 이해할 수 있다면 그 방법론은 충분하지 않습니다.* ### 일상적인 문제 해결[​](#일상적인-문제-해결 "해당 헤딩으로 이동") 방법론은 개발 프로젝트에서 자주 발생하는 문제에 대해 **명확한 근거와 해결책**을 제시해야 합니다. **이를 위해 CLI, 린터(linter) 같은 도구들도 함께 제공해야 합니다.** 이를 통해 개발자들은 아키텍처 및 개발 과정에서 발생하는 반복적인 문제를 피하고, 검증된 접근 방식을 활용할 수 있습니다. > *@sergeysova: 방법론을 기반으로 코드를 작성하는 개발자는 이미 많은 문제에 대한 해법을 내장한 채로 시작하기 때문에, 문제 발생 빈도가 10배 정도 줄어들 것이라고 상상해보세요.* ## 한계[​](#한계 "해당 헤딩으로 이동") 우리는 *특정 관점을 강요하지 않으며*, 동시에 *개발자로서의 습관이 문제 해결을 방해할 수 있다는 점*도 이해합니다. 개발자마다 시스템 설계, 개발 경험 수준이 다르기 때문에, **다음 사항을 이해하는 것이 중요합니다:** * **항상 통하지는 않음**
너무 단순하고 명확한 접근이 모든 상황과 모든 사람에게 항상 효과적인 것은 아닙니다. > *@sergeysova: 일부 개념은 직접 문제를 겪고, 오랜 시간 고민하며 해결하는 과정을 거쳐야만 직관적으로 이해할 수 있습니다.* > > * *수학: 그래프 이론* > * *물리학: 양자 역학* > * *프로그래밍: 애플리케이션 아키텍처* * **가능하고 바람직한 방향**
단순함, 확장 가능성 ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [아키텍쳐 문제들](/documentation/kr/docs/about/understanding/architecture.md#problems) --- # Motivation **Feature-Sliced Design**은 [여러 개발자들의 연구와 경험을 결합하여](https://github.com/feature-sliced/documentation/discussions)
복잡하고 점점 더 커지는 프로젝트의 개발을 단순화하고 비용을 줄이려는 아이디어에서 출발했습니다. 물론 이 방법론이 모든 문제를 해결하는 만능은 아니며, [적용상의 한계](/documentation/kr/docs/about/mission.md)가 존재합니다. 그럼에도 불구하고 *이 방법론이 제공하는 실질적인 효용성*에 대해 많은 개발자들이 관심을 가지고 있습니다. note 자세한 논의 내용은 [토론 게시글](https://github.com/feature-sliced/documentation/discussions/27)에서 확인할 수 있습니다. ## 기존 솔루션만으로 부족한 이유[​](#기존-솔루션만으로-부족한-이유 "해당 헤딩으로 이동") > 일반적으로 다음과 같은 반문들이 제기됩니다: * *"이미 `SOLID`, `KISS`, `YAGNI`, `DDD`, `GRASP`, `DRY` 같은 확립된 원칙들이 있는데, 왜 또 다른 방법론이 필요한가?"* * *"문서화, 테스트, 구조화된 프로세스로 충분히 해결할 수 있지 않은가?"* * *"모든 개발자가 위의 원칙을 제대로 따른다면 문제가 생기지 않았을 것이다."* * *"이미 필요한 건 다 발명되었고, 당신이 잘 활용하지 못할 뿐이다."* * *"프레임워크 X를 쓰면 된다. 거기에 다 들어있다."* ### 원칙만으로는 충분하지 않다[​](#원칙만으로는-충분하지-않다 "해당 헤딩으로 이동") **좋은 아키텍처를 위해 원칙이 존재하는 것만으로는 부족합니다.** * 모든 개발자가 이러한 원칙을 깊이 이해하고 올바르게 적용하는 것은 어렵습니다. * 설계 원칙은 일반적인 지침일 뿐, **확장 가능하고 유연한 애플리케이션 구조를 어떻게 설계할 것인가**에 대한 구체적인 답을 주지 않습니다. ### 프로세스가 항상 작동하지는 않는다[​](#프로세스가-항상-작동하지는-않는다 "해당 헤딩으로 이동") *문서화, 테스트, 프로세스* 관리가 중요하지만, 여기에 많은 비용을 투자하더라도
**아키텍처 문제나 신규 인력 온보딩 문제를 완전히 해결하지는 못합니다.** * 문서가 방대해지거나 오래되면 각 개발자가 프로젝트에 빠르게 적응하는 데 큰 도움이 되지 않습니다. * 모든 구성원이 아키텍처를 동일하게 이해하고 있는지 지속적으로 확인하는 데에도 상당한 리소스가 소모됩니다. * bus-factor에 대해서도 잊지 말아야 합니다 ### 기존 프레임워크를 모든 상황에 적용할 수는 없다[​](#기존-프레임워크를-모든-상황에-적용할-수는-없다 "해당 헤딩으로 이동") * 많은 솔루션은 진입 장벽이 높아 새로운 개발자를 투입하기 어렵습니다. * 대부분의 경우 프로젝트 초기에 기술 스택이 이미 정해지므로, **특정 기술에 종속되지 않고 주어진 조건에서 일할 수 있어야 합니다.** > Q: 내 프로젝트에서 `React/Vue/Redux/Effector/Mobx/{당신의_기술}`을 쓸 때, entities 구조와 관계를 어떻게 더 잘 설계할 수 있을까요? ### 결과적으로[​](#결과적으로 "해당 헤딩으로 이동") 각 프로젝트는 시간이 많이 들고 다른 곳에 재사용하기 힘든 *눈송이처럼 독특한* 구조로 남습니다. > @sergeysova: *이것이 현재 프론트엔드 개발의 문제입니다. 각 리드는 제각각의 아키텍처와 구조를 만들지만, 그것이 시간이 지나도 유지될지는 보장할 수 없습니다. 결과적으로 소수의 개발자만 프로젝트를 유지할 수 있고, 새로운 팀원이 합류할 때마다 긴 적응 기간이 필요해집니다.* ## 개발자에게 왜 필요한가?[​](#개발자에게-왜-필요한가 "해당 헤딩으로 이동") ### 아키텍처 고민을 줄이고 비즈니스 기능에 집중[​](#아키텍처-고민을-줄이고-비즈니스-기능에-집중 "해당 헤딩으로 이동") 이 방법론은 아키텍처 설계 부담을 줄여, 개발자가 **비즈니스 로직 구현에 더 집중**할 수 있게 합니다.
동시에 구조를 표준화하여 프로젝트 간 **일관성**을 보장합니다. *커뮤니티에서 신뢰를 얻으려면, 다른 개발자들이 이 방법론을 빠르게 익히고 실제 프로젝트 문제를 해결할 수 있어야 합니다.* ### 경험으로 입증된 솔루션 제공[​](#경험으로-입증된-솔루션-제공 "해당 헤딩으로 이동") 이 방법론은 복잡한 비즈니스 로직을 다루는 데 검증된 해법을 제공합니다.
또한, 실제 사례와 best practices 모음이기도 하므로 개발자들에게 실질적인 도움이 됩니다. ### 프로젝트의 장기적 건강성 유지[​](#프로젝트의-장기적-건강성-유지 "해당 헤딩으로 이동") 이 방법론은 많은 리소스를 들이지 않고도 **기술 부채와 구조적 문제를 미리 감지하고 해결**할 수 있게 돕습니다.
**기술 부채는 시간이 갈수록 누적되며, 이를 관리하는 것은 리드와 팀 전체의 책임입니다.** ## 비즈니스에 왜 필요한가?[​](#비즈니스에-왜-필요한가 "해당 헤딩으로 이동") ### 빠른 온보딩[​](#빠른-온보딩 "해당 헤딩으로 이동") 이 방법론에 익숙한 개발자를 투입하면 **추가 교육 없이도 빠르게 프로젝트에 적응**할 수 있습니다.
덕분에 프로젝트 투입 속도가 빨라지고, 인력 확보에도 유리해집니다. ### 검증된 솔루션 제공[​](#검증된-솔루션-제공 "해당 헤딩으로 이동") 이 방법론은 비즈니스가 직면하는 **시스템 개발 문제에 대한 실질적인 해결책**을 제공합니다.
대부분의 비즈니스는 개발 중 발생하는 문제를 해결할 프레임워크나 솔루션을 필요로 합니다. ### 프로젝트 전 단계에 적용 가능[​](#프로젝트-전-단계에-적용-가능 "해당 헤딩으로 이동") 이 방법론은 운영, 유지보수 단계뿐 아니라 **MVP 단계에서도** 도움이 됩니다. MVP의 목표는 장기 아키텍처가 아닌 **실제 기능 제공**이지만,
방법론의 best practices를 적용하면 제한된 시간 내에서도 합리적인 타협점을 찾을 수 있습니다. *테스팅에도 같은 원리가 적용됩니다.* ## 방법론이 필요하지 않은 경우[​](#방법론이-필요하지-않은-경우 "해당 헤딩으로 이동") * 프로젝트 수명이 짧은 경우 * 지속적인 아키텍처 관리가 필요 없는 경우 * 비즈니스가 코드 품질과 전달 속도의 연관성을 인식하지 못하는 경우 * 신속한 납품이 사후 지원보다 우선인 경우 ### 비즈니스 규모[​](#비즈니스-규모 "해당 헤딩으로 이동") * **소규모** → 즉시 사용 가능한 빠른 솔루션이 필요. 성장하면서 품질, 안정성 투자 필요성을 인식하게 됨. * **중간 규모** → 기능 경쟁 속에서도 품질 개선, 리팩토링, 테스트에 투자하며 확장 가능한 아키텍처를 중시함. * **대규모** → 이미 자체적인 아키텍처 접근 방식을 보유하고 있어 외부 방법론을 새로 도입할 가능성은 낮음. ## 계획[​](#계획 "해당 헤딩으로 이동") 주요 목표는 [여기에 정의되어 있으며](/documentation/kr/docs/about/mission.md#goals), 앞으로 방법론이 나아갈 방향도 함께 고려하고 있습니다. ### 경험 결합[​](#경험-결합 "해당 헤딩으로 이동") 현재 우리는 `core-team`의 다양한 경험을 모아 **더 단단한 방법론**을 만들고 있습니다. 물론 그 결과가 Angular 3.0처럼 될 수도 있지만, 중요한 것은 **복잡한 아키텍처 설계 문제를 깊이 탐구하는 것**입니다. *커뮤니티 경험도 반영하여, 모두가 납득할 수 있는 최적의 합의점을 찾는 것이 목표입니다.* ### 사양을 넘어선 생명력[​](#사양을-넘어선-생명력 "해당 헤딩으로 이동") 모든 것이 계획대로 진행된다면, 이 방법론은 단순히 사양과 툴킷에만 국한되지 않을 것입니다. * 관련 보고서나 기사 * 다른 기술로 마이그레이션할 수 있는 CODE\_MODE * 대규모 솔루션 유지보수자들에게 적용할 기회 * *특히 React는 다른 프레임워크에 비해 구체적인 해결책을 제시하지 않는다는 점이 문제입니다.* ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [(토론) 방법론이 필요하지 않나요?](https://github.com/feature-sliced/documentation/discussions/27) * [방법론의 목표와 한계](/documentation/kr/docs/about/mission.md) * [프로젝트에서 다루는 지식의 유형](/documentation/kr/docs/about/understanding/knowledge-types.md) --- # Promote in company WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/206) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Do the project and the company need a methodology?[​](#do-the-project-and-the-company-need-a-methodology "해당 헤딩으로 이동") > About the justification of the application, Those duty ## How can I submit a methodology to a business?[​](#how-can-i-submit-a-methodology-to-a-business "해당 헤딩으로 이동") ## How to prepare and justify a plan to move to the methodology?[​](#how-to-prepare-and-justify-a-plan-to-move-to-the-methodology "해당 헤딩으로 이동") --- # Promote in team WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/182) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* * Onboard newcomers * Development Guidelines ("where to search N module", etc...) * New approach for tasks ## See also[​](#see-also "해당 헤딩으로 이동") * [(Thread) The simplicity of the old approaches and the importance of mindfulness](https://t.me/feature_sliced/3360) * [(Thread) About the convenience of searching by layers](https://t.me/feature_sliced/1918) --- # Integration aspects ## Summary[​](#summary "해당 헤딩으로 이동") First 5 minutes (RU): [YouTube video player](https://www.youtube.com/embed/TFA6zRO_Cl0?start=2110) ## Also[​](#also "해당 헤딩으로 이동") **Advantages**: * [Overview](/documentation/kr/docs/get-started/overview.md) * CodeReview * Onboarding **Disadvantages:** * Mental complexity * High entry threshold * "Layers hell" * Typical problems of feature-based approaches --- # Partial Application WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/199) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > How to partially apply the methodology? Does it make sense? What if I ignore it? --- # Abstractions WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/186) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## The law of leaky abstractions[​](#the-law-of-leaky-abstractions "해당 헤딩으로 이동") ## Why are there so many abstractions[​](#why-are-there-so-many-abstractions "해당 헤딩으로 이동") > Abstractions help to cope with the complexity of the project. The question is - will these abstractions be specific only for this project, or will we try to derive general abstractions based on the specifics of the frontend > Architecture and applications in general are inherently complex, and the only question is how to better distribute and describe this complexity ## About scopes of responsibility[​](#about-scopes-of-responsibility "해당 헤딩으로 이동") > About optional abstractions ## See also[​](#see-also "해당 헤딩으로 이동") * [About the need for new layers](https://t.me/feature_sliced/2801) * [About the difficulty in understanding the methodology and layers](https://t.me/feature_sliced/2619) --- # About architecture ## 문제점들[​](#문제점들 "해당 헤딩으로 이동") 일반적으로 아키텍처 논의는 프로젝트가 커지고 **개발 생산성이 크게 저하되거나 진행이 지연될 때** 제기됩니다. ### Bus-factor & 온보딩[​](#bus-factor--온보딩 "해당 헤딩으로 이동") 프로젝트 구조와 아키텍처를 **일부 기존 팀원들만 이해하고 있습니다.** **예시:** * *신규 팀원이 독립적으로 업무를 하기까지 시간이 오래 걸립니다* * *아키텍처 설계 원칙이 없어, 개발자마다 다른 방식으로 문제를 해결합니다* * *거대한 모놀리스에서 데이터와 흐름을 추적하기 어렵습니다* ### 암묵적인 부작용과 예측 불가능한 영향[​](#암묵적인-부작용과-예측-불가능한-영향 "해당 헤딩으로 이동") 개발이나 리팩터링 과정에서 **작은 변경이 전체에 영향을 주는 부작용**이 자주 발생합니다.
*(모듈 간 의존성이 얽혀 있어, 한 부분을 수정하면 다른 부분이 함께 깨질 수 있습니다.)* **예시:** * *기능 간 불필요한 의존성이 생깁니다* * *한 페이지의 상태(store) 변경이 다른 페이지 동작에 예기치 않은 영향을 줍니다* * *비즈니스 로직이 여러 곳에 흩어져 있어 흐름을 추적하기 어렵습니다* ### 제어되지 않는 로직 재사용[​](#제어되지-않는-로직-재사용 "해당 헤딩으로 이동") 기존 로직을 재사용하거나 수정하기 어렵습니다. 보통 [두 가지 대표적인 문제 상황](https://github.com/feature-sliced/documentation/discussions/14)이 나타납니다: * 재사용할 수 있는 코드가 있음에도 **각 모듈을 매번 처음부터 새로 구현합니다.** * 반대로, 거의 한 곳에서만 쓰이는 코드까지 무분별하게 `shared` 폴더에 옮겨져, **사실상 쓸모없는 공용 모듈이 쌓입니다.** **예시:** * 같은 계산, 검증 로직이 여러 군데에서 **반복** 구현되어, 수정할 때 모든 위치를 일일이 고쳐야 합니다 * 동일한 버튼이나 팝업 컴포넌트가 스타일, 동작만 조금 다른 여러 버전으로 중복 존재합니다 * 유틸 함수들이 규칙 없이 계속 쌓여, 어떤 함수가 있는지 찾기 어렵고 중복도 많습니다 ## 요구사항[​](#요구사항 "해당 헤딩으로 이동") 이상적인 아키텍처를 위한 핵심 요구사항은 다음과 같습니다. note 여기서 "쉽다"라는 표현은 "대다수의 개발자들이 합리적인 시간 내에 이해하고 적용할 수 있다"는 의미입니다.
[모든 상황에 완벽히 맞는 아키텍처는 없기 때문에, 실용적인 합의가 중요합니다.](/documentation/kr/docs/about/mission.md#limitations) ### 명시성[​](#명시성 "해당 헤딩으로 이동") * 프로젝트 구조와 아키텍처를 **누구나 쉽게 이해하고 설명할 수 있어야** 합니다. * 아키텍처는 프로젝트의 **비즈니스 도메인과 가치**를 반영해야 합니다. * 계층 간 **의존 관계와 영향 범위**가 명확해야 합니다. * **중복된 로직을 쉽게 식별**할 수 있어야 합니다. * 핵심 로직이 프로젝트 전반에 **분산되지 않도록** 관리해야 합니다. * 불필요한 추상화나 복잡한 규칙은 최소화해야 합니다. ### 제어[​](#제어 "해당 헤딩으로 이동") * **새로운 기능을 빠르게 개발하고, 문제를 쉽게 해결**할 수 있어야 합니다. * 프로젝트의 전반적인 개발 흐름을 체계적으로 관리할 수 있어야 합니다. * 코드의 **확장성, 유지보수성, 제거 용이성**이 보장되어야 합니다. * 기능 단위로 **명확한 경계와 격리**가 필요합니다. * 컴포넌트는 **쉽게 교체, 삭제할 수 있어야** 합니다. * *[변경을 위한 과도한 최적화는 지양합니다](https://youtu.be/BWAeYuWFHhs?t=1631) — 미래의 요구사항을 정확히 예측하기 어렵기 때문입니다.* * *[삭제 용이성을 고려한 설계가 더 중요합니다](https://youtu.be/BWAeYuWFHhs?t=1666) — 현재의 요구사항을 기준으로 의사결정하는 것이 실용적입니다.* ### 적응성[​](#적응성 "해당 헤딩으로 이동") * **다양한 규모와 성격의 프로젝트**에 적용 가능해야 합니다. * *기존 시스템 및 인프라와 무리 없이 통합될 수 있어야 합니다.* * *프로젝트 전 주기(초기, 운영, 확장)에서 일관되게 적용 가능해야 합니다.* * 특정 기술 스택이나 플랫폼에 종속되지 않아야 합니다. * **병렬 개발과 팀 확장**이 원활해야 합니다. * **비즈니스 요구사항과 기술 환경 변화**에 유연하게 대응할 수 있어야 합니다. ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [(React Berlin Talk) Oleg Isonen - Feature Driven Architecture](https://youtu.be/BWAeYuWFHhs) * [(React SPB Meetup #1) Sergey Sova - Feature Slices](https://t.me/feature_slices) * [(Article) 프로젝트 모듈화에 대하여](https://alexmngn.medium.com/why-react-developers-should-modularize-their-applications-d26d381854c1) * [(Article) 관점 분리와 기능 기반 구조화에 대하여](https://ryanlanciaux.com/blog/2017/08/20/a-feature-based-approach-to-react-development/) --- # Knowledge types 소프트웨어 프로젝트를 개발할 때 다루게 되는 지식은 크게 세 가지로 나눌 수 있습니다: ### 기반 지식 (Fundamental Knowledge)[​](#기반-지식-fundamental-knowledge "해당 헤딩으로 이동") 프로그래밍의 기초가 되는 지식으로, 시간이 지나도 크게 변하지 않습니다: * 알고리즘과 자료구조 * 컴퓨터 과학의 핵심 개념 * 프로그래밍 언어의 기본 원리와 API ### 기술 스택 (Technical Stack)[​](#기술-스택-technical-stack "해당 헤딩으로 이동") 프로젝트 개발에 직접적으로 사용되는 도구들에 대한 지식입니다: * 프로그래밍 언어와 프레임워크 * 라이브러리와 개발 도구 * (선택적으로) 개발 환경과 배포 도구 ### 프로젝트 도메인 지식 (Project Knowledge)[​](#프로젝트-도메인-지식-project-knowledge "해당 헤딩으로 이동") 특정 프로젝트에만 해당하는 고유한 지식입니다: * 비즈니스 로직과 규칙 * 프로젝트만의 아키텍처 결정사항 * 팀 내 개발 규칙과 관례
이러한 지식은 다른 프로젝트에서는 크게 가치가 없지만, **신규 팀원이 프로젝트에 기여하기 위해서는 반드시 필요합니다.** note **Feature-Sliced Design**은 이러한 지식 유형을 고려하여 설계되었습니다: * 프로젝트 도메인 지식에 대한 의존도를 최소화 * 아키텍처가 더 많은 책임을 지도록 설계 * 기술 스택 지식을 체계적으로 구조화 * 새로운 팀원의 온보딩 과정을 단순화 ## 참고 자료[​](#see-also "해당 헤딩으로 이동") * [(영상 🇷🇺) Ilya Klimov - 지식 유형에 관하여](https://youtu.be/4xyb_tA-uw0?t=249) --- # Naming 개발자들은 각자의 경험과 관점에 따라 같은 대상을 다르게 부르는 경우가 많습니다. 이는 팀 내에서 혼동을 유발할 수 있습니다. 예를 들어: * UI 컴포넌트를 "ui", "components", "ui-kit", "views" 등으로 표현 * 공통 코드를 "core", "shared", "app" 등으로 지칭 * 비즈니스 로직을 "store", "model", "state" 등으로 명명 ## Feature-Sliced Design의 표준 네이밍[​](#naming-in-fsd "해당 헤딩으로 이동") FSD는 다음과 같이 명확한 네이밍 규칙을 제시합니다: ### Layers[​](#layers "해당 헤딩으로 이동") * `app` * `processes` * `pages` * `features` * `entities` * `shared` ### Segments[​](#segments "해당 헤딩으로 이동") * `ui` * `model` * `lib` * `api` * `config` 이러한 표준 용어를 사용하는 것은 매우 중요합니다. * 팀 내 의사소통이 명확해집니다 * 새로운 팀원의 적응이 쉬워집니다 * 커뮤니티에 도움을 요청할 때도 원활한 소통이 가능합니다 ## 네이밍 충돌 해결[​](#when-can-naming-interfere "해당 헤딩으로 이동") FSD 용어가 프로젝트의 비즈니스 용어와 중복될 수 있습니다. 예시: * `FSD#process` vs 애플리케이션의 시뮬레이션 프로세스 * `FSD#page` vs 로그 페이지 * `FSD#model` vs 자동차 모델 예를 들어, 개발자가 코드에서 "process"라는 단어를 보았을 때 **어떤 의미인지 해석하는 데 시간이 걸릴 수 있습니다.** 이러한 **충돌은 개발 효율을 저하시킬 수 있습니다.** 따라서 프로젝트 용어집(glossary)에 FSD 특유의 용어가 포함되어 있다면, 팀원 및 비기술적 이해관계자와의 커뮤니케이션에서 주의해야 합니다. ### 용어 사용 가이드[​](#용어-사용-가이드 "해당 헤딩으로 이동") 1. **기술적 커뮤니케이션** * FSD 용어 사용 시 FSD 접두어 사용을 권장합니다. * 예: 이 기능을 FSD features 계층으로 이동하는 것이 좋겠습니다. 2. **비기술적 커뮤니케이션** * FSD 관련 용어는 지양하고, 일반적인 비즈니스 용어를 사용합니다. * 예: 코드 구조 대신 기능이나 목적 중심으로 설명합니다. ## 참고 자료[​](#see-also "해당 헤딩으로 이동") * [(토론) Naming의 적응성](https://github.com/feature-sliced/documentation/discussions/16) * [(토론) Entities Naming 설문조사](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-464894) * [(토론) "processes" vs "flows" vs ...](https://github.com/feature-sliced/documentation/discussions/20) * [(토론) "model" vs "store" vs ...](https://github.com/feature-sliced/documentation/discussions/68) --- # Needs driven TL;DR *새로운 Feature의 목표가 불분명하거나 작업 정의가 모호한가요? **이 방법론의 핵심은 작업과 목표를 명확히 정의하는 데 있습니다.*** *프로젝트 구조는 항상 변합니다. 요구사항과 Feature는 계속 바뀌고, 코드는 점점 복잡해집니다. **좋은 아키텍처는 변화에 쉽게 적응할 수 있어야 합니다.*** ## 왜 이런 접근이 필요한가?[​](#왜-이런-접근이-필요한가 "해당 헤딩으로 이동") 각 Entity의 이름과 구조를 명확히 하려면, **그 코드가 해결하려는 목적을 정확히 이해**해야 합니다. > *@sergeysova: 개발할 때 Entity와 함수 이름에는 반드시 그 의도를 반영하려고 합니다.* *작업이 불명확하면 테스트 작성이 어렵고, 에러 처리가 비효율적이며, 결국 사용자 경험이 저하됩니다.* ## 우리가 말하는 작업이란?[​](#우리가-말하는-작업이란 "해당 헤딩으로 이동") 프론트엔드는 사용자의 문제를 해결하고 요구를 충족하는 인터페이스를 제공합니다.
사용자는 서비스에서 **자신의 필요를 해결하거나 목표를 달성**하려 합니다. *관리자와 분석가는 이를 명확히 정의하고, 개발자는 네트워크 지연, 에러, 사용자 실수 같은 환경을 고려하여 구현합니다.* **즉, 사용자의 목표가 곧 개발자의 작업입니다.** > *Feature-Sliced Design의 핵심 철학 중 하나는 — 프로젝트의 전체 작업을 더 작은 목표 단위로 나누는 것입니다.* ## 개발에 어떤 영향을 주는가?[​](#개발에-어떤-영향을-주는가 "해당 헤딩으로 이동") ### 작업 분해[​](#작업-분해 "해당 헤딩으로 이동") 개발자는 유지보수성을 위해 작업을 점진적으로 분해합니다. * *최상위 Entity로 나누기* * *더 작은 단위로 세분화하기* * *각 Entity에 명확한 이름 부여하기* > **모든 Entity는 사용자의 문제 해결에 직접 기여해야 합니다.** ### 작업의 본질 이해[​](#작업의-본질-이해 "해당 헤딩으로 이동") Entity의 이름을 정하려면 **그 목적과 역할을 충분히 이해**해야 합니다. * 구체적인 사용 목적 * 구현하는 사용자 작업의 범위 * 다른 작업과의 연관성 결론적으로, **이름을 고민하는 과정에서 불명확한 작업을 미리 발견할 수 있습니다.** > Entity의 이름을 정의하려면, 먼저 그 Entity가 해결할 작업을 명확히 이해해야 합니다. ## 어떻게 정의할 것인가?[​](#어떻게-정의할-것인가 "해당 헤딩으로 이동") **Feature가 해결할 작업을 정의하려면, 그 본질을 파악해야 합니다.**
이는 주로 프로젝트 관리자와 분석가의 역할입니다. *방법론은 단순히 **개발자에게 방향을 제시**합니다.* > *@sergeysova: 프론트엔드는 단순히 **무엇을 보여준다**가 아니라,* “왜 이것을 보여줘야 하는가?”를 통해 사용자의 실제 필요를 이해해야 합니다. 사용자의 필요를 이해하면, **제품이 목표 달성을 어떻게 돕는지 구체화**할 수 있습니다.
모든 새로운 작업은 비즈니스와 사용자의 문제를 함께 해결해야 하며, 분명한 목적을 가져야 합니다. ***개발자는 맡은 작업의 목표를 분명히 이해해야 합니다.** 비록 완벽한 프로세스가 없더라도, 관리, 기획 담당자와의 소통을 통해 목표를 파악하고 효과적으로 구현할 수 있어야 합니다.* ## 이점[​](#이점 "해당 헤딩으로 이동") 전체 Process를 통해 얻을 수 있는 이점을 살펴보겠습니다. ### 1. 사용자 작업 이해[​](#1-사용자-작업-이해 "해당 헤딩으로 이동") 사용자 문제와 비즈니스 요구를 이해하면, 기술적 제약 내에서도 더 나은 솔루션을 제안할 수 있습니다. > *이 모든 것은 개발자가 자신의 역할과 목표에 적극적으로 관심을 가질 때만 가능합니다.
그렇지 않다면, 어떤 방법론도 큰 의미를 가지지 못합니다.* ### 2. 구조화와 체계화[​](#2-구조화와-체계화 "해당 헤딩으로 이동") 작업을 이해하면 **사고 과정과 코드가 자연스럽게 정리되고 구조화**됩니다. ### 3. 기능과 그 구성 요소 이해[​](#3-기능과-그-구성-요소-이해 "해당 헤딩으로 이동") Feature는 사용자에게 **명확한 가치를 제공**해야 합니다. * 여러 기능이 섞이면 **경계 위반** * Feature는 분리와 확장이 가능해야 함 * 핵심 질문: **이 Feature가 사용자에게 어떤 가치를 주는가?** * 예시: * ❌ `지도-사무실` (모호함) * ⭕ `회의실-예약`, `직원-검색`, `근무지-변경` (명확함) > \_@sergeysova: Feature는 핵심 구현 코드만 포함해야 합니다. > > **관련 없는 코드는 제외하고, 해당 Feature의 핵심 로직만 담아야 합니다.** ### 4. 유지보수성[​](#4-유지보수성 "해당 헤딩으로 이동") 비즈니스 로직을 코드에 명확히 반영하면, **장기적인 유지보수성**이 높아집니다. *새로운 팀원이 합류해도 코드만 읽으면 **무엇을, 왜 구현했는지** 이해할 수 있습니다* > (도메인 주도 설계에서 말하는 [비즈니스 언어](https://thedomaindrivendesign.io/developing-the-ubiquitous-language) 개념과 유사) *** ## 현실적 고려사항[​](#현실적-고려사항 "해당 헤딩으로 이동") 비즈니스 프로세스와 설계가 명확하다면 구현은 쉽습니다.
그러나 현실에서는 충분한 설계 없이 Feature가 반복적으로 추가되는 경우가 많습니다. **결과적으로 지금 당장은 적절해 보이던 Feature가 한 달 뒤 확장에서 전체 구조를 흔들 수 있습니다.** > [토론](https://t.me/sergeysova/318): 개발자는 보통 2\~3단계 앞을 예측하려 하지만, 경험에 따라 그 한계가 달라집니다.
숙련된 개발자는 최대 10단계 앞까지 내다보고 Feature 분할과 통합을 더 잘합니다.
그러나 때로는 경험으로도 해결하기 어려운 복잡한 상황이 발생하며, 이때는 문제를 최소한으로 쪼개는 것이 중요합니다. ## 방법론의 역할[​](#방법론의-역할 "해당 헤딩으로 이동") 이 방법론의 목적은 **개발자가 사용자의 문제를 효과적으로 해결하도록 돕는 것**입니다. 즉, 방법론은 단순히 코드를 위한 규칙이 아니라,
**사용자의 필요를 이해하고 반영하기 위한 도구**입니다. ### 방법론 요구 사항[​](#방법론-요구-사항 "해당 헤딩으로 이동") **Feature-Sliced Design**이 충족해야 할 두 가지: 1. **Feature, Process, Entity를 구성하는 명확한 방법 제공** * 코드 분할 기준, 명명 규칙 정의 2. **[변화하는 요구사항에 유연한 아키텍처 제공](/documentation/kr/docs/about/understanding/architecture.md#adaptability)** ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [(포스트) 명확한 작업 정의 가이드 (+ 토론)](https://t.me/sergeysova/318) > ***이 문서는 해당 토론을 바탕**으로 작성되었습니다. 원문은 링크를 참고하세요.* * [(토론) Feature 분해 방법론](https://t.me/atomicdesign/18972) * [(아티클) "효과적인 애플리케이션 구조화"](https://alexmngn.medium.com/how-to-better-organize-your-react-applications-2fd3ea1920f1) --- # Signals of architecture WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/194) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > If there is a limitation on the part of the architecture, then there are obvious reasons for this, and consequences if they are ignored > The methodology and architecture gives signals, and how to deal with it depends on what risks you are ready to take on and what is most suitable for your team) ## See also[​](#see-also "해당 헤딩으로 이동") * [(Thread) About signals from architecture and dataflow](https://t.me/feature_sliced/2070) * [(Thread) About the fundamental nature of architecture](https://t.me/feature_sliced/2492) * [(Thread) About highlighting weak points](https://t.me/feature_sliced/3979) * [(Thread) How to understand that the data model is swollen](https://t.me/feature_sliced/4228) --- # Branding Guidelines FSD's visual identity is based on its core-concepts: `Layered`, `Sliced self-contained parts`, `Parts & Compose`, `Segmented`. But also we tend to design simple, pretty identity, which should convey the FSD philisophy and be easy to recognize. **Please, use FSD's identity "as-is", without changes but with our assets for your comfort.** This brand guide will help you to use FSD's identity correctly. Compatibility FSD had [another legacy identity](https://drive.google.com/drive/folders/11Y-3qZ_C9jOFoW2UbSp11YasOhw4yBdl?usp=sharing) before. Old design didn't represent core-concepts of methodology. Also it was created as pure draft, and should have been actualized. For a compatible and long-term use of the brand, we have been carefully rebranding for a year (2021-2022). **So that you can be sure when using identity of FSD 🍰** *But prefer namely actual identity, not old!* ## Title[​](#title "해당 헤딩으로 이동") * ✅ **Correct:** `Feature-Sliced Design`, `FSD` * ❌ **Incorrect:** `Feature-Sliced`, `Feature Sliced`, `FeatureSliced`, `feature-sliced`, `feature sliced`, `FS` ## Emojii[​](#emojii "해당 헤딩으로 이동") The cake 🍰 image represents FSD core concepts quite well, so it has been chosen as our signature emoji > Example: *"🍰 Architectural design methodology for Frontend projects"* ## Logo & Palette[​](#logo--palette "해당 헤딩으로 이동") FSD has few variations of logo for different context, but it recommended to prefer **primary** | | | | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- | | Theme | Logo (Ctrl/Cmd + Click for download) | Usage | | primary
(#29BEDC, #517AED) | [![logo-primary](/documentation/kr/img/brand/logo-primary.png)](/documentation/kr/img/brand/logo-primary.png) | Preferred in most cases | | flat
(#3193FF) | [![logo-flat](/documentation/kr/img/brand/logo-flat.png)](/documentation/kr/img/brand/logo-flat.png) | For one-color context | | monochrome
(#FFF) | [![logo-monocrhome](/documentation/kr/img/brand/logo-monochrome.png)](/documentation/kr/img/brand/logo-monochrome.png) | For grayscale context | | square
(#3193FF) | [![logo-square](/documentation/kr/img/brand/logo-square.png)](/documentation/kr/img/brand/logo-square.png) | For square boundaries | ## Banners & Schemes[​](#banners--schemes "해당 헤딩으로 이동") [![banner-primary](/documentation/kr/img/brand/banner-primary.jpg)](/documentation/kr/img/brand/banner-primary.jpg) [![banner-monochrome](/documentation/kr/img/brand/banner-monochrome.jpg)](/documentation/kr/img/brand/banner-monochrome.jpg) ## Social Preview[​](#social-preview "해당 헤딩으로 이동") Work in progress... ## Presentation template[​](#presentation-template "해당 헤딩으로 이동") Work in progress... ## See also[​](#see-also "해당 헤딩으로 이동") * [Discussion (github)](https://github.com/feature-sliced/documentation/discussions/399) * [History of development with references (figma)](https://www.figma.com/file/RPphccpoeasVB0lMpZwPVR/FSD-Brand?node-id=0%3A1) --- # Decomposition cheatsheet Use this as a quick reference when you're deciding how to decompose your UI. PDF versions are also available below, so you can print it out and keep one under your pillow. ## Choosing a layer[​](#choosing-a-layer "해당 헤딩으로 이동") [Download PDF](/documentation/kr/assets/files/choosing-a-layer-en-12fdf3265c8fc4f6b58687352b81fce7.pdf) ![Definitions of all layers and self-check questions](/documentation/kr/assets/images/choosing-a-layer-en-5b67f20bb921ba17d78a56c0dc7654a9.jpg) ## Examples[​](#examples "해당 헤딩으로 이동") ### Tweet[​](#tweet "해당 헤딩으로 이동") ![decomposed-tweet-bordered-bgLight](/documentation/kr/assets/images/decompose-twitter-7b9a50f879d763c49305b3bf0751ee35.png) ### GitHub[​](#github "해당 헤딩으로 이동") ![decomposed-github-bordered](/documentation/kr/assets/images/decompose-github-a0eeb839a4b5ef5c480a73726a4451b0.jpg) ## See also[​](#see-also "해당 헤딩으로 이동") * [(Thread) General logic for features and entities](https://t.me/feature_sliced/4262) * [(Thread) Decomposition of swollen logic](https://t.me/feature_sliced/4210) * [(Thread) About understanding the areas of responsibility during decomposition](https://t.me/feature_sliced/4088) * [(Thread) Decomposition of the Product List widget](https://t.me/feature_sliced/3828) * [(Article) Different approaches to the decomposition of logic](https://www.pluralsight.com/guides/how-to-organize-your-react-+-redux-codebase) * [(Thread) About the difference between features and entities](https://t.me/feature_sliced/3776) * [(Thread) About the difference between things and entities (2)](https://t.me/feature_sliced/3248) * [(Thread) About the application of criteria for decomposition](https://t.me/feature_sliced/3833) --- # FAQ info 질문은 언제든 [Telegram](https://t.me/feature_sliced), [Discord](https://discord.gg/S8MzWTUsmp), [GitHub Discussions](https://github.com/feature-sliced/documentation/discussions)에서 남겨 주세요. ### Toolkit이나 Linter가 있나요?[​](#toolkit이나-linter가-있나요 "해당 헤딩으로 이동") 프로젝트 아키텍처를 FSD 규칙에 맞게 검사 [Steiger](https://github.com/feature-sliced/steiger) Linter가 있으며, CLI · IDE 확장을 통해 사용할 수 있는 [폴더 생성기](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools)도 함께 제공됩니다. ### Page Layout / Template은 어디에 보관해야 하나요?[​](#page-layouttemplate은-어디에-보관해야-하나요 "해당 헤딩으로 이동") * **단순 마크업**이라면 `shared/ui`에 두는 것이 일반적입니다. * 코드가 몇 줄뿐이라면 굳이 추상화하지 말고 각 페이지에 직접 작성해도 무방합니다. * 복잡한 Layout이 필요하다면 별도 **Widget** 또는 **Page**로 분리하고 App Router(또는 Nested Routing)에서 조합하세요. ### Feature와 Entity의 차이는 무엇인가요?[​](#feature와-entity의-차이는-무엇인가요 "해당 헤딩으로 이동") | 구분 | 정의 | 예시 | | ----------- | -------------------------------------------- | --------------------- | | **Entity** | 애플리케이션이 다루는 **비즈니스 개체** | `user`, `product` | | **Feature** | 사용자가 Entity로 수행하는 **실제 상호작용** | 로그인, 장바구니 담기 | 더 자세한 내용과 예시는 [Slices](/documentation/kr/docs/reference/layers.md#entities)에서 확인할 수 있습니다. ### Pages, Features, Entities를 서로 포함할 수 있나요?[​](#pages-features-entities를-서로-포함할-수-있나요 "해당 헤딩으로 이동") 가능합니다. 다만 **상위 Layer**에서만 조합해야 합니다.
예: Widget 내부에서 여러 Feature를 props / children 형태로 결합할 수 있지만, 한 Feature가 다른 Feature를 직접 import 하는 것은 [**Layer Import 규칙**](/documentation/kr/docs/reference/layers.md#import-rule-on-layers)에 의해 금지됩니다. ### Atomic Design을 함께 사용할 수 있나요?[​](#atomic-design을-함께-사용할-수-있나요 "해당 헤딩으로 이동") 네. FSD는 Atomic Design 사용을 **요구하지도, 금지하지도** 않습니다.
필요하다면 `ui` Segment 내부에서 Atomic 분류를 적용할 수 있습니다. [예시](https://t.me/feature_sliced/1653) ### FSD 관련 참고 자료가 더 있나요?[​](#fsd-관련-참고-자료가-더-있나요 "해당 헤딩으로 이동") 커뮤니티가 정리한 자료 모음은 [feature‑sliced/awesome](https://github.com/feature-sliced/awesome)에서 확인할 수 있습니다. ### Feature‑Sliced Design이 필요한 이유는 무엇인가요?[​](#featuresliced-design이-필요한-이유는-무엇인가요 "해당 헤딩으로 이동") 표준화된 아키텍처는 프로젝트를 빠르게 파악하게 해 줍니다.
온보딩 속도를 높이고 “폴더 구조 논쟁”을 줄여 주는 것이 FSD의 핵심 가치입니다. 자세한 배경은 [Motivation](/documentation/kr/docs/about/motivation.md) 페이지를 참고하세요. ### 주니어 개발자도 아키텍처 방법론이 필요할까요?[​](#주니어-개발자도-아키텍처-방법론이-필요할까요 "해당 헤딩으로 이동") 필요합니다.
*혼자 개발할 때는 문제가 없어 보여도, 개발 공백이 생기거나 새로운 팀원이 합류하면 구조의 중요성이 드러납니다* ### 인증(Auth) Context는 어떻게 다루나요?[​](#인증auth-context는-어떻게-다루나요 "해당 헤딩으로 이동") [예제 가이드](/documentation/kr/docs/guides/examples/auth.md)에서 자세히 설명했습니다. --- # 개요 **Feature-Sliced Design (FSD)** 는 프론트엔드 애플리케이션 구조를 위한 아키텍처 방법론입니다. 코드를 어떻게 분리하고 구성할지를 명확히 정의하여, 변화하는 비즈니스 요구 속에서도 프로젝트를 이해하기 쉽고 안정적으로 유지할 수 있도록 돕습니다. FSD는 단순한 규칙 집합이 아니라 실무를 위한 도구 체계도 함께 제공합니다. * 프로젝트 아키텍처를 검사하는 [Linter](https://github.com/feature-sliced/steiger) * CLI 및 IDE 기반의 [폴더 생성기](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools) * 다양한 구조를 참고할 수 있는 [예제 모음](/documentation/kr/examples.md) ## 내 프로젝트에 적합할까요?[​](#is-it-right-for-me "해당 헤딩으로 이동") FSD는 다음 조건에 해당하면 도입할 수 있습니다: * **프론트엔드**(웹, 모바일, 데스크톱 등)를 개발하고 있고 * **라이브러리**가 아닌 **애플리케이션**을 개발하고 있다면 언어, UI 프레임워크, 상태 관리 도구에 대한 제약은 없습니다.
Monorepo 환경에서도 사용 가능하며, 프로젝트 구조를 나눠 **점진적으로 적용**할 수 있습니다. > 현재 구조가 문제가 없다면 반드시 바꿀 필요는 없습니다.
다만 아래와 같은 상황이라면 도입을 고려해보세요: > > * 프로젝트가 커지면서 구조가 얽히고, 기능 개발 속도가 느려졌을 때 > * 새로운 팀원이 구조를 이해하기 어려운 상황일 때 구조 전환을 결정했다면 [Migration 가이드](/documentation/kr/docs/guides/migration/from-custom.md)를 참고하세요. ## 구조 예시[​](#basic-example "해당 헤딩으로 이동") 간단한 FSD 구조는 다음과 같습니다: * `📁 app` * `📁 pages` * `📁 shared` 이 상위 폴더들은 각각 **Layer**에 해당합니다. * `📂 app` * `📁 routes` * `📁 analytics` * `📂 pages` * `📁 home` * `📂 article-reader` * `📁 ui` * `📁 api` * `📁 settings` * `📂 shared` * `📁 ui` * `📁 api` * 📂 pages 내부 폴더들은 **Slice**입니다. 일반적으로 도메인(이 예시에서는 페이지) 기준으로 구분됩니다. * `📂 app`, `📂 shared`, `📂 pages/article-reader` 내의 하위 폴더들은 **Segment**입니다. Segment는 해당 코드의 **기능 목적**(UI, API 통신 등)에 따라 분류합니다. ## 개념[​](#concepts "해당 헤딩으로 이동") FSD는 다음과 같은 3단계 계층 구조를 따릅니다: ![아래에 설명된 FSD 개념의 계층 구조](/documentation/kr/assets/images/visual_schema-e826067f573946613dcdc76e3f585082.jpg) 위 다이어그램은 FSD의 계층 구조를 시각적으로 보여줍니다. 세 개의 수직 블록 그룹은 각각 **Layer**, **Slice**, **Segment**를 나타냅니다. 왼쪽의 Layer 블록에는 `app`, `processes`, `pages`, `widgets`, `features`, `entities`, `shared`가 포함됩니다. 예를 들어, `entities` Layer 안에는 여러 개의 Slice가 존재하며, 예시로는 `user`, `post`, `comment` 등이 있습니다. 각 Slice는 다시 기능 목적에 따라 나뉘는 Segment로 구성됩니다. 예시로 `post` Slice에는 `ui`, `model`, `api` Segment가 포함됩니다. ### Layer[​](#layers "해당 헤딩으로 이동") Layer는 모든 FSD 프로젝트의 표준 최상위 폴더입니다. 1. **App\*** - Routing, Entrypoint, Global Styles, Provider 등 앱을 실행하는 모든 요소 2. **Processes**(더 이상 사용되지 않음) - 페이지 간 복합 시나리오 3. **Pages** - 전체 page 또는 중첩 Routing의 핵심 영역 4. **Widgets** - 독립적으로 동작하는 대형 UI·기능 블록 5. **Features** - 제품 전반에서 재사용되는 비즈니스 기능 6. **Entities** - user, product 같은 핵심 도메인 Entity 7. **Shared**\* - 프로젝트 전반에서 재사용되는 일반 유틸리티 *\* - **App·Shared** Layer는 Slice 없이 곧바로 Segment로 구성됩니다.* 상위 Layer의 모듈은 자신보다 하위 Layer만 참조할 수 있습니다. ### Slice[​](#slices "해당 헤딩으로 이동") Slice는 Layer 내부를 비즈니스 도메인별로 나눕니다. 이름·개수에 제한이 없으며, 같은 Layer 내 다른 Slice를 참조할 수 없습니다. 이 규칙이 높은 응집도와 낮은 결합도를 보장합니다. ### Segment[​](#segments "해당 헤딩으로 이동") Slice와 App·Shared Layer는 Segment로 세분화되어, 기술적 목적에 따라 코드를 그룹화합니다. 일반적으로 다음과 같은 Segment를 사용합니다 * `ui` - UI components, date formatter, styles 등 UI 표현과 직접 관련된 코드 * `api` - request functions, data types, mappers 등 백엔드 통신 및 데이터 로직 * `model` - schema, interfaces, store, business logic 등 애플리케이션 도메인 모델 * `lib` - 해당 Slice에서 여러 모듈이 함께 사용하는 공통 library code * `config` - configuration files, feature flags 등 환경·기능 설정 대부분의 Layer에서는 위 다섯 Segment로 충분합니다. 필요하다면 App 또는 Shared Layer에서만 추가 Segment를 정의하세요. (필수 규칙은 아닙니다.) ## 장점[​](#advantages "해당 헤딩으로 이동") FSD 구조를 사용하면 다음과 같은 장점을 얻을 수 있습니다: * **일관성**
구조가 표준화되어 팀 간 협업과 신규 멤버 온보딩이 쉬워집니다. * **격리성**
Layer와 Slice 간 의존성을 제한하여, 특정 모듈만 안전하게 수정할 수 있습니다. * **재사용 범위 제어**
재사용 가능한 코드를 필요한 범위에서만 활용할 수 있어, **DRY** 원칙과 실용성을 균형 있게 유지합니다. * **도메인 중심 구조**
비즈니스 용어 기반의 구조로 되어 있어, 전체 코드를 몰라도 특정 기능을 독립적으로 구현할 수 있습니다. ## 점진적 도입[​](#incremental-adoption "해당 헤딩으로 이동") 기존 프로젝트에 FSD를 도입하는 방법: 1. `app`, `shared` Layer를 먼저 정리하며 기반을 다집니다. 2. 기존 UI를 `widgets`, `pages` Layer로 대략 분배합니다.
이 과정에서 FSD 규칙을 위반해도 괜찮습니다. 3. Import 위반을 하나씩 해결하면서, `entities`, `features`를 추출합니다. > 리팩토링 중에는 새로운 대규모 Entity 추가를 피하는 것이 좋습니다. ## 다음 단계[​](#next-steps "해당 헤딩으로 이동") * [Tutorial](/documentation/kr/docs/get-started/tutorial.md)을 통해 FSD 방식의 사고를 익혀보세요. * 다양한 [예제](/documentation/kr/examples.md)를 통해 실제 프로젝트 구조를 살펴보세요. * 궁금한 점은 [Telegram 커뮤니티](https://t.me/feature_sliced)에서 질문해보세요. --- # 튜토리얼 ## Part 1. 설계[​](#part-1-설계 "해당 헤딩으로 이동") 이 튜토리얼에서는 Real World App이라고도 알려진 Conduit를 살펴보겠습니다. Conduit는 기본적인 [Medium](https://medium.com/) 클론입니다 - 글을 읽고 쓸 수 있으며 다른 사람의 글에 댓글을 달 수 있습니다. ![Conduit home page](/documentation/kr/assets/images/realworld-feed-anonymous-8cbba45f488931979f6c8da8968ad685.jpg) 이 애플리케이션은 매우 작은 애플리케이션이므로 과도한 분해를 피하고 간단하게 유지할 것입니다. 전체 애플리케이션이 세 개의 레이어인 **App**, **Pages**, 그리고 **Shared**에 맞춰 들어갈 것입니다. 그렇지 않다면 우리는 계속해서 추가적인 레이어를 도입할 것입니다. 준비되셨나요? ### 먼저 페이지를 나열해 봅시다.[​](#먼저-페이지를-나열해-봅시다 "해당 헤딩으로 이동") 위의 스크린샷을 보면 최소한 다음과 같은 페이지들이 있다고 가정할 수 있습니다: * 홈 (글 피드) * 로그인 및 회원가입 * 글 읽기 * 글 편집기 * 사용자 프로필 보기 * 사용자 프로필 편집 (사용자 설정) 이 페이지들 각각은 Pages *레이어*의 독립된 *슬라이스*가 될 것입니다. 개요에서 언급했듯이 슬라이스는 단순히 레이어 내의 폴더이고, 레이어는 `pages`와 같은 미리 정의된 이름을 가진 폴더일 뿐입니다. 따라서 우리의 Pages 폴더는 다음과 같이 보일 것입니다. ``` 📂 pages/ 📁 feed/ 📁 sign-in/ 📁 article-read/ 📁 article-edit/ 📁 profile/ 📁 settings/ ``` Feature-Sliced Design이 규제되지 않은 코드 구조와 다른 주요 차이점은 페이지들이 서로를 참조할 수 없다는 것입니다. 즉, 한 페이지가 다른 페이지의 코드를 가져올 수 없습니다. 이는 **레이어의 import 규칙** 때문입니다. *슬라이스의 모듈은 엄격히 아래에 있는 레이어에 위치한 다른 슬라이스만 가져올 수 있습니다.* 이 경우 페이지는 슬라이스이므로, 이 페이지 내의 모듈(파일)은 같은 레이어인 Pages가 아닌 아래 레이어의 코드만 참조할 수 있습니다. ### 피드 자세히 보기[​](#피드-자세히-보기 "해당 헤딩으로 이동") ![Anonymous user’s perspective](/documentation/kr/assets/images/realworld-feed-anonymous-8cbba45f488931979f6c8da8968ad685.jpg) *익명 사용자의 관점* ![Authenticated user’s perspective](/documentation/kr/assets/images/realworld-feed-authenticated-15427d9ff7baae009b47b501bee6c059.jpg) *인증된 사용자의 관점* 피드 페이지에는 세 가지 동적 영역이 있습니다. 1. 로그인 여부를 나타내는 로그인 링크 2. 피드에서 필터링을 트리거하는 태그 목록 3. 좋아요 버튼이 있는 하나/두 개의 글 피드 로그인 링크는 모든 페이지에 공통적인 헤더의 일부이므로 나중에 따로 다루겠습니다. #### 태그 목록[​](#태그-목록 "해당 헤딩으로 이동") 태그 목록을 만들기 위해서는 사용 가능한 태그를 가져오고, 각 태그를 칩으로 렌더링하고, 선택된 태그를 클라이언트 측 저장소에 저장해야 합니다. 이러한 작업들은 각각 "API 상호작용", "사용자 인터페이스", "저장소" 카테고리에 속합니다. Feature-Sliced Design에서는 코드를 *세그먼트*를 사용하여 목적별로 분리합니다. 세그먼트는 슬라이스 내의 폴더이며, 목적을 설명하는 임의의 이름을 가질 수 있지만, 일부 목적은 너무 일반적이어서 특정 세그먼트 이름에 대한 규칙이 있습니다. * 📂 `api/` 백엔드 상호작용 * 📂 `ui/` 렌더링과 외관을 다루는 코드 * 📂 `model/` 저장소와 비즈니스 로직 * 📂 `config/` 기능 플래그, 환경 변수 및 기타 구성 형식 태그를 가져오는 코드는 `api`에, 태그 컴포넌트는 `ui`에, 저장소 상호작용은 `model`에 배치할 것입니다. #### 글[​](#글 "해당 헤딩으로 이동") 같은 그룹화 원칙을 사용하여 글 피드를 같은 세 개의 세그먼트로 분해할 수 있습니다. * 📂 `api/`: 좋아요 수가 포함된 페이지네이션된 글 가져오기 * 📂 `ui/`: * 태그가 선택된 경우 추가 탭을 렌더링할 수 있는 탭 목록 * 개별 글 * 기능적 페이지네이션 * 📂 `model/`: 현재 로드된 글과 현재 페이지의 클라이언트 측 저장소 (필요한 경우) ### 일반적인 코드 재사용[​](#일반적인-코드-재사용 "해당 헤딩으로 이동") 대부분의 페이지는 의도가 매우 다르지만, 앱 전체에 걸쳐 일부 요소는 동일하게 유지됩니다. 예를 들어, 디자인 언어를 준수하는 UI 키트나 모든 것이 동일한 인증 방식으로 REST API를 통해 수행되는 백엔드의 규칙 등이 있습니다. 슬라이스는 격리되도록 설계되었기 때문에, 코드 재사용은 더 낮은 계층인 **Shared**에 의해 촉진됩니다. Shared는 슬라이스가 아닌 세그먼트를 포함한다는 점에서 다른 계층과 다릅니다. 이런 면에서 Shared 계층은 계층과 슬라이스의 하이브리드로 생각할 수 있습니다. 일반적으로 Shared의 코드는 미리 계획되지 않고 개발 중에 추출됩니다. 실제로 어떤 코드 부분이 공유되는지는 개발 중에만 명확해지기 때문입니다. 그러나 어떤 종류의 코드가 자연스럽게 Shared에 속하는지 머릿속에 메모해 두는 것은 여전히 도움이 됩니다. * 📂 `ui/` — UI 키트, 비즈니스 로직이 없는 순수한 UI. 예: 버튼, 모달 대화 상자, 폼 입력. * 📂 `api/` — 요청 생성 기본 요소(예: 웹의 `fetch()`)에 대한 편의 래퍼 및 선택적으로 백엔드 사양에 따라 특정 요청을 트리거하는 함수. * 📂 `config/` — 환경 변수 파싱 * 📂 `i18n/` — 언어 지원에 대한 구성 * 📂 `router/` — 라우팅 기본 요소 및 라우트 상수 이는 Shared의 세그먼트 이름의 몇 가지 예시일 뿐이며, 이 중 일부를 생략하거나 자신만의 세그먼트를 만들 수 있습니다. 새로운 세그먼트를 만들 때 기억해야 할 유일한 중요한 점은 세그먼트 이름이 **본질(무엇인지)이 아닌 목적(왜)을 설명해야 한다**는 것입니다. "components", "hooks", "modals"과 같은 이름은 이 파일들이 무엇인지는 설명하지만 내부 코드를 탐색하는 데 도움이 되지 않기 때문에 사용해서는 안 됩니다. 이는 팀원들이 이러한 폴더의 모든 파일을 파헤쳐야 하며, 관련 없는 코드를 가까이 유지하게 되어 리팩토링의 영향을 받는 코드 영역이 넓어지고 결과적으로 코드 리뷰와 테스트를 더 어렵게 만듭니다. ### 엄격한 공개 API 정의[​](#엄격한-공개-api-정의 "해당 헤딩으로 이동") Feature-Sliced Design의 맥락에서 *공개 API*라는 용어는 슬라이스나 세그먼트가 프로젝트의 다른 모듈에서 가져올 수 있는 것을 선언하는 것을 의미합니다. 예를 들어, JavaScript에서는 슬라이스의 다른 파일에서 객체를 다시 내보내는 `index.js` 파일일 수 있습니다. 이를 통해 외부 세계와의 계약(즉, 공개 API)이 동일하게 유지되는 한 슬라이스 내부의 코드를 자유롭게 리팩토링할 수 있습니다. 슬라이스가 없는 Shared 계층의 경우, Shared의 모든 것에 대한 단일 인덱스를 정의하는 것과 반대로 각 세그먼트에 대해 별도의 공개 API를 정의하는 것이 일반적으로 더 편리합니다. 이렇게 하면 Shared에서의 가져오기가 자연스럽게 의도별로 구성됩니다. 슬라이스가 있는 다른 계층의 경우 반대가 사실입니다 — 일반적으로 슬라이스당 하나의 인덱스를 정의하고 슬라이스가 외부 세계에 알려지지 않은 자체 세그먼트 세트를 결정하도록 하는 것이 더 실용적입니다. 다른 계층은 일반적으로 내보내기가 훨씬 적기 때문입니다. 우리의 슬라이스/세그먼트는 서로에게 다음과 같이 나타날 것입니다. ``` 📂 pages/ 📂 feed/ 📄 index 📂 sign-in/ 📄 index 📂 article-read/ 📄 index 📁 … 📂 shared/ 📂 ui/ 📄 index 📂 api/ 📄 index 📁 … ``` `pages/feed`나 `shared/ui`와 같은 폴더 내부의 내용은 해당 폴더에만 알려져 있으며, 다른 파일은 이러한 폴더의 내부 구조에 의존해서는 안 됩니다. ### UI의 큰 재사용 블록[​](#ui의-큰-재사용-블록 "해당 헤딩으로 이동") 앞서 모든 페이지에 나타나는 헤더를 다시 살펴보기로 했습니다. 모든 페이지에서 처음부터 다시 만드는 것은 비실용적이므로 재사용하고 싶을 것입니다. 우리는 이미 코드 재사용을 용이하게 하는 Shared를 가지고 있지만, Shared에 큰 UI 블록을 넣는 데는 주의할 점이 있습니다 — Shared 계층은 위의 계층에 대해 알지 못해야 합니다. Shared와 Pages 사이에는 Entities, Features, Widgets의 세 가지 다른 계층이 있습니다. 일부 프로젝트는 이러한 계층에 큰 재사용 가능한 블록에 필요한 것이 있을 수 있으며, 이는 해당 재사용 가능한 블록을 Shared에 넣을 수 없다는 것을 의미합니다. 그렇지 않으면 상위 계층에서 가져오게 되어 금지됩니다. 이것이 Widgets 계층이 필요한 이유입니다. Widgets는 Shared, Entities, Features 위에 위치하므로 이들 모두를 사용할 수 있습니다. 우리의 경우, 헤더는 매우 간단합니다 — 정적 로고와 최상위 탐색입니다. 탐색은 사용자가 현재 로그인했는지 여부를 확인하기 위해 API에 요청을 해야 하지만, 이는 `api` 세그먼트에서 간단한 가져오기로 처리할 수 있습니다. 따라서 우리는 헤더를 Shared에 유지할 것입니다. ### 폼이 있는 페이지 자세히 보기[​](#폼이-있는-페이지-자세히-보기 "해당 헤딩으로 이동") 읽기가 아닌 편집을 위한 페이지도 살펴보겠습니다. ![Conduit post editor](/documentation/kr/assets/images/realworld-editor-authenticated-10de4d01479270886859e08592045b1e.jpg) 간단해 보이지만, 폼 유효성 검사, 오류 상태, 데이터 지속성 등 아직 탐구하지 않은 애플리케이션 개발의 여러 측면을 포함하고 있습니다. 이 페이지를 만들려면 Shared에서 일부 입력과 버튼을 가져와 이 페이지의 `ui` 세그먼트에서 폼을 구성할 것입니다. 그런 다음 `api` 세그먼트에서 백엔드에 글을 생성하는 변경 요청을 정의할 것입니다. 요청을 보내기 전에 유효성을 검사하려면 유효성 검사 스키마가 필요하며, 이를 위한 좋은 위치는 데이터 모델이기 때문에 `model` 세그먼트입니다. 여기서 오류 메시지를 생성하고 `ui` 세그먼트의 다른 컴포넌트를 사용하여 표시할 것입니다. 사용자 경험을 개선하기 위해 우발적인 데이터 손실을 방지하기 위해 입력을 지속시킬 수도 있습니다. 이것도 `model` 세그먼트의 작업입니다. ### 요약[​](#요약 "해당 헤딩으로 이동") 우리는 여러 페이지를 검토하고 애플리케이션의 예비 구조를 개략적으로 설명했습니다. 1. Shared layer 1. `ui`는 재사용 가능한 UI 키트를 포함할 것입니다. 2. `api`는 백엔드와의 기본적인 상호작용을 포함할 것입니다. 3. 나머지는 필요에 따라 정리될 것입니다. 2. Pages layer — 각 페이지는 별도의 슬라이스입니다. 1. `ui`는 페이지 자체와 모든 부분을 포함할 것입니다. 2. `api`는 `shared/api`를 사용하여 더 특화된 데이터 가져오기를 포함할 것입니다. 3. `model`은 표시할 데이터의 클라이언트 측 저장소를 포함할 수 있습니다. 이제 코드 작성을 시작해 봅시다! ## Part 2. 코드 작성[​](#part-2-코드-작성 "해당 헤딩으로 이동") 이제 설계를 완료했으니 실제로 코드를 작성해 봅시다. React와 [Remix](https://remix.run)를 사용할 것입니다. 이 프로젝트를 위한 템플릿이 준비되어 있습니다. GitHub에서 클론하여 시작하세요. . `npm install`로 의존성을 설치하고 `npm run dev`로 개발 서버를 시작하세요. 을 열면 빈 앱이 보일 것입니다. ### 페이지 레이아웃[​](#페이지-레이아웃 "해당 헤딩으로 이동") 모든 페이지에 대한 빈 컴포넌트를 만드는 것부터 시작하겠습니다. 프로젝트에서 다음 명령을 실행하세요. ``` npx fsd pages feed sign-in article-read article-edit profile settings --segments ui ``` 이렇게 하면 `pages/feed/ui/`와 같은 폴더와 모든 페이지에 대한 인덱스 파일인 `pages/feed/index.ts`가 생성됩니다. ### 피드 페이지 연결[​](#피드-페이지-연결 "해당 헤딩으로 이동") 애플리케이션의 루트 경로를 피드 페이지에 연결해 봅시다. `pages/feed/ui`에 `FeedPage.tsx` 컴포넌트를 만들고 다음 내용을 넣으세요: pages/feed/ui/FeedPage.tsx ``` export function FeedPage() { return (

conduit

A place to share your knowledge.

); } ``` 그런 다음 피드 페이지의 공개 API인 `pages/feed/index.ts` 파일에서 이 컴포넌트를 다시 내보내세요. pages/feed/index.ts ``` export { FeedPage } from "./ui/FeedPage"; ``` 이제 루트 경로에 연결합니다. Remix에서 라우팅은 파일 기반이며, 라우트 파일은 `app/routes` 폴더에 있어 Feature-Sliced Design과 잘 맞습니다. `app/routes/_index.tsx`에서 `FeedPage` 컴포넌트를 사용하세요. app/routes/\_index.tsx ``` import type { MetaFunction } from "@remix-run/node"; import { FeedPage } from "pages/feed"; export const meta: MetaFunction = () => { return [{ title: "Conduit" }]; }; export default FeedPage; ``` 그런 다음 개발 서버를 실행하고 애플리케이션을 열면 Conduit 배너가 보일 것입니다! ![The banner of Conduit](/documentation/kr/assets/images/conduit-banner-a20e38edcd109ee21a8b1426d93a66b3.jpg) ### API 클라이언트[​](#api-클라이언트 "해당 헤딩으로 이동") RealWorld 백엔드와 통신하기 위해 Shared에 편리한 API 클라이언트를 만들어 봅시다. 클라이언트를 위한 `api`와 백엔드 기본 URL과 같은 변수를 위한 `config`, 두 개의 세그먼트를 만드세요. ``` npx fsd shared --segments api config ``` 그런 다음 `shared/config/backend.ts`를 만드세요. shared/config/backend.ts ``` export { mockBackendUrl as backendBaseUrl } from "mocks/handlers"; ``` shared/config/index.ts ``` export { backendBaseUrl } from "./backend"; ``` RealWorld 프로젝트는 편리하게 [OpenAPI 사양](https://github.com/gothinkster/realworld/blob/main/api/openapi.yml)을 제공하므로, 클라이언트를 위한 자동 생성 타입을 활용할 수 있습니다. 추가 타입 생성기가 포함된 [`openapi-fetch` 패키지](https://openapi-ts.pages.dev/openapi-fetch/)를 사용할 것입니다. 다음 명령을 실행하여 최신 API 타입을 생성하세요. ``` npm run generate-api-types ``` 이렇게 하면 `shared/api/v1.d.ts` 파일이 생성됩니다. 이 파일을 사용하여 `shared/api/client.ts`에 타입이 지정된 API 클라이언트를 만들 것입니다. shared/api/client.ts ``` import createClient from "openapi-fetch"; import { backendBaseUrl } from "shared/config"; import type { paths } from "./v1"; export const { GET, POST, PUT, DELETE } = createClient({ baseUrl: backendBaseUrl }); ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; ``` ### 피드의 실제 데이터[​](#피드의-실제-데이터 "해당 헤딩으로 이동") 이제 백엔드에서 가져온 글을 피드에 추가할 수 있습니다. 글 미리보기 컴포넌트를 구현하는 것부터 시작하겠습니다. 다음 내용으로 `pages/feed/ui/ArticlePreview.tsx`를 만드세요. pages/feed/ui/ArticlePreview\.tsx ``` export function ArticlePreview({ article }) { /* TODO */ } ``` TypeScript를 사용하고 있으므로 글 객체에 타입을 지정하면 좋을 것 같습니다. 생성된 `v1.d.ts`를 살펴보면 글 객체가 `components["schemas"]["Article"]`을 통해 사용 가능한 것을 볼 수 있습니다. 그럼 Shared에 데이터 모델이 있는 파일을 만들고 모델을 내보내겠습니다. shared/api/models.ts ``` import type { components } from "./v1"; export type Article = components["schemas"]["Article"]; ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; ``` 이제 글 미리보기 컴포넌트로 돌아가 데이터로 마크업을 채울 수 있습니다. 컴포넌트를 다음 내용으로 업데이트하세요. pages/feed/ui/ArticlePreview\.tsx ``` import { Link } from "@remix-run/react"; import type { Article } from "shared/api"; interface ArticlePreviewProps { article: Article; } export function ArticlePreview({ article }: ArticlePreviewProps) { return (
{article.author.username} {new Date(article.createdAt).toLocaleDateString(undefined, { dateStyle: "long", })}

{article.title}

{article.description}

Read more...
    {article.tagList.map((tag) => (
  • {tag}
  • ))}
); } ``` 좋아요 버튼은 지금은 아무 작업도 하지 않습니다. 글 읽기 페이지를 만들고 좋아요 기능을 구현할 때 수정하겠습니다. 이제 글을 가져와서 이러한 카드를 여러 개 렌더링할 수 있습니다. Remix에서 데이터 가져오기는 *로더* — 페이지가 필요로 하는 것을 정확히 가져오는 서버 측 함수 — 를 통해 수행됩니다. 로더는 페이지를 대신하여 API와 상호 작용하므로 페이지의 `api` 세그먼트에 넣을 것입니다: pages/feed/api/loader.ts ``` import { json } from "@remix-run/node"; import { GET } from "shared/api"; export const loader = async () => { const { data: articles, error, response } = await GET("/articles"); if (error !== undefined) { throw json(error, { status: response.status }); } return json({ articles }); }; ``` 페이지에 연결하려면 라우트 파일에서 `loader`라는 이름으로 내보내야 합니다. pages/feed/index.ts ``` export { FeedPage } from "./ui/FeedPage"; export { loader } from "./api/loader"; ``` app/routes/\_index.tsx ``` import type { MetaFunction } from "@remix-run/node"; import { FeedPage } from "pages/feed"; export { loader } from "pages/feed"; export const meta: MetaFunction = () => { return [{ title: "Conduit" }]; }; export default FeedPage; ``` 마지막 단계는 피드에 이러한 카드를 렌더링하는 것입니다. `FeedPage`를 다음 코드로 업데이트하세요. pages/feed/ui/FeedPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const { articles } = useLoaderData(); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}
); } ``` ### 태그로 필터링[​](#태그로-필터링 "해당 헤딩으로 이동") 태그와 관련해서는 백엔드에서 태그를 가져오고 현재 선택된 태그를 저장해야 합니다. 가져오기 방법은 이미 알고 있습니다 — 로더에서 또 다른 요청을 하면 됩니다. `remix-utils` 패키지에서 `promiseHash`라는 편리한 함수를 사용할 것입니다. 이 패키지는 이미 설치되어 있습니다. 로더 파일인 `pages/feed/api/loader.ts`를 다음 코드로 업데이트하세요. pages/feed/api/loader.ts ``` import { json } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async () => { return json( await promiseHash({ articles: throwAnyErrors(GET("/articles")), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` 오류 처리를 일반 함수 `throwAnyErrors`로 추출했다는 점에 주목하세요. 꽤 유용해 보이므로 나중에 재사용할 수 있을 것 같습니다. 지금은 그냥 주목해 두겠습니다. 이제 태그 목록으로 넘어갑시다. 이는 상호작용이 가능해야 합니다 — 태그를 클릭하면 해당 태그가 선택되어야 합니다. Remix 규칙에 따라 URL 검색 매개변수를 선택된 태그의 저장소로 사용할 것입니다. 브라우저가 저장을 처리하게 하고 우리는 더 중요한 일에 집중하겠습니다. `pages/feed/ui/FeedPage.tsx`를 다음 코드로 업데이트하세요. pages/feed/ui/FeedPage.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const { articles, tags } = useLoaderData(); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}

Popular Tags

{tags.tags.map((tag) => ( ))}
); } ``` 그런 다음 로더에서 `tag` 검색 매개변수를 사용해야 합니다. `pages/feed/api/loader.ts`의 `loader` 함수를 다음과 같이 변경하세요. pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag } } }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` 이게 전부입니다. `model` 세그먼트가 필요하지 않습니다. Remix는 꽤 깔끔하죠. ### 페이지네이션[​](#페이지네이션 "해당 헤딩으로 이동") 비슷한 방식으로 페이지네이션을 구현할 수 있습니다. 직접 시도해 보거나 아래 코드를 복사하세요. 어차피 당신을 판단할 사람은 없습니다. pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } /** Amount of articles on one page. */ export const LIMIT = 20; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag, limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` pages/feed/ui/FeedPage.tsx ``` import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import { LIMIT, type loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const [searchParams] = useSearchParams(); const { articles, tags } = useLoaderData(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}
    {Array(pageAmount) .fill(null) .map((_, index) => index + 1 === currentPage ? (
  • {index + 1}
  • ) : (
  • ), )}

Popular Tags

{tags.tags.map((tag) => ( ))}
); } ``` 이것으로 완료되었습니다. 탭 목록도 비슷하게 구현할 수 있지만, 인증을 구현할 때까지 잠시 보류하겠습니다. 그런데 말이 나왔으니! ### 인증[​](#인증 "해당 헤딩으로 이동") 인증에는 두 개의 페이지가 관련됩니다 - 로그인과 회원가입입니다. 이들은 대부분 동일하므로 필요한 경우 코드를 재사용할 수 있도록 `sign-in`이라는 동일한 슬라이스에 유지하는 것이 합리적입니다. `pages/sign-in`의 `ui` 세그먼트에 다음 내용으로 `RegisterPage.tsx`를 만드세요. pages/sign-in/ui/RegisterPage.tsx ``` import { Form, Link, useActionData } from "@remix-run/react"; import type { register } from "../api/register"; export function RegisterPage() { const registerData = useActionData(); return (

Sign up

Have an account?

{registerData?.error && (
    {registerData.error.errors.body.map((error) => (
  • {error}
  • ))}
)}
); } ``` 이제 고쳐야 할 깨진 import가 있습니다. 새로운 세그먼트가 필요하므로 다음과 같이 만드세요. ``` npx fsd pages sign-in -s api ``` 그러나 등록의 백엔드 부분을 구현하기 전에 Remix가 세션을 처리할 수 있도록 일부 인프라 코드가 필요합니다. 다른 페이지에서도 필요할 수 있으므로 이는 Shared로 갑니다. 다음 코드를 `shared/api/auth.server.ts`에 넣으세요. 이는 Remix에 매우 특화된 것이므로 너무 걱정하지 마세요. 그냥 복사-붙여넣기 하세요. shared/api/auth.server.ts ``` import { createCookieSessionStorage, redirect } from "@remix-run/node"; import invariant from "tiny-invariant"; import type { User } from "./models"; invariant( process.env.SESSION_SECRET, "SESSION_SECRET must be set for authentication to work", ); const sessionStorage = createCookieSessionStorage<{ user: User; }>({ cookie: { name: "__session", httpOnly: true, path: "/", sameSite: "lax", secrets: [process.env.SESSION_SECRET], secure: process.env.NODE_ENV === "production", }, }); export async function createUserSession({ request, user, redirectTo, }: { request: Request; user: User; redirectTo: string; }) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); session.set("user", user); return redirect(redirectTo, { headers: { "Set-Cookie": await sessionStorage.commitSession(session, { maxAge: 60 * 60 * 24 * 7, // 7 days }), }, }); } export async function getUserFromSession(request: Request) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); return session.get("user") ?? null; } export async function requireUser(request: Request) { const user = await getUserFromSession(request); if (user === null) { throw redirect("/login"); } return user; } ``` 그리고 바로 옆에 있는 `models.ts` 파일에서 `User` 모델도 내보내세요. shared/api/models.ts ``` import type { components } from "./v1"; export type Article = components["schemas"]["Article"]; export type User = components["schemas"]["User"]; ``` 이 코드가 작동하려면 `SESSION_SECRET` 환경 변수를 설정해야 합니다. 프로젝트 루트에 `.env` 파일을 만들고 `SESSION_SECRET=`을 작성한 다음 키보드에서 무작위로 키를 눌러 긴 무작위 문자열을 만드세요. 다음과 같은 결과가 나와야 합니다. .env ``` SESSION_SECRET=dontyoudarecopypastethis ``` 마지막으로 이 코드를 사용하기 위해 공개 API에 일부 내보내기를 추가하세요. shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; export { createUserSession, getUserFromSession, requireUser } from "./auth.server"; ``` 이제 RealWorld 백엔드와 실제로 통신하여 등록을 수행하는 코드를 작성할 수 있습니다. 그것을 `pages/sign-in/api`에 유지할 것입니다. `register.ts`라는 파일을 만들고 다음 코드를 넣으세요. pages/sign-in/api/register.ts ``` import { json, type ActionFunctionArgs } from "@remix-run/node"; import { POST, createUserSession } from "shared/api"; export const register = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const username = formData.get("username")?.toString() ?? ""; const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users", { body: { user: { email, password, username } }, }); if (error) { return json({ error }, { status: 400 }); } else { return createUserSession({ request: request, user: data.user, redirectTo: "/", }); } }; ``` pages/sign-in/index.ts ``` export { RegisterPage } from './ui/RegisterPage'; export { register } from './api/register'; ``` 거의 다 왔습니다! 페이지와 액션을 `/register` 라우트에 연결하기만 하면 됩니다. `app/routes`에 `register.tsx`를 만드세요. app/routes/register.tsx ``` import { RegisterPage, register } from "pages/sign-in"; export { register as action }; export default RegisterPage; ``` 이제 로 가면 사용자를 생성할 수 있어야 합니다! 애플리케이션의 나머지 부분은 아직 이에 반응하지 않을 것입니다. 곧 그 문제를 해결하겠습니다. 매우 유사한 방식으로 로그인 페이지를 구현할 수 있습니다. 직접 시도해 보거나 그냥 코드를 가져와서 계속 진행하세요. pages/sign-in/api/sign-in.ts ``` import { json, type ActionFunctionArgs } from "@remix-run/node"; import { POST, createUserSession } from "shared/api"; export const signIn = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users/login", { body: { user: { email, password } }, }); if (error) { return json({ error }, { status: 400 }); } else { return createUserSession({ request: request, user: data.user, redirectTo: "/", }); } }; ``` pages/sign-in/ui/SignInPage.tsx ``` import { Form, Link, useActionData } from "@remix-run/react"; import type { signIn } from "../api/sign-in"; export function SignInPage() { const signInData = useActionData(); return (

Sign in

Need an account?

{signInData?.error && (
    {signInData.error.errors.body.map((error) => (
  • {error}
  • ))}
)}
); } ``` pages/sign-in/index.ts ``` export { RegisterPage } from './ui/RegisterPage'; export { register } from './api/register'; export { SignInPage } from './ui/SignInPage'; export { signIn } from './api/sign-in'; ``` app/routes/login.tsx ``` import { SignInPage, signIn } from "pages/sign-in"; export { signIn as action }; export default SignInPage; ``` 이제 사용자가 이 페이지에 실제로 접근할 수 있는 방법을 제공해 봅시다. ### 헤더[​](#헤더 "해당 헤딩으로 이동") 1부에서 논의했듯이, 앱 헤더는 일반적으로 Widgets나 Shared에 배치됩니다. 매우 간단하고 모든 비즈니스 로직을 외부에 유지할 수 있기 때문에 Shared에 넣을 것입니다. 이를 위한 장소를 만들어 봅시다. ``` npx fsd shared ui ``` 이제 다음 내용으로 `shared/ui/Header.tsx`를 만드세요. shared/ui/Header.tsx ``` import { useContext } from "react"; import { Link, useLocation } from "@remix-run/react"; import { CurrentUser } from "../api/currentUser"; export function Header() { const currentUser = useContext(CurrentUser); const { pathname } = useLocation(); return ( ); } ``` 이 컴포넌트를 `shared/ui`에서 내보내세요. shared/ui/index.ts ``` export { Header } from "./Header"; ``` 헤더에서는 `shared/api`에 유지되는 컨텍스트에 의존합니다. 그것도 만드세요. shared/api/currentUser.ts ``` import { createContext } from "react"; import type { User } from "./models"; export const CurrentUser = createContext(null); ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; export { createUserSession, getUserFromSession, requireUser } from "./auth.server"; export { CurrentUser } from "./currentUser"; ``` 이제 페이지에 헤더를 추가해 봅시다. 모든 페이지에 있어야 하므로 루트 라우트에 추가하고 outlet(페이지가 렌더링될 위치)을 `CurrentUser` 컨텍스트 제공자로 감싸는 것이 합리적입니다. 이렇게 하면 전체 앱과 헤더가 현재 사용자 객체에 접근할 수 있습니다. 또한 쿠키에서 실제로 현재 사용자 객체를 가져오는 로더를 추가할 것입니다. `app/root.tsx`에 다음 내용을 넣으세요. app/root.tsx ``` import { cssBundleHref } from "@remix-run/css-bundle"; import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node"; import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData, } from "@remix-run/react"; import { Header } from "shared/ui"; import { getUserFromSession, CurrentUser } from "shared/api"; export const links: LinksFunction = () => [ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), ]; export const loader = ({ request }: LoaderFunctionArgs) => getUserFromSession(request); export default function App() { const user = useLoaderData(); return (
); } ``` 이 시점에서 홈 페이지에 다음과 같은 내용이 표시되어야 합니다. ![The feed page of Conduit, including the header, the feed, and the tags. The tabs are still missing.](/documentation/kr/assets/images/realworld-feed-without-tabs-5da4c9072101ac20e82e2234bd3badbe.jpg) 헤더, 피드, 태그를 포함한 Conduit의 피드 페이지. 탭은 아직 없습니다. ### 탭[​](#탭 "해당 헤딩으로 이동") 이제 인증 상태를 감지할 수 있으므로 탭과 글 좋아요를 빠르게 구현하여 피드 페이지를 완성해 봅시다. 또 다른 폼이 필요하지만 이 페이지 파일이 꽤 커지고 있으므로 이러한 폼을 인접한 파일로 옮기겠습니다. `Tabs.tsx`, `PopularTags.tsx`, `Pagination.tsx`를 다음 내용으로 만들 것입니다. pages/feed/ui/Tabs.tsx ``` import { useContext } from "react"; import { Form, useSearchParams } from "@remix-run/react"; import { CurrentUser } from "shared/api"; export function Tabs() { const [searchParams] = useSearchParams(); const currentUser = useContext(CurrentUser); return (
    {currentUser !== null && (
  • )}
  • {searchParams.has("tag") && (
  • {searchParams.get("tag")}
  • )}
); } ``` pages/feed/ui/PopularTags.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import type { loader } from "../api/loader"; export function PopularTags() { const { tags } = useLoaderData(); return (

Popular Tags

{tags.tags.map((tag) => ( ))}
); } ``` pages/feed/ui/Pagination.tsx ``` import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import { LIMIT, type loader } from "../api/loader"; export function Pagination() { const [searchParams] = useSearchParams(); const { articles } = useLoaderData(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (
    {Array(pageAmount) .fill(null) .map((_, index) => index + 1 === currentPage ? (
  • {index + 1}
  • ) : (
  • ), )}
); } ``` 이제 `FeedPage`를 다음과 같이 업데이트하세요. pages/feed/ui/FeedPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; import { Tabs } from "./Tabs"; import { PopularTags } from "./PopularTags"; import { Pagination } from "./Pagination"; export function FeedPage() { const { articles } = useLoaderData(); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}
); } ``` 마지막으로 로더를 업데이트하여 새로운 필터를 처리하세요. pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET, requireUser } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { /* unchanged */ } /** Amount of articles on one page. */ export const LIMIT = 20; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); if (url.searchParams.get("source") === "my-feed") { const userSession = await requireUser(request); return json( await promiseHash({ articles: throwAnyErrors( GET("/articles/feed", { params: { query: { limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, headers: { Authorization: `Token ${userSession.token}` }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); } return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag, limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` 피드 페이지를 떠나기 전에, 글에 대한 좋아요를 처리하는 코드를 추가해 봅시다. `ArticlePreview.tsx`를 다음과 같이 변경하세요. pages/feed/ui/ArticlePreview\.tsx ``` import { Form, Link } from "@remix-run/react"; import type { Article } from "shared/api"; interface ArticlePreviewProps { article: Article; } export function ArticlePreview({ article }: ArticlePreviewProps) { return (
{article.author.username} {new Date(article.createdAt).toLocaleDateString(undefined, { dateStyle: "long", })}

{article.title}

{article.description}

Read more...
    {article.tagList.map((tag) => (
  • {tag}
  • ))}
); } ``` 이 코드는 글에 좋아요를 표시하기 위해 `/article/:slug`로 `_action=favorite`과 함께 POST 요청을 보냅니다. 아직 작동하지 않겠지만, 글 읽기 페이지 작업을 시작하면서 이것도 구현할 것입니다. 이것으로 피드가 공식적으로 완성되었습니다! 야호! ### 글 읽기 페이지[​](#글-읽기-페이지 "해당 헤딩으로 이동") 먼저 데이터가 필요합니다. 로더를 만들어 봅시다. ``` npx fsd pages article-read -s api ``` pages/article-read/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import invariant from "tiny-invariant"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET, getUserFromSession } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ request, params }: LoaderFunctionArgs) => { invariant(params.slug, "Expected a slug parameter"); const currentUser = await getUserFromSession(request); const authorization = currentUser ? { Authorization: `Token ${currentUser.token}` } : undefined; return json( await promiseHash({ article: throwAnyErrors( GET("/articles/{slug}", { params: { path: { slug: params.slug }, }, headers: authorization, }), ), comments: throwAnyErrors( GET("/articles/{slug}/comments", { params: { path: { slug: params.slug }, }, headers: authorization, }), ), }), ); }; ``` pages/article-read/index.ts ``` export { loader } from "./api/loader"; ``` 이제 `/article/:slug` 라우트에 연결할 수 있습니다. `article.$slug.tsx`라는 라우트 파일을 만드세요. app/routes/article.$slug.tsx ``` export { loader } from "pages/article-read"; ``` 페이지 자체는 세 가지 주요 블록으로 구성됩니다 - 글 헤더와 액션(두 번 반복), 글 본문, 댓글 섹션입니다. 다음은 페이지의 마크업입니다. 특별히 흥미로운 내용은 없습니다: pages/article-read/ui/ArticleReadPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticleMeta } from "./ArticleMeta"; import { Comments } from "./Comments"; export function ArticleReadPage() { const { article } = useLoaderData(); return (

{article.article.title}

{article.article.body}

    {article.article.tagList.map((tag) => (
  • {tag}
  • ))}

); } ``` 더 흥미로운 것은 `ArticleMeta`와 `Comments`입니다. 이들은 글 좋아요, 댓글 작성 등과 같은 쓰기 작업을 포함합니다. 이들을 작동시키려면 먼저 백엔드 부분을 구현해야 합니다. 페이지의 `api` 세그먼트에 `action.ts`를 만드세요: pages/article-read/api/action.ts ``` import { redirect, type ActionFunctionArgs } from "@remix-run/node"; import { namedAction } from "remix-utils/named-action"; import { redirectBack } from "remix-utils/redirect-back"; import invariant from "tiny-invariant"; import { DELETE, POST, requireUser } from "shared/api"; export const action = async ({ request, params }: ActionFunctionArgs) => { const currentUser = await requireUser(request); const authorization = { Authorization: `Token ${currentUser.token}` }; const formData = await request.formData(); return namedAction(formData, { async delete() { invariant(params.slug, "Expected a slug parameter"); await DELETE("/articles/{slug}", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirect("/"); }, async favorite() { invariant(params.slug, "Expected a slug parameter"); await POST("/articles/{slug}/favorite", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async unfavorite() { invariant(params.slug, "Expected a slug parameter"); await DELETE("/articles/{slug}/favorite", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async createComment() { invariant(params.slug, "Expected a slug parameter"); const comment = formData.get("comment"); invariant(typeof comment === "string", "Expected a comment parameter"); await POST("/articles/{slug}/comments", { params: { path: { slug: params.slug } }, headers: { ...authorization, "Content-Type": "application/json" }, body: { comment: { body: comment } }, }); return redirectBack(request, { fallback: "/" }); }, async deleteComment() { invariant(params.slug, "Expected a slug parameter"); const commentId = formData.get("id"); invariant(typeof commentId === "string", "Expected an id parameter"); const commentIdNumeric = parseInt(commentId, 10); invariant( !Number.isNaN(commentIdNumeric), "Expected a numeric id parameter", ); await DELETE("/articles/{slug}/comments/{id}", { params: { path: { slug: params.slug, id: commentIdNumeric } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async followAuthor() { const authorUsername = formData.get("username"); invariant( typeof authorUsername === "string", "Expected a username parameter", ); await POST("/profiles/{username}/follow", { params: { path: { username: authorUsername } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async unfollowAuthor() { const authorUsername = formData.get("username"); invariant( typeof authorUsername === "string", "Expected a username parameter", ); await DELETE("/profiles/{username}/follow", { params: { path: { username: authorUsername } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, }); }; ``` 그 슬라이스에서 이를 내보내고 라우트에서도 내보내세요. 그리고 페이지 자체도 연결하겠습니다. pages/article-read/index.ts ``` export { ArticleReadPage } from "./ui/ArticleReadPage"; export { loader } from "./api/loader"; export { action } from "./api/action"; ``` app/routes/article.$slug.tsx ``` import { ArticleReadPage } from "pages/article-read"; export { loader, action } from "pages/article-read"; export default ArticleReadPage; ``` 이제 독자 페이지에서 좋아요 버튼을 아직 구현하지 않았지만, 피드의 좋아요 버튼이 작동하기 시작할 것입니다! 이 라우트로 "좋아요" 요청을 보내고 있었기 때문입니다. 한번 시도해 보세요. `ArticleMeta`와 `Comments`는 다시 한번 폼들의 모음입니다. 이전에 이미 해봤으니, 코드를 가져와서 넘어가겠습니다. pages/article-read/ui/ArticleMeta.tsx ``` import { Form, Link, useLoaderData } from "@remix-run/react"; import { useContext } from "react"; import { CurrentUser } from "shared/api"; import type { loader } from "../api/loader"; export function ArticleMeta() { const currentUser = useContext(CurrentUser); const { article } = useLoaderData(); return (
{article.article.author.username} {article.article.createdAt}
{article.article.author.username == currentUser?.username ? ( <> Edit Article    ) : ( <>    )}
); } ``` pages/article-read/ui/Comments.tsx ``` import { useContext } from "react"; import { Form, Link, useLoaderData } from "@remix-run/react"; import { CurrentUser } from "shared/api"; import type { loader } from "../api/loader"; export function Comments() { const { comments } = useLoaderData(); const currentUser = useContext(CurrentUser); return (
{currentUser !== null ? (
) : (

Sign in   or   Sign up   to add comments on this article.

)} {comments.comments.map((comment) => (

{comment.body}

  {comment.author.username} {comment.createdAt} {comment.author.username === currentUser?.username && (
)}
))}
); } ``` 이것으로 우리의 글 읽기 페이지도 완성되었습니다! 이제 작성자를 팔로우하고, 글에 좋아요를 누르고, 댓글을 남기는 버튼들이 예상대로 작동해야 합니다. ![Article reader with functioning buttons to like and follow](/documentation/kr/assets/images/realworld-article-reader-6a420e4f2afe139d2bdd54d62974f0b9.jpg) 기능하는 좋아요와 팔로우 버튼이 있는 글 읽기 페이지 ### 글 작성 페이지[​](#글-작성-페이지 "해당 헤딩으로 이동") 이것은 이 튜토리얼에서 다룰 마지막 페이지이며, 여기서 가장 흥미로운 부분은 폼 데이터를 어떻게 검증할 것인가 입니다. 페이지 자체인 `article-edit/ui/ArticleEditPage.tsx`는 꽤 간단할 것이며, 추가적인 복잡성은 다른 두 개의 컴포넌트로 숨겨질 것입니다. pages/article-edit/ui/ArticleEditPage.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { TagsInput } from "./TagsInput"; import { FormErrors } from "./FormErrors"; export function ArticleEditPage() { const article = useLoaderData(); return (
); } ``` 이 페이지는 현재 글(새로 작성하는 경우가 아니라면)을 가져와서 해당하는 폼 필드를 채웁니다. 이전에 본 적이 있습니다. 흥미로운 부분은 `FormErrors`인데, 이는 검증 결과를 받아 사용자에게 표시할 것입니다. 한번 살펴보겠습니다. pages/article-edit/ui/FormErrors.tsx ``` import { useActionData } from "@remix-run/react"; import type { action } from "../api/action"; export function FormErrors() { const actionData = useActionData(); return actionData?.errors != null ? (
    {actionData.errors.map((error) => (
  • {error}
  • ))}
) : null; } ``` 여기서는 우리의 액션이 `errors` 필드, 즉 사람이 읽을 수 있는 오류 메시지 배열을 반환할 것이라고 가정하고 있습니다. 곧 액션에 대해 다루겠습니다. 또 다른 컴포넌트는 태그 입력입니다. 이는 단순한 입력 필드에 선택된 태그의 추가적인 미리보기가 있는 것입니다. 여기에는 특별한 것이 없습니다: pages/article-edit/ui/TagsInput.tsx ``` import { useEffect, useRef, useState } from "react"; export function TagsInput({ name, defaultValue, }: { name: string; defaultValue?: Array; }) { const [tagListState, setTagListState] = useState(defaultValue ?? []); function removeTag(tag: string): void { const newTagList = tagListState.filter((t) => t !== tag); setTagListState(newTagList); } const tagsInput = useRef(null); useEffect(() => { tagsInput.current && (tagsInput.current.value = tagListState.join(",")); }, [tagListState]); return ( <> setTagListState(e.target.value.split(",").filter(Boolean)) } />
{tagListState.map((tag) => ( [" ", "Enter"].includes(e.key) && removeTag(tag) } onClick={() => removeTag(tag)} >{" "} {tag} ))}
); } ``` 이제 API 부분입니다. 로더는 URL을 살펴보고, 글 슬러그가 포함되어 있다면 기존 글을 수정하는 것이므로 해당 데이터를 로드해야 합니다. 그렇지 않으면 아무것도 반환하지 않습니다. 그 로더를 만들어 봅시다. pages/article-edit/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { GET, requireUser } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ params, request }: LoaderFunctionArgs) => { const currentUser = await requireUser(request); if (!params.slug) { return { article: null }; } return throwAnyErrors( GET("/articles/{slug}", { params: { path: { slug: params.slug } }, headers: { Authorization: `Token ${currentUser.token}` }, }), ); }; ``` 액션은 새로운 필드 값들을 받아 우리의 데이터 스키마를 통해 실행하고, 모든 것이 올바르다면 이러한 변경사항을 백엔드에 커밋합니다. 이는 기존 글을 업데이트하거나 새 글을 생성하는 방식으로 이루어집니다. pages/article-edit/api/action.ts ``` import { json, redirect, type ActionFunctionArgs } from "@remix-run/node"; import { POST, PUT, requireUser } from "shared/api"; import { parseAsArticle } from "../model/parseAsArticle"; export const action = async ({ request, params }: ActionFunctionArgs) => { try { const { body, description, title, tags } = parseAsArticle( await request.formData(), ); const tagList = tags?.split(",") ?? []; const currentUser = await requireUser(request); const payload = { body: { article: { title, description, body, tagList, }, }, headers: { Authorization: `Token ${currentUser.token}` }, }; const { data, error } = await (params.slug ? PUT("/articles/{slug}", { params: { path: { slug: params.slug } }, ...payload, }) : POST("/articles", payload)); if (error) { return json({ errors: error }, { status: 422 }); } return redirect(`/article/${data.article.slug ?? ""}`); } catch (errors) { return json({ errors }, { status: 400 }); } }; ``` 스키마는 `FormData`를 위한 파싱 함수로도 작동하여, 깨끗한 필드를 편리하게 얻거나 마지막에 처리할 오류를 던질 수 있게 해줍니다. 그 파싱 함수는 다음과 같이 보일 수 있습니다. pages/article-edit/model/parseAsArticle.ts ``` export function parseAsArticle(data: FormData) { const errors = []; const title = data.get("title"); if (typeof title !== "string" || title === "") { errors.push("Give this article a title"); } const description = data.get("description"); if (typeof description !== "string" || description === "") { errors.push("Describe what this article is about"); } const body = data.get("body"); if (typeof body !== "string" || body === "") { errors.push("Write the article itself"); } const tags = data.get("tags"); if (typeof tags !== "string") { errors.push("The tags must be a string"); } if (errors.length > 0) { throw errors; } return { title, description, body, tags: data.get("tags") ?? "" } as { title: string; description: string; body: string; tags: string; }; } ``` 물론 이는 다소 길고 반복적이지만, 사람이 읽을 수 있는 오류 메시지를 위해 우리가 지불해야 하는 대가입니다. 이것은 Zod 스키마일 수도 있지만, 그렇게 하면 프론트엔드에서 오류 메시지를 렌더링해야 하고, 이 폼은 그런 복잡성을 감당할 만한 가치가 없습니다. 마지막 단계로 - 페이지, 로더, 그리고 액션을 라우트에 연결합니다. 우리는 생성과 편집을 모두 깔끔하게 지원하므로 `editor._index.tsx`와 `editor.$slug.tsx` 모두에서 동일한 것을 내보낼 수 있습니다. pages/article-edit/index.ts ``` export { ArticleEditPage } from "./ui/ArticleEditPage"; export { loader } from "./api/loader"; export { action } from "./api/action"; ``` app/routes/editor.\_index.tsx, app/routes/editor.$slug.tsx (same content) ``` import { ArticleEditPage } from "pages/article-edit"; export { loader, action } from "pages/article-edit"; export default ArticleEditPage; ``` 이제 완료되었습니다! 로그인하고 새 글을 작성해보세요. 또는 글을 "잊어버리고" 검증이 작동하는 것을 확인해보세요. ![The Conduit article editor, with the title field saying “New article” and the rest of the fields empty. Above the form there are two errors: “Describe what this article is about” and “Write the article itself”.](/documentation/kr/assets/images/realworld-article-editor-bc3ee45c96ae905fdbb54d6463d12723.jpg) 제목 필드에 "새 글"이라고 쓰여 있고 나머지 필드는 비어 있는 Conduit 글 편집기. 폼 위에 두 개의 오류가 있습니다. **"이 글이 무엇에 관한 것인지 설명해주세요"**, **"글 본문을 작성해주세요"**. 프로필과 설정 페이지는 글 읽기와 편집기 페이지와 매우 유사하므로, 독자인 여러분의 연습 과제로 남겨두겠습니다 :) --- # Handling API Requests ## Shared API Requests[​](#shared-api-requests "해당 헤딩으로 이동") Shared API request 로직은 `shared/api` 폴더에 두세요.
이렇게 하면 애플리케이션 전체에서 손쉽게 재사용할 수 있고, 빠른 프로토타이핑이 가능합니다. 대부분의 프로젝트에서는 이 폴더 구조와 client.ts 설정만으로 충분합니다. 일반적인 파일 구조 예시: * 📂 shared * 📂 api * 📄 client.ts * 📄 index.ts * 📂 endpoints * 📄 login.ts `client.ts` 파일은 HTTP request 관련 설정을 **한 곳에서** 관리합니다.
`fetch()` 또는 `axios` instance에 공통 설정을 적용하여 다음을 처리합니다: * 백엔드 기본 URL * Default headers (예: 인증 header) * 데이터 직렬화 아래 예시를 참고하세요. * Axios * Fetch shared/api/client.ts ``` // Axios 예시 import axios from 'axios'; export const client = axios.create({ baseURL: 'https://your-api-domain.com/api/', timeout: 5000, headers: { 'X-Custom-Header': 'my-custom-value' } }); ``` shared/api/client.ts ``` export const client = { async post(endpoint: string, body: any, options?: RequestInit) { const response = await fetch(`https://your-api-domain.com/api${endpoint}`, { method: 'POST', body: JSON.stringify(body), ...options, headers: { 'Content-Type': 'application/json', 'X-Custom-Header': 'my-custom-value', ...options?.headers, }, }); return response.json(); } // ... other methods like put, delete, etc. }; ``` `shared/api/endpoints` 폴더에 endpoint별 request 함수를 정리하세요. note 예시의 가독성을 위해 form handling과 검증(Zod·Valibot 등)은 생략했습니다. 자세한 내용은 [Type Validation and Schemas](/documentation/kr/docs/guides/examples/types.md#type-validation-schemas-and-zod)에서 확인하세요. shared/api/endpoints/login.ts ``` import { client } from '../client'; export interface LoginCredentials { email: string; password: string; } export function login(credentials: LoginCredentials) { return client.post('/login', credentials); } ``` `shared/api/index.ts`에서 request 함수와 타입을 내보내세요: shared/api/index.ts ``` export { client } from './client'; // If you want to export the client itself export { login } from './endpoints/login'; export type { LoginCredentials } from './endpoints/login'; ``` ## Slice-specific API Requests[​](#slice-specific-api-requests "해당 헤딩으로 이동") 특정 페이지나 feature에서만 쓰이는 request는 해당 slice의 api 폴더에 두어 정리하세요. 이렇게 하면 slice별 로직이 깔끔하게 분리됩니다. * 📂 pages * 📂 login * 📄 index.ts * 📂 api * 📄 login.ts * 📂 ui * 📄 LoginPage.tsx pages/login/api/login.ts ``` import { client } from 'shared/api'; interface LoginCredentials { email: string; password: string; } export function login(credentials: LoginCredentials) { return client.post('/login', credentials); } ``` 이 함수는 재사용 가능성이 낮으므로, slice의 public API로 내보낼 필요가 없습니다. note entities layer에 API request와 response 타입을 배치하지 마세요. 백엔드 response 타입과 프론트엔드 `entities` 타입이 다를 수 있습니다. `shared/api`나 slice의 `api` 폴더에서 데이터를 변환하고, entities는 프론트엔드 관점에 집중하도록 설계하세요. ## API 타입과 클라이언트 자동 생성[​](#api-타입과-클라이언트-자동-생성 "해당 헤딩으로 이동") 백엔드에 OpenAPI 스펙이 있다면 [orval](https://orval.dev/)이나 [openapi-typescript](https://openapi-ts.dev/) 같은 도구로 API 타입과 request 함수를 생성할 수 있습니다. 생성된 코드는 `shared/api/openapi` 등에 두고 `README.md`에 생성 방법과 타입 설명을 문서화하세요. ## 서버 상태 라이브러리 연동[​](#서버-상태-라이브러리-연동 "해당 헤딩으로 이동") [TanStack Query (React Query)](https://tanstack.com/query/latest)나 [Pinia Colada](https://pinia-colada.esm.dev/) 같은 서버 상태 관리 라이브러리를 사용할 때는 slice 간 타입이나 cache key를 공유해야 할 수 있습니다. 이럴 때는 `shared` layer에 다음을 배치하세요. * API 데이터 타입 (API data types) * 캐시 키 (cache keys) * 공통 query/mutation 옵션 (common query/mutation options) --- # Authentication 일반적으로 **인증(Authentication)** 플로우는 세 단계로 구성됩니다. 1. **Credential 입력 수집** — 아이디, 패스워드(또는 OAuth redirect URL)를 입력받습니다. 2. **백엔드 Endpoint 호출** — `/login`, `/oauth/callback`, `/2fa` 등 로그인 관련 API endpoint에 request을 보냅니다. 3. **Token 저장** — 발급된 token을 **cookie** 또는 **store** 에 저장해 이후 request에 사용합니다. ## 1. Credential 입력 수집[​](#1-credential-입력-수집 "해당 헤딩으로 이동") > OAuth 로그인을 사용한다면 **2단계를 건너뛰고 바로 [token 저장](#how-to-store-the-token-for-authenticated-requests)** 단계로 이동합니다. ### 1‑1. 로그인 전용 페이지[​](#11-로그인전용-페이지 "해당 헤딩으로 이동") 웹 애플리케이션에서는 보통 **/login** 같은 로그인 전용 페이지를 제공해 사용자 이름과 패스워드를 입력받습니다.
페이지가 단순하므로 추가 **decomposition(구조 분할)** 이 필요 없으며, 로그인 폼과 회원가입 폼을 하나의 컴포넌트로 만들어 재사용할 수 있습니다. * 📂 pages * 📂 login * 📂 ui * 📄 LoginPage.tsx (or your framework's component file format) * 📄 RegisterPage.tsx * 📄 index.ts * other pages… * `LoginPage`·`RegisterPage` 두 컴포넌트를 **분리**해 구현하고, 필요 시 `index.ts`에서 export 합니다. * 각 컴포넌트는 **form elements**와 form submit handler만 포함해 단순성을 유지합니다. ### 1‑2. 로그인 dialog 만들기[​](#12-로그인dialog-만들기 "해당 헤딩으로 이동") 모든 페이지에서 호출할 로그인 dialog가 필요하다면 **재사용 가능한 widget**으로 구현하세요.
widget으로 만들면 과도하게 구조를 쪼개지 않으면서도, 어떤 페이지에서도 동일한 dialog을 쉽게 띄울 수 있습니다. * 📂 widgets * 📂 login-dialog * 📂 ui * 📄 LoginDialog.tsx * 📄 index.ts * other widgets… > 이후 설명은 로그인 전용 페이지를 기준으로 하지만, 동일한 원칙이 dialog widget에도 적용됩니다. ### 1‑3. Client‑side Validation[​](#13-clientsidevalidation "해당 헤딩으로 이동") 회원가입 페이지에서 입력 오류를 즉시 알려 주는 것이 UX에 도움이 됩니다.
검증 schema는 `pages/login/model` segment에 정의하고 `ui` segment에서 재사용하세요.
아래 예시는 [Zod](https://zod.dev) 로 타입과 값을 동시에 검증하는 패턴입니다. pages/login/model/registration-schema.ts ``` import { z } from "zod"; export const registrationData = z.object({ email: z.string().email(), password: z.string().min(6), confirmPassword: z.string(), }).refine((data) => data.password === data.confirmPassword, { message: "비밀번호가 일치하지 않습니다", path: ["confirmPassword"], }); ``` 그런 다음, ui segment에서 이 schema를 사용해 사용자 입력을 검증할 수 있습니다: pages/login/ui/RegisterPage.tsx ``` import { registrationData } from "../model/registration-schema"; function validate(formData: FormData) { const data = Object.fromEntries(formData.entries()); try { registrationData.parse(data); } catch (error) { // TODO: Show error message to the user } } export function RegisterPage() { return (
validate(new FormData(e.target))}>
) } ``` ## 2. Send credentials[​](#2-send-credentials "해당 헤딩으로 이동") 사용자 **credentials**(e‑mail, password)을 백엔드 **endpoint**로 전송하는 request 함수을 생성합니다.
이 함수는 Zustand, Redux Toolkit, **TanStack Query** `useMutation` 등에서 호출할 수 있습니다. ### 2‑1. 함수 placement[​](#21-함수-placement "해당 헤딩으로 이동") | 목적 | 권장 위치 | 이유 | | ----------- | ----------------- | -------------------------- | | 전역 재사용 | `shared/api` | 모든 slice에서 import 가능 | | 로그인 전용 | `pages/login/api` | slice 내부 capsule 유지 | #### `shared/api`에 저장하기[​](#sharedapi에-저장하기 "해당 헤딩으로 이동") 모든 API request을 `shared/api`에 모아 endpoint로 그룹화합니다. * 📂 shared * 📂 api * 📂 endpoints * 📄 login.ts * other endpoint functions… * 📄 client.ts * 📄 index.ts `📄 client.ts`는 원시 request 함수(`fetch` 등)를 감싸 **기본 URL, 공통 헤더, 직렬화** 등을 처리합니다. shared/api/endpoints/login.ts ``` import { POST } from "../client"; export function login({ email, password }: { email: string, password: string }) { return POST("/login", { email, password }); } ``` shared/api/index.ts ``` export { login } from "./endpoints/login"; ``` #### page의 `api` segment에 저장하기[​](#page의-api-segment에-저장하기 "해당 헤딩으로 이동") 로그인 요청이 로그인 페이지에서만 필요하다면, 해당 페이지의 `api` segment에 함수를 두십시오. * 📂 pages * 📂 login * 📂 api * 📄 login.ts * 📂 ui * 📄 LoginPage.tsx * 📄 index.ts * other pages… pages/login/api/login.ts ``` import { POST } from "shared/api"; export function login({ email, password }: { email: string, password: string }) { return POST("/login", { email, password }); } ``` > 이 함수는 로그인 페이지 내부에서만 사용하므로 index.ts에 재-export할 필요가 없습니다. ### Two‑Factor Auth (2FA)[​](#twofactorauth2fa "해당 헤딩으로 이동") 1. `/login` 응답에 `has2FA` 플래그가 있으면 `/login/2fa` 페이지로 redirect합니다. 2. 2FA 페이지와 관련 API는 `pages/login` slice에 함께 둡니다. 3. `/2fa/verify` 같은 별도 endpoint를 호출하는 함수를 `shared/api` 또는 `pages/login/api`에 배치합니다. ## Authenticated Requests를 위한 token 저장[​](#how-to-store-the-token-for-authenticated-requests "해당 헤딩으로 이동") 로그인, 비밀번호, OAuth, 2단계 인증 등 어떤 방식이든 인증 API 호출의 **응답(response)** 으로 token을 받습니다.
이 token을 저장해 두면 이후 **모든 API 요청(request)** 에 token을 자동으로 포함해 인증을 통과할 수 있습니다. 웹 애플리케이션에서 token을 저장하기에 **가장 바람직한 방법은 cookie**입니다. cookie를 사용하면 token을 직접 저장하거나 관리할 필요가 없으므로, 프론트엔드 아키텍처 차원에서 별도의 고려가 거의 필요 없습니다. 프레임워크에 서버 사이드 기능이 있다면(예: [Remix](https://remix.run)), 서버 측 cookie 로직을 `shared/api`에 두세요. Remix 예제는 [튜토리얼의 Authentication 섹션](/documentation/kr/docs/get-started/tutorial.md#authentication)을 참고하면 됩니다. 그러나 cookie를 사용할 수 없는 환경도 있습니다. 이 경우 token을 **직접** 저장하고, 만료 시 token을 갱신(Refresh)하는 로직도 구현해야 합니다. FSD에서는 **어느 layer 또는 어느 segment에** token을 저장할지, 그리고 **어떻게** 앱 전역에 노출할지 다양한 선택지가 존재합니다. ### 3‑1. Shared[​](#31-shared "해당 헤딩으로 이동") 이 접근법은 `shared/api`에 정의한 **API 클라이언트**와 잘 어울리는 방식입니다. token을 module scope나 reactive store에 담아 두면, 인증이 필요한 다른 API 호출 함수에서 그대로 참조할 수 있습니다. token 자동 재발급(Refresh)는 클라이언트 **middleware**로 구현합니다. 1. 로그인 시 **access token, refresh token** 저장 2. 인증이 필요한 request 실행 3. 만료 코드가 오면 refresh token으로 새 token을 받아 저장하고 **기존 request**을 재시도 #### Token 관리 분리 전략[​](#token-관리-분리-전략 "해당 헤딩으로 이동") * **전담 segment 부재**
token 저장, 재발급 로직이 request 로직과 같은 파일에 섞이면
규모가 커질수록 유지보수가 어려워집니다.
→ **request 함수, 클라이언트**는 `shared/api`,
**token 관리 로직**은 `shared/auth` segment로 분리하세요. * **token과 사용자 정보를 함께 받는 경우**
백엔드가 token과 함께 **현재 사용자 정보**를 반환한다면 1. 별도 store에 함께 저장하거나 2. `/me`·`/users/current` 엔드포인트를 다시 호출해 가져올 수 있습니다. ### 3‑2. Entities[​](#32-entities "해당 헤딩으로 이동") FSD 프로젝트에서는 **User entity**(또는 **Current User entity**)를 두는 경우가 많습니다.
두 entity가 하나로 합쳐져도 무방합니다. note **Current User**는 “viewer” 또는 “me”라고도 부릅니다.
권한·개인 정보가 있는 **단일 인증 사용자**와, 공개 목록에 나타나는 **모든 사용자 목록**를 구분하기 위해서입니다. #### Token을 User Entities에 저장하기[​](#token을-user-entities에-저장하기 "해당 헤딩으로 이동") `model` segment에 **reactive store**를 만들고, token과 user 객체를 함께 보관하세요. API 클라이언트는 일반적으로 `shared/api` 정의되거나 entity 전체에 분산되어 있습니다. 따라서 주요 과제는 layer의 import 규칙([import rule on layers](/documentation/kr/docs/reference/layers.md#import-rule-on-layers))을 위반하지 않으면서 다른 request에서도 token을 사용할 수 있도록 하는 것입니다. > Layer 규칙 — Slice의 module은 **자기보다 아래 layer**의 Slice만 import할 수 있습니다. ##### 해결 방법[​](#해결-방법 "해당 헤딩으로 이동") 1. **request마다 token을 직접 넘기기** * 구현은 단순하지만 반복적이고, 타입 안전성이 없으면 실수 위험이 큽니다. * `shared/api`에 middleware pattern을 적용하기도 어렵습니다. 2. **앱 전역(Context / `localStorage`)에 노출** * token key는 `shared/api`에 두고, token store는 User entity에서 export합니다. * Context Provider는 App layer에 배치합니다. * 설계 자유도가 높지만, 상위 layer에 **암묵적 의존성**이 생깁니다.
⇒ Context나 `localStorage`가 누락된 경우 **명확한 에러**를 제공해 주세요. 3. **token이 바뀔 때마다 API 클라이언트에 업데이트** * store **subscription**으로 "token 변경 → 클라이언트 상태 업데이트”를 수행합니다. * 방법 2와 마찬가지로 암묵적 의존성이 있으나, * 방법 2는 **선언형(pull)**, * 방법 3은 **명령형(push)** 접근입니다. token을 노출한 뒤에는 `model` segment에 **비즈니스 로직**을 추가할 수 있습니다. * 만료 시간 도달 시 token 갱신 * 일정 시간이 지나면 token 자동 무효화 실제 백엔드 호출은 **User entity의 `api` segment**나 `shared/api`에서 수행하세요. ### 3‑3. Pages / Widgets — 권장하지 않음[​](#33pages--widgets권장하지-않음 "해당 헤딩으로 이동") * page, widget layer에 token을 저장하면 전역 의존성이 생기고 다른 slice에서 재사용하기 어려워집니다. * `Shared` 또는 `Entities` 중 한 곳에 token을 저장하는 것을 권장합니다. ## 4. Logout & Token Invalidation[​](#4logouttokeninvalidation "해당 헤딩으로 이동") ### 로그아웃과 token 무효화[​](#로그아웃과-token-무효화 "해당 헤딩으로 이동") 일반적으로 애플리케이션에는 `로그아웃 전용 페이지`가 없습니다.
그러나 로그아웃 기능은 매우 중요하며 다음 두 단계로 이루어집니다. 1. 백엔드에 인증된 로그아웃 request (예: `POST /logout`) 2. token store reset (access/refresh token 모두 제거) > 모든 API request을 `shared/api`에 모아 관리한다면, 로그아웃 API는 `login()` 근처 (`shared/api/endpoints/logout.ts`)에 배치합니다.
특정 UI(예: Header)에서만 호출된다면 `widgets/header/api/logout.ts` 같이 버튼 근처에 두는 것도 좋습니다. token store reset은 로그아웃 버튼을 가진 UI에서 트리거됩니다.
request와 reset를 widget의 `model` segment에 함께 둘 수도 있습니다. ### 자동 로그아웃[​](#자동-로그아웃 "해당 헤딩으로 이동") 다음 두 경우에는 반드시 token store를 초기화하세요. * 로그아웃 request 실패 * 로그인 token 갱신(`/refresh`) 실패 > token을 Entities(User)에 보관한다면 해당 entitle의 `model` segment에서 초기화 코드를 둡니다.
Shared layer라면 `shared/auth` segment로 분리하는 것도 좋습니다. --- # Autocomplete WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/170) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About decomposition by layers ## See also[​](#see-also "해당 헤딩으로 이동") * [(Discussion) About the application of the methodology for the selection with loaded dictionaries](https://github.com/feature-sliced/documentation/discussions/65#discussioncomment-480807) --- # Browser API WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/197) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About working with the Browser API: localStorage, audio Api, bluetooth API, etc. > > You can ask about the idea in more detail [@alex\_novi](https://t.me/alex_novich) --- # CMS WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/172) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Features may be different[​](#features-may-be-different "해당 헤딩으로 이동") In some projects, all the functionality is concentrated in data from the server > ## How to work more correctly with CMS markup[​](#how-to-work-more-correctly-with-cms-markup "해당 헤딩으로 이동") > > --- # Feedback WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/187) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Errors, Alerts, Notifications, ... --- # i18n WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/171) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Where to place it? How to work with this?[​](#where-to-place-it-how-to-work-with-this "해당 헤딩으로 이동") * * * --- # Metric WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/181) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About ways to initialize metrics in the application --- # Monorepositories WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/221) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About applicability for mono repositories, about bff, about microapps ## See also[​](#see-also "해당 헤딩으로 이동") * [(Discussion) About mono repositories and plug-ins-packages](https://github.com/feature-sliced/documentation/discussions/50) * [(Thread) About the application for a mono repository](https://t.me/feature_sliced/2412) --- # Page layouts 여러 페이지에서 **동일한 공통 layout(header, sidebar, footer)** 을 사용하고,
그 안의 **Content 영역**(각 페이지에서 렌더링할 컴포넌트)만 달라질 때 사용하는 *page layout* 개념을 설명합니다. info 더 궁금한 점이 있나요? 페이지 우측의 피드백 버튼을 눌러 의견을 남겨 주세요. 여러분의 제안은 문서 개선에 큰 도움이 됩니다! ## Simple layout[​](#simple-layout "해당 헤딩으로 이동") simple layout은 아래 예시에서 볼 수 있습니다.
header, 두 개의 sidebar, 외부 링크(GitHub, Twitter)가 있는 footer로 구성되며, 복잡한 비즈니스 로직은 없습니다. * **정적 요소**: 고정된 menu, logo, footer 등 * **동적 요소**: sidebar toggle, header 오른쪽의 theme switch button 이 Layout 컴포넌트는 `shared/ui` 또는 `app/layouts` 같은 common 폴더에 두고,
`siblingPages`(SiblingPageSidebar)와 `headings`(HeadingsSidebar) props로 sidebar content를 **주입(의존성 주입)** 받아 사용합니다. shared/ui/layout/Layout.tsx ``` import { Link, Outlet } from "react-router-dom"; import { useThemeSwitcher } from "./useThemeSwitcher"; export function Layout({ siblingPages, headings }) { const [theme, toggleTheme] = useThemeSwitcher(); return (
{/* 여기에 주요 콘텐츠가 들어갑니다 */}
  • GitHub
  • Twitter
); } ``` shared/ui/layout/useThemeSwitcher.ts ``` export function useThemeSwitcher() { const [theme, setTheme] = useState("light"); function toggleTheme() { setTheme(theme === "light" ? "dark" : "light"); } useEffect(() => { document.body.classList.remove("light", "dark"); document.body.classList.add(theme); }, [theme]); return [theme, toggleTheme] as const; } ``` 사이드바 구현은 생략했습니다. ## layout에 widget 적용하기[​](#layout에-widget-적용하기 "해당 헤딩으로 이동") 간혹 layout 컴포넌트에서 인증 처리나 데이터 로딩 같은 비즈니스 로직을 직접 실행해야 할 수 있습니다. 예를 들어, [React Router](https://reactrouter.com/)의 deeply nested routes를 사용할 때, child routes(예: `/users`, `/users/:id`, `/users/:id/settings` 등)의 공통 로직(인증 처리, 데이터 로딩 등)을 layout 레벨에서 한 번에 처리하면 편리합니다. 이 경우 layout 컴포넌트를 `shared`나 `widgets` 폴더에 두면 [layer에 대한 import 규칙](/documentation/kr/docs/reference/layers.md#import-rule-on-layers)을 위반합니다. > Slice의 module은 자신보다 하위 layer에만 있는 Slice를 import할 수 있습니다. 이 문제가 정말 중요한지 먼저 고려해 봐야 합니다. * *이 layout이 정말 필요한가요?* * *꼭 widget으로 구현해야 하나요?* 비즈니스 로직을 사용하는 layout이 2\~3개 페이지만 적용된다면, layout 역할이 단순 wrapper인지 확인하고, 아래 대안을 고려하세요. 1. **App layer에서 inline으로 작성하기**
Router의 nesting 기능을 이용하면, 공통된 URL 패턴을 가진 여러 경로(예: /users, /users/profile, /users/settings)를 하나의 `route group` 으로 묶을 수 있습니다. 이렇게 만든 route group에 한 번만 layout을 지정하면, 해당 그룹의 모든 페이지에 동일한 layout이 적용됩니다. 2. **코드 복사 & 붙여넣기**
레이아웃은 자주 변경되지 않으므로, 필요한 페이지만 복사해 두고 수정할 때만 업데이트하세요. 이렇게 하면 다른 페이지에 영향을 주지 않으며, 페이지 간 관계를 주석으로 남겨 누락을 방지할 수 있습니다. 위 방식이 적합하지 않다면, layout에 widget을 포함하는 다음 두 가지 해결책을 검토하세요: 1. **Render Props 또는 Slots 사용하기**
React에서는 [render props](https://www.patterns.dev/react/render-props-pattern/)를, Vue에서는 [slots](https://vuejs.org/guide/components/slots)를 사용해 `부모 layout 컴포넌트에 자식 UI를 props/slot 형태로 전달해 특정 위치에 주입(injection)` 하는 방식입니다. 2. **layout을 App layer로 이동하기**
`app/layouts` 등에 layout 파일을 두고, 필요한 widget을 조합해 사용하세요. ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") React 및 Remix(React Router와 유사)의 인증 layout 구축에 대한 예시는 [튜토리얼](/documentation/kr/docs/get-started/tutorial.md)에서 확인하실 수 있습니다. --- # Desktop/Touch platforms WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/198) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About the application of the methodology for desktop/touch --- # SSR WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/173) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > About the implementation of SSR using the methodology --- # Theme WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/207) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Where should I put my work with the theme and palette?[​](#where-should-i-put-my-work-with-the-theme-and-palette "해당 헤딩으로 이동") > ## Discussion about the location of the theme, i18n logic[​](#discussion-about-the-location-of-the-theme-i18n-logic "해당 헤딩으로 이동") > --- # Types 이 가이드는 TypeScript 같은 정적 타입 언어에서 데이터를 정의·활용하는 방법과, FSD 구조 내에서 타입을 어디에 배치할지 설명합니다. info 더 궁금한 점이 있나요? 페이지 우측의 피드백 버튼을 눌러 의견을 남겨 주세요. 여러분의 제안은 문서 개선에 큰 도움이 됩니다! ## 유틸리티 타입[​](#유틸리티-타입 "해당 헤딩으로 이동") 유틸리티 타입은 **스스로 큰 의미를 갖지는 않지만 다른 타입과 함께 자주 쓰이는 보조 타입**입니다. 예를 들어 배열 요소 타입을 추출하는 `ArrayValues`를 아래와 같이 정의할 수 있습니다. ``` type ArrayValues = T[number]; ``` Source: 프로젝트 전역에서 유틸리티 타입을 사용하려면 두 가지 방법이 있습니다. 1. **외부 라이브러리 설치**
대표적으로 [`type-fest`](https://github.com/sindresorhus/type-fest)를 설치합니다. 2. **내부 라이브러리 구축**
`shared/lib/utility-types` 폴더를 만들고 README에 “우리 팀에서 유틸리티 타입이라 부르는 기준”과 “추가·제외 규칙”을 명확히 적어 둡니다. > 유틸리티 타입의 **재사용 가능성**을 지나친 기대를 하지 마세요.
재사용 가능하다고 해서 반드시 전역에 둘 필요는 없습니다. 아래처럼 **사용 위치 근처**에 두는 편이 유지보수에 유리할 때가 많습니다. * 📂 pages * 📂 home * 📂 api * 📄 ArrayValues.ts (유틸리티 타입) * 📄 getMemoryUsageMetrics.ts (유틸리티 타입을 사용하는 코드) warning `shared/types` 폴더를 만들거나 각 slice에 `types` segment를 추가하고 싶을 수 있습니다.
그러나 **types 는 코드의 목적을 설명하지 못하는 분류**입니다.
segment와 폴더는 무엇을 담는지가 아니라 왜 존재하는지를 드러내야 합니다. ## 비즈니스 entity와 상호 참조[​](#비즈니스entity와-상호-참조 "해당 헤딩으로 이동") 앱에서 가장 핵심이 되는 타입은 **비즈니스 entity**—즉, 도메인 객체—입니다.
음악 스트리밍 서비스를 예로 들면 *Song*, *Album* 등이 entity입니다. ### 1. 백엔드 Response 타입[​](#1-백엔드-response-타입 "해당 헤딩으로 이동") 백엔드에서 내려오는 데이터를 먼저 타입으로 정의합니다.
추가적인 타입 안전성을 위해 [Zod](https://zod.dev) 같은 schema 기반 유효성 검사을 적용할 수도 있습니다. shared/api/songs.ts ``` import type { Artist } from "./artists"; interface Song { id: number; title: string; artists: Array; } export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise>); } ``` `Song` 타입은 다른 entity인 `Artist`를 참조합니다.
**Request·Response 코드를 Shared에 두면** 이런 상호 참조를 한곳에서 관리할 수 있어 유지보수가 간편해집니다. 반대로 이 함수를 `entities/song/api` 내부에 두면 다음과 같은 문제가 생깁니다. * `entities/artist` slice가 `Song`을 **가져오고 싶어도**
FSD의 [layer별 import 규칙](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) 때문에 **동일 layer 간(import)** 의존은 금지됩니다. * 규칙 요약 > *“한 slice의 모듈은 자신보다 **아래 layer**에 있는 slice만 import할 수 있다.”* 즉, 동일 layer 간 cross-import가 막혀 있어 **Artist → Song** 의존을 직접 연결하기 어렵습니다.
이럴 땐 제네릭 파라미터화 또는 `@x` Public API 같은 방법을 선택해야 합니다. ### 2. 상호 참조 해결 전략[​](#2-상호-참조-해결-전략 "해당 헤딩으로 이동") 1. **제네릭 타입 매개변수화**
entity 간 연결이 필요한 타입에는 제네릭 타입 매개변수를 선언하고, 필요한 제약 조건을 부여합니다. 예를 들어, Song 타입에 ArtistType이라는 제약 조건을 설정할 수 있습니다. entities/song/model/song.ts ``` interface Song { id: number; title: string; artists: Array; } ``` 제네릭 방식은 `Cart = { items: Product[] }`처럼 단순한 타입과 잘 어울립니다. 반면 `Country‑City`처럼 긴밀히 결합된 구조는 분리하기 어렵습니다. 2. **Cross-import (Public API(@x) 활용)**
FSD에서 entity 간 의존을 허용하려면, 참조 대상 entity 내부에 상대 entity 전용 Public API를 `@x` 디렉터리에 둡니다. 예를 들어 `artist`와 `playlist`가 `song`을 참조해야 한다면 다음과 같이 구성합니다. * 📂 entities * 📂 song * 📂 @x * 📄 artist.ts (artist entity용 public API) * 📄 playlist.ts (playlist entity용 public API) * 📄 index.ts (기본 public API) 파일 `📄 entities/song/@x/artist.ts`의 내용은 `📄 entities/song/index.ts`와 유사합니다: entities/song/@x/artist.ts ``` export type { Song } from "../model/song.ts"; ``` 이제 `📄 entities/artist/model/artist.ts`에서는 다음과 같이 `Song`을 가져옵니다. entities/artist/model/artist.ts ``` import type { Song } from "entities/song/@x/artist"; export interface Artist { name: string; songs: Array; } ``` 이렇게 명시적으로 연결하면 각 entity의 의존 관계를 쉽게 파악하고, 도메인 분리를 유지할 수 있습니다. ## 데이터 전송 객체와 mappers[​](#data-transfer-objects-and-mappers "해당 헤딩으로 이동") 데이터 전송 객체(Data Transfer Object, DTO)는 백엔드에서 전달되는 데이터 구조를 의미합니다. DTO를 그대로 써도 될 때가 있지만, 프론트엔드에서 쓰기엔 다소 불편합니다. 이때 `mapper`를 사용해 DTO를 더 다루기 쉬운 형태로 변환합니다. ### DTO 배치 위치[​](#dto배치-위치 "해당 헤딩으로 이동") * 백엔드 타입을 별도 패키지로 공유하는 경우 → 해당 패키지에서 DTO를 가져오면 끝입니다. * 코드 공유가 없는 경우 → 프론트엔드 코드베이스 어딘가에 DTO를 넣어야 합니다. Request 함수가 `shared/api`에 있다면 DTO도 바로 옆에 두는 편이 좋습니다. shared/api/songs.ts ``` import type { ArtistDTO } from "./artists"; interface SongDTO { id: number; title: string; artist_ids: Array; } export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise>); } ``` ### mapper 배치 위치[​](#mapper배치-위치 "해당 헤딩으로 이동") mapper는 DTO를 인자로 받아 변환하므로, DTO 정의와 최대한 가까이 둡니다. `shared/api`에 Request와 DTO가 있다면 mapper도 그곳에 둡니다. shared/api/songs.ts ``` import type { ArtistDTO } from "./artists"; interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array; } interface Song { id: string; title: string; /** 디스크 번호까지 포함한 전체 제목 */ fullTitle: string; artistIds: Array; } function adaptSongDTO(dto: SongDTO): Song { return { id: String(dto.id), title: dto.title, fullTitle: `${dto.disc_no} / ${dto.title}`, artistIds: dto.artist_ids.map(String), }; } export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO)); } ``` Request·Store가 `entity slice` 내부에 있다면 mapper도 해당 slice에 배치합니다(cross-import 제한 주의). entities/song/api/dto.ts ``` import type { ArtistDTO } from "entities/artist/@x/song"; export interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array; } ``` entities/song/api/mapper.ts ``` import type { SongDTO } from "./dto"; export interface Song { id: string; title: string; /** 노래의 전체 제목, 디스크 번호까지 포함된 제목입니다. */ fullTitle: string; artistIds: Array; } export function adaptSongDTO(dto: SongDTO): Song { return { id: String(dto.id), title: dto.title, fullTitle: `${dto.disc_no} / ${dto.title}`, artistIds: dto.artist_ids.map(String), }; } ``` entities/song/api/listSongs.ts ``` import { adaptSongDTO } from "./mapper"; export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO)); } ``` entities/song/model/songs.ts ``` import { createSlice, createEntityAdapter } from "@reduxjs/toolkit"; import { listSongs } from "../api/listSongs"; export const fetchSongs = createAsyncThunk('songs/fetchSongs', listSongs); const songAdapter = createEntityAdapter(); const songsSlice = createSlice({ name: "songs", initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSongs.fulfilled, (state, action) => { songAdapter.upsertMany(state, action.payload); }) }, }); ``` ### 중첩 DTO 처리[​](#중첩dto처리 "해당 헤딩으로 이동") 백엔드 Response 에 여러 entity가 포함되면 서로를 알지 않을 수 없습니다. 예를 들어 곡 정보에 저자 객체 전체가 포함될 수 있습니다. 이런 경우, 간접 연결(middleware 등) 대신 `@x` 표기법을 활용한 명시적 cross‑import가 낫습니다. 아래는 Redux Toolkit + Normalizr 예시입니다. entities/song/model/songs.ts ``` import { createSlice, createEntityAdapter, createAsyncThunk, createSelector, } from '@reduxjs/toolkit' import { normalize, schema } from 'normalizr' import { getSong } from "../api/getSong"; // Normalizr entity schema export const artistEntity = new schema.Entity('artists') export const songEntity = new schema.Entity('songs', { artists: [artistEntity], }) const songAdapter = createEntityAdapter() export const fetchSong = createAsyncThunk( 'songs/fetchSong', async (id: string) => { const data = await getSong(id) // 데이터를 정규화하여 리듀서가 예측 가능한 payload를 로드할 수 있도록 합니다: const normalized = normalize(data, songEntity) // `action.payload = { songs: {}, artists: {} }` return normalized.entities } ) export const slice = createSlice({ name: 'songs', initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSong.fulfilled, (state, action) => { songAdapter.upsertMany(state, action.payload.songs) }) }, }) const reducer = slice.reducer export default reducer ``` entities/song/@x/artist.ts ``` export { fetchSong } from "../model/songs"; ``` entities/artist/model/artists.ts ``` import { createSlice, createEntityAdapter } from '@reduxjs/toolkit' import { fetchSong } from 'entities/song/@x/artist' const artistAdapter = createEntityAdapter() export const slice = createSlice({ name: 'users', initialState: artistAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSong.fulfilled, (state, action) => { // 같은 fetch 결과를 처리하며, 여기서 artists를 삽입합니다. artistAdapter.upsertMany(state, action.payload.artists) }) }, }) const reducer = slice.reducer export default reducer ``` 이 방법을 사용하면 slice 간 완전한 독립성은 조금 줄어들지만, 어차피 분리하기 힘든 두 entity 의 의존 관계를 코드에 명확하게 드러낼 수 있습니다. 따라서 나중에 둘 중 하나를 수정할 때는, 연결된 엔티티까지 함께 리팩토링하는 것이 안전합니다. ## Global 타입과 Redux[​](#global-타입과-redux "해당 헤딩으로 이동") Global 타입은 애플리케이션 전반에서 사용되는 타입을 의미하며, 크게 두 가지로 나눌 수 있습니다:
1. 애플리케이션 특성이 없는 제너릭 타입 2. 애플리케이션 전체에 알고 있어야 하는 타입 ### 1) 제너릭 타입[​](#1-제너릭-타입 "해당 헤딩으로 이동") 첫 번째 경우에는 관련 타입을 Shared 폴더 안에 적절한 segment로 배치하면 됩니다. 예를 들어, 분석 전역 변수를 위한 Interface가 있다면 `shared/analytics`에 두는 것이 좋습니다. warning `shared/types` 폴더는 만들지 않는 편이 좋습니다. “타입이기 때문”이라는 이유만으로 무관한 항목을 묶으면 코드 검색이 어려워집니다. ### 2) 애플리케이션 Global 타입[​](#2-애플리케이션-global-타입 "해당 헤딩으로 이동") `Redux(순수 Redux + RTK 미사용)` 프로젝트에서 자주 나타납니다. 모든 reducer를 합쳐야 store 타입이 완성되지만, 이 타입은 전역에서 selector에 필요합니다. app/store/index.ts ``` import { combineReducers, createStore } from "redux"; import { songReducer } from "entities/song"; import { artistReducer } from "entities/artist"; const rootReducer = combineReducers(songReducer, artistReducer); const store = createStore(rootReducer); type RootState = ReturnType; type AppDispatch = typeof store.dispatch; ``` `shared/store`에서 `useAppDispatch`, `useAppSelector` 훅을 만들고 싶어도, [import 규칙](/documentation/kr/docs/reference/layers.md#import-rule-on-layers) 때문에 App layer의 `RootState·AppDispatch`를 가져올 수 없습니다. > 한 slice의 module은 자신보다 하위 layer에 있는 slice만 import할 수 있습니다. #### 권장 해결책[​](#권장-해결책 "해당 헤딩으로 이동") Shared ↔ App layer 간에 암묵적 의존성을 허용합니다. 두 타입은 변동 가능성이 작고 Redux 개발자에게 익숙하므로 부담이 적습니다. app/store/index.ts ``` /* 이전 코드 블록과 동일한 내용입니다… */ declare type RootState = ReturnType; declare type AppDispatch = typeof store.dispatch; ``` shared/store/index.ts ``` import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux"; export const useAppDispatch = useDispatch.withTypes() export const useAppSelector: TypedUseSelectorHook = useSelector; ``` ## 열거형(enum)[​](#열거형enum "해당 헤딩으로 이동") * `가장 가까운 사용 위치`에 정의합니다. * `segment`는 사용 위치로 결정합니다. * UI Toast 위치 → `ui` segment * 백엔드 Response 상태 → `api` segment 프로젝트 전역에서 공용으로 쓰이는 값(예: Response 상태, 디자인 토큰)은 `Shared`에 두고, 의미에 맞는 segment(`api`, `ui` 등)를 선택합니다. ## 타입 검증 Schema와 Zod[​](#타입-검증-schema와-zod "해당 헤딩으로 이동") 데이터 형태·제약을 검증하려면 [Zod](https://zod.dev) 같은 라이브러리로 검증 스키마를 정의합니다. 가능하면 사용 코드와 같은 위치에 둡니다. * 백엔드 Response 검증 → api segment 옆 * 폼 입력 검증 → ui segment(또는 복잡할 경우 model) 검증 schema는 DTO를 파싱하고, schema에 맞지 않으면 즉시 오류를 던집니다.
([Data transfer objects and mappers](#data-transfer-objects-and-mappers) 섹션도 참고하세요.)
특히 백엔드 Response이 schema와 일치하지 않을 때 Request을 실패시키면, 조기에 버그를 발견할 수 있으므로 schema를 `api` segment에 두는 편이 일반적입니다. ## Component props, context 타입[​](#componentprops-context타입 "해당 헤딩으로 이동") 일반적으로 `Component·Context 파일과 같은 파일`에 둡니다. 단일 파일(Vue·Svelte 등)에서 여러 Component가 Interface를 공유해야 한다면, 같은 폴더(보통 `ui` segment)에 별도 파일을 만듭니다. pages/home/ui/RecentActions.tsx ``` interface RecentActionsProps { actions: Array<{ id: string; text: string }>; } export function RecentActions({ actions }: RecentActionsProps) { /* … */ } ``` Vue에서 Interface를 별도 파일에 저장한 예는 다음과 같습니다: pages/home/ui/RecentActionsProps.ts ``` export interface RecentActionsProps { actions: Array<{ id: string; text: string }>; } ``` pages/home/ui/RecentActions.vue ``` ``` ## Ambient 선언 파일(\*.d.ts)[​](#ambient-선언-파일dts "해당 헤딩으로 이동") [Vite](https://vitejs.dev)나 [ts-reset](https://www.totaltypescript.com/ts-reset) 같은 일부 패키지는 전역 Ambient 선언이 필요합니다. * **단순**하면 `src/`에 두어도 무방 * 구조를 **명확히** 하려면 `app/ambient/`에 배치 타입이 없는 패키지는 `shared/lib/untyped-packages/%LIB%.d.ts`에 직접 선언합니다. ### 타입이 없는 외부 패키지[​](#타입이-없는-외부-패키지 "해당 헤딩으로 이동") 타입 정의가 없는 패키지는 **미타입(declare module)으로 선언**하거나 **직접 타입**을 작성해야 합니다.
권장 위치는 `shared/lib/untyped-packages`. 이 폴더에 **`%LIBRARY_NAME%.d.ts`** 파일을 만들고 필요한 타입을 선언합니다. shared/lib/untyped-packages/use-react-screenshot.d.ts ``` // 공식 타입 정의가 없는 라이브러리 예시 declare module "use-react-screenshot"; ``` ## 타입 자동 생성[​](#타입-자동-생성 "해당 헤딩으로 이동") 외부 schema(OpenAPI 등)로부터 타입을 생성하는 경우, 전용 디렉터리를 둡니다.
예: `shared/api/openapi` — README에 `파일 용도·재생성 방법`을 기록하면 좋습니다. --- # White Labels WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/215) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Figma, brand uikit, templates, adaptability to brands ## See also[​](#see-also "해당 헤딩으로 이동") * [(Thread) About the application for white-labels (branded) projects](https://t.me/feature_sliced/1543) * [(Presentation) About white-labels apps and design](http://yadi.sk/i/5IdhzsWrpO3v4Q) --- # Cross-import WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/220) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Cross-import는 Layer나 추상화가 원래의 책임 범위를 넘어설 때 발생합니다. 방법론에서는 이러한 Cross-import를 해결하기 위한 별도의 Layer를 정의합니다. ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [(스레드) Cross-import가 불가피한 상황 논의](https://t.me/feature_sliced/4515) * [(스레드) Entity에서 Cross-import 해결 방법](https://t.me/feature_sliced/3678) * [(스레드) Cross-import와 책임 범위 관계](https://t.me/feature_sliced/3287) * [(스레드) Segment 간 import 이슈 해결](https://t.me/feature_sliced/4021) * [(스레드) Shared 내부 구조의 Cross-import 해결](https://t.me/feature_sliced/3618) --- # Desegmentation WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/148) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## 상황[​](#상황 "해당 헤딩으로 이동") 프로젝트에서 동일한 도메인의 모듈들이 서로 연관되어 있음에도 불구하고, 프로젝트 전체에 불필요하게 분산되어 있는 경우가 많습니다. ``` ├── components/ | ├── DeliveryCard | ├── DeliveryChoice | ├── RegionSelect | ├── UserAvatar ├── actions/ | ├── delivery.js | ├── region.js | ├── user.js ├── epics/ | ├── delivery.js | ├── region.js | ├── user.js ├── constants/ | ├── delivery.js | ├── region.js | ├── user.js ├── helpers/ | ├── delivery.js | ├── region.js | ├── user.js ├── entities/ | ├── delivery/ | | ├── getters.js | | ├── selectors.js | ├── region/ | ├── user/ ``` ## 문제점[​](#문제점 "해당 헤딩으로 이동") 이는 높은 응집도 원칙을 위반하며, **Changes Axis의 과도한 확장**을 초래합니다. ## 무시했을 때의 결과[​](#무시했을-때의-결과 "해당 헤딩으로 이동") * delivery 관련 로직 수정 시 여러 위치의 코드를 찾아 수정해야 하며, 이는 **Changes Axis를 불필요하게 확장**합니다 * user 관련 로직을 이해하려면 프로젝트 전반의 **actions, epics, constants, entities, components**를 모두 찾아봐야 합니다 * 암묵적 연결로 인해 도메인 영역이 비대해지고 관리가 어려워집니다 * 불필요한 파일들이 쌓여 문제 인식이 어려워집니다 ## 해결 방안[​](#해결-방안 "해당 헤딩으로 이동") 도메인이나 use case와 관련된 모듈들을 한 곳에 모아 배치합니다. 이를 통해 모듈 학습이나 수정 시 필요한 모든 요소를 쉽게 찾을 수 있습니다. > 이 접근은 코드베이스의 탐색성과 가독성을 높이고, 모듈 간 관계를 더 명확하게 보여줍니다. ``` - ├── components/ - | ├── DeliveryCard - | ├── DeliveryChoice - | ├── RegionSelect - | ├── UserAvatar - ├── actions/ - | ├── delivery.js - | ├── region.js - | ├── user.js - ├── epics/{...} - ├── constants/{...} - ├── helpers/{...} ├── entities/ | ├── delivery/ + | | ├── ui/ # ~ components/ + | | | ├── card.js + | | | ├── choice.js + | | ├── model/ + | | | ├── actions.js + | | | ├── constants.js + | | | ├── epics.js + | | | ├── getters.js + | | | ├── selectors.js + | | ├── lib/ # ~ helpers | ├── region/ | ├── user/ ``` ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [(아티클) Coupling과 Cohesion의 명확한 이해](https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/) * [(아티클) Coupling, Cohesion과 Law of Demeter](https://medium.com/german-gorelkin/low-coupling-high-cohesion-d36369fb1be9) --- # Routing WIP 작성 진행 중 행을 앞당기고 싶다면 다음을 도와주세요: * 📢 의견 공유 [글에 댓글·이모지 달기](https://github.com/feature-sliced/documentation/issues/169) * 💬 자료 모으기 [채팅방에 관련 자료 남기기](https://t.me/feature_sliced) * ⚒️ 기여하기 [다른 방식으로 기여](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## 상황[​](#상황 "해당 헤딩으로 이동") Page의 URL이 하위 Layer에 하드코딩되어 있는 경우가 있습니다. entities/post/card ``` ... ``` ## 문제점[​](#문제점 "해당 헤딩으로 이동") URL이 Page Layer에 집중되지 않고, 하위 Layer에 분산되어 관리됩니다. ## 무시했을 때의 결과[​](#무시했을-때의-결과 "해당 헤딩으로 이동") URL 변경 시 Page Layer 외의 여러 하위 Layer에 있는 URL과 redirect 로직을 모두 고려해야 합니다. 결과적으로 단순한 Product Card 같은 Component도 Page의 책임을 가지게 되어, 프로젝트 구조가 불필요하게 복잡해집니다. ## 해결 방안[​](#해결-방안 "해당 헤딩으로 이동") URL과 redirect 로직은 Page Layer와 그 상위 Layer에서만 다루도록 합니다. 이를 위해 composition, props 전달, Factory 패턴 등을 활용해 URL 정보를 하위 Layer에 전달합니다. ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [(스레드) Entity/Feature/Widget에서 Routing 처리의 영향](https://t.me/feature_sliced/4389) * [(스레드) Page에서만 Route 로직을 다뤄야 하는 이유](https://t.me/feature_sliced/3756) --- # 기존 아키텍처에서 FSD로의 마이그레이션 이 가이드는 기존 아키텍처를 **Feature-Sliced Design(FSD)** 으로 단계별 전환하는 방법을 설명합니다. 아래 폴더 구조를 예시로 살펴보세요. (파란 화살표를 클릭하면 펼쳐집니다). 📁 src * 📁 actions * 📁 product * 📁 order * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 routes * 📁 products.jsx * 📄 products.\[id].jsx * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles * 📄 App.jsx * 📄 index.js ## 시작 전 체크리스트[​](#before-you-start "해당 헤딩으로 이동") Feature-Sliced Design(FSD)이 **정말 필요한지 먼저 확인하세요.**
모든 프로젝트가 새로운 아키텍처를 요구하는 것은 아닙니다. ### 전환을 고려해야 할 징후[​](#전환을-고려해야-할-징후 "해당 헤딩으로 이동") 1. 신규 팀원이 프로젝트에 적응하기 어려워하는 경우 2. 코드 일부를 수정할 때, 관련 없는 다른 코드에 오류가 발생하는 경우가 **잦은** 경우 3. 새 기능을 추가할 때 고려해야 할 사항이 너무 많아 어려움을 겪는 경우 **팀의 합의 없이 FSD 전환을 시작하지 마세요.**
팀 리더라도 전환의 이점이 학습·전환 비용을 상회한다는 점을 먼저 설득해야 합니다.
또한, 개선 효과가 바로 눈에 띄지 않을 수 있으므로 **팀원** 및 **프로젝트 매니저(PM)** 의 승인을 사전에 확보하고 이점을 공유하세요. PM 설득 시 고려할 사항 * FSD 전환은 단계적으로 진행할 수 있어 기존 기능 개발을 중단하지 않아도 됩니다. * 명확한 아키텍처 구조는 신규 개발자 온보딩 시간을 단축합니다. * 공식 문서를 활용하면 별도 문서 유지·관리 비용을 절감할 수 있습니다. *** 마이그레이션을 시작하기로 결정했다면, `📁 src` 폴더에 별칭(alias)을 설정하는 것을 첫 단계로 삼으세요.
## 1단계: 페이지 단위로 코드 분리하기[​](#divide-code-by-pages "해당 헤딩으로 이동") 대부분의 커스텀 아키텍처는 규모와 관계없이 이미 어느 정도 페이지 단위로 코드를 나누고 있습니다. `📁 pages` 폴더가 있다면 이 단계를 건너뛰어도 됩니다. 위에 예시 폴더처럼 `📁 routes`만 있다면 다음 순서를 따르세요. 1. `📁 pages` 폴더를 새로 만듭니다. 2. `📁 routes`에 있던 **페이지용 컴포넌트**를 가능한 한 모두 `📁 pages` 폴더로 옮깁니다. 3. 코드를 옮길 때마다 해당 페이지 전용 폴더를 만들고 그 안에 `index` 파일을 추가해 entry를 노출합니다. note 이 단계에서는 **Page A에서 Page B의 코드를 import**해도 괜찮습니다. 나중 단계에서 이러한 의존성을 분리할 예정이니, 우선 **페이지 폴더를 만드는 것**에 집중하세요. route file: src/routes/products.\[id].js ``` export { ProductPage as default } from "src/pages/product" ``` page index file: src/pages/product/index.js ``` export { ProductPage } from "./ProductPage.jsx" ``` page component file: src/pages/product/ProductPage.jsx ``` export function ProductPage(props) { return
; } ``` ## 2단계: 페이지 외부 코드를 분리하기[​](#separate-everything-else-from-pages "해당 헤딩으로 이동") 1. **`📁 src/shared` 폴더를 만든다.** * `📁 pages` 또는 `📁 routes`를 **import하지 않는** 모든 코드를 이곳으로 이동한다. 2. **`📁 src/app` 폴더를 만든다.** * `📁 pages` 또는 `📁 routes`를 **import하는** 코드를 이곳으로 옮긴다. 라우트 파일도 여기에 포함한다. > **Shared layer에는 slice가 없다.**
따라서 segment 간 import는 자유롭다. 이제 폴더 구조는 다음과 같아야 합니다: 📁 src * 📁 app * 📁 routes * 📄 products.jsx * 📄 products.\[id].jsx * 📄 App.jsx * 📄 index.js * 📁 pages * 📁 product * 📁 ui * 📄 ProductPage.jsx * 📄 index.js * 📁 catalog * 📁 shared * 📁 actions * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles ## 3단계: 페이지 간의 cross-imports 해결[​](#tackle-cross-imports-between-pages "해당 헤딩으로 이동") 한 페이지가 다른 페이지의 코드를 가져오고 있다면 두 가지 방법으로 의존성을 제거한다. | 방법 | 사용 시점 | | -------------------- | ------------------------------------------------------------ | | **A. 코드 복사** | 페이지마다 로직이 달라질 가능성이 있거나, 재사용성이 낮을 때 | | **B. Shared로 이동** | 여러 페이지에서 공통으로 쓰일 때 | * Shared 이동 위치 예시 * UI 구성 요소 → `📁 shared/ui` * 설정 상수   → `📁 shared/config` * 백엔드 호출  → `📁 shared/api` note 코드 복사는 잘못이 아니다. **중복보다 의존성 최소화**가 더 중요할 때가 많다.
다만 비즈니스 로직은 중복을 피해야 하며, 복사 시에도 DRY 원칙을 염두에 둔다. ## 4단계: Shared 레이어 정리하기[​](#unpack-shared-layer "해당 헤딩으로 이동") * **한 페이지에서만 쓰이는 코드**는 해당 페이지 **slice**로 이동한다. * `actions / reducers / selectors`도 예외가 아니다. **사용처와 가까이** 두는 편이 좋다. Shared는 모든 layer가 의존할 수 있는 **공통 의존점**이므로, 코드를 최소화해 변경 위험을 낮춘다. 최종 폴더 구조는 다음과 같아야 합니다: 📁 src * 📁 app (unchanged) * 📁 pages * 📁 product * 📁 actions * 📁 reducers * 📁 selectors * 📁 ui * 📄 Component.jsx * 📄 Container.jsx * 📄 ProductPage.jsx * 📄 index.js * 📁 catalog * 📁 shared (only objects that are reused) * 📁 actions * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles ## 5단계: 기술적 목적별 segment 정리[​](#organize-by-technical-purpose "해당 헤딩으로 이동") | segment | 용도 예시 | | -------- | ---------------------------------- | | `ui` | Components, formatters, styles | | `api` | Backend requests, DTOs, mappers | | `model` | Store, schema, business logic | | `lib` | Shared utilities / helpers | | `config` | Configuration files, feature flags | > “**무엇인지**”가 아니라 “**무엇을 위해**” 존재하는지를 기준으로 나눈다.
따라서 `components`, `utils`, `types` 같은 이름은 지양한다. 1. **각 페이지**에 `ui / model / api` 등 필요한 segment를 만든다. 2. **Shared** 폴더를 정리한다. * `components·containers` → `shared/ui` * `helpers·utils` → `shared/lib` (기능별 그룹화 후) * `constants` → `shared/config` ## 선택 단계[​](#optional-steps "해당 헤딩으로 이동") ### 6단계: 여러 페이지에서 재사용되는 Redux slice를 Entities / Features layer로 분리하기[​](#form-entities-features-from-redux "해당 헤딩으로 이동") * 여러 페이지에서 재사용되는 Redux **slice**는 주로 **product, user** 같은 **business entity**를 표현합니다.
이 경우 **Entities layer**로 옮기고, **entity**마다 폴더를 하나씩 만듭니다. * 댓글 작성처럼 **사용자 행동(action)** 을 다루는 **slice**는 **Features layer**로 이동합니다. **Entities**와 **Features**는 서로 독립적으로 사용될 수 있도록 설계되어 있습니다.
Entitles 간 연결이 필요하면 [Business-Entities Cross-Relations 가이드](/documentation/kr/docs/guides/examples/types.md#business-entities-and-their-cross-references)를 참고하세요.
해당 **slice**와 연관된 API 함수는 `📁 shared/api`에 그대로 두어도 무방합니다. ### 7단계: modules 폴더 리팩터링[​](#refactor-your-modules "해당 헤딩으로 이동") `📁 modules`는 과거에 비즈니스 로직을 모아 두던 곳으로, 성격상 **Features layer**와 비슷합니다.
단, 앱 Header처럼 **large UI block**(예: global Header, Sidebar)이라면 **Widgets layer**로 옮기는 편이 좋습니다. ### 8단계: shared/ui에 presentational UI 기반 마련하기[​](#form-clean-ui-foundation "해당 헤딩으로 이동") `📁 shared/ui`에는 비즈니스 로직이 전혀 없는, 재사용 가능한 presentational UI 컴포넌트만 남겨야 합니다. * 기존 `📁 components` · `📁 containers`에 있던 컴포넌트에서 비즈니스 로직을 분리해 상위 layer로 이동합니다. * 여러 곳에서 쓰이지 않는 부분은 **복사(paste)** 해서 각 layer에서 독립적으로 관리해도 괜찮습니다. ## 참고 자료[​](#see-also "해당 헤딩으로 이동") * [(러시아어 영상) Ilya Klimov — "끝없는 리팩터링의 악순환에서 벗어나기: 기술 부채가 동기와 제품에 미치는 영향](https://youtu.be/aOiJ3k2UvO4) --- # v1 -> v2 마이그레이션 가이드 ## v2 도입 배경[​](#v2-도입-배경 "해당 헤딩으로 이동") **feature-slices** 개념은 2018년 [첫 발표](https://t.me/feature_slices)된 이후, 다양한 프로젝트 경험과 커뮤니티 피드백을 거치며 발전해 왔습니다.
동시에 **[기본 원칙](https://feature-sliced.github.io/featureslices.dev/v1.0.html)**(표준화된 프로젝트 구조, 비즈니스 로직 우선 분리, isolated features, Public API)은 그대로 유지되었습니다. 하지만 v1에는 다음과 같은 한계가 있었습니다: * 과도한 **boilerplate** 발생 * 추상화 규칙이 모호해 **코드베이스 복잡도** 상승 * 암묵적 설계로 **확장·온보딩 어려움** 이러한 한계를 해결하기 위해 ([`v2`](https://github.com/feature-sliced/documentation))는 기존 장점을 유지하면서도 위 과제들을 보완하도록 설계되었습니다.
또한 [Oleg Isonen](https://github.com/kof)이 발표한 [**feature-driven**](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) 등 유사 방법론과 아이디어를 융합해 애플리케이션 구조를 한층 더 **유연**, **명확**, **효율적**으로 다듬었습니다. > 이 과정에서 방법론의 공식 명칭은 *feature-slice*에서 **feature-sliced**로 정식화되었습니다. ## v2 마이그레이션 이유[​](#v2-마이그레이션-이유 "해당 헤딩으로 이동") > `WIP:` 작업이 진행 중이며, 일부 세부 사항이 변경될 수 있습니다. ### 직관적 구조 제공[​](#직관적-구조-제공 "해당 헤딩으로 이동") v2는 **layer → slice → segment** 3단계만 알면 대부분 구조 결정을 내릴 수 있습니다.
덕분에 새로운 팀원이 **어디에 무엇을 둬야 하나** 부터 고민하지 않아 온보딩 속도가 빨라집니다. ### 유연한 모듈화[​](#유연한-모듈화 "해당 헤딩으로 이동") * **독립 영역**은 slice 단위로, **전역 흐름**은 Processes layer로 분리해 확장성을 확보합니다. * 새 module을 추가할 때 *(layer → slice → segment)* 규칙만 따르면 폴더 재배치와 리팩터링 작업 부담이 크게 줄어듭니다. #### 커뮤니티·도구 지원 확대[​](#커뮤니티도구-지원-확대 "해당 헤딩으로 이동") v2 개발은 **코어 팀**과 커뮤니티 기여자들이 함께 이끌고 있습니다. 다음 리소스를 활용해 보세요: * **실제 사례 공유**: 다양한 프로젝트 환경에서의 적용 사례 * **단계별 가이드**: 설정·구성·운영 전 과정을 담은 튜토리얼 * **코드 템플릿 & 예제**: 시작부터 배포까지 참고할 수 있는 실전 코드 * **온보딩 문서**: 신규 개발자를 위한 개념 요약 및 학습 자료 * **검증 툴킷**: steiger CLI 등 정책 준수·lint를 지원하는 유틸리티 > v1 지원은 계속 유지되지만, 새로운 기능·개선 사항은 **v2**에 우선 반영됩니다.
주요 업데이트 시에도 **안정적 마이그레이션 경로**를 보장합니다. ## 주요 변경 사항[​](#주요-변경-사항 "해당 헤딩으로 이동") ### Layer 구조 명확화[​](#layer-구조-명확화 "해당 헤딩으로 이동") v2에서는 layer를 최상위부터 최하위까지 명시적으로 구분합니다: * `/app` > `/processes` > **`/pages`** > **`/features`** > `/entities` > `/shared` * 모든 모듈이 `pages`/`features` layer에만 속하지 않습니다. * 이 구조를 통해 [layer별 의존 규칙](https://t.me/atomicdesign/18708)을 명시적으로 설정할 수 있습니다. * **상위 layer**는 더 넓은 **Context**를 제공합니다. * 상위 layer 모듈은 **하위 layer** 모듈만 import할 수 있습니다. * **하위 layer**는 **변경 리스크(Risk)와 책임(Responsibility)** 이 더 큽니다. * 재사용 빈도가 높아, 수정 시 영향 범위가 넓습니다. ### Shared 통합[​](#shared-통합 "해당 헤딩으로 이동") 프로젝트 `src` 루트에 흩어져 있던 UI, lib, API 인프라 추상화를 `/src/shared` 폴더로 통합했습니다. * `shared/ui` - 공통 UI components(선택 사항) * *기존 `Atomic Design` 사용도 가능합니다.* * `shared/lib` - 재사용 가능한 helper libraries * *무분별한 helper dump 지양* * `shared/api` - API entry points * *각 feature/page 내 local 정의 가능하지만, 전역 entry point 집중을 권장* * `shared` 폴더에는 **business logic** 의존을 두지 않습니다 * *불가피할 경우 `entities` layer 이상으로 로직을 옮기세요.* ### Entities / Processes Layer 추가[​](#entities--processes-layer-추가 "해당 헤딩으로 이동") v2에서는 로직 복잡성과 높은 결합을 줄이기 위한 **새로운 추상화**가 추가되었습니다. * **`/entities`**
프론트엔드에서 사용되는 **business entities**(예: `user`, `order`, `i18n`, `blog`)를 담당하는 layer입니다. * **`/processes`**
애플리케이션 전반에 걸친 **비즈니스 process**(예: `payment`, `auth`, `quick-tour`)를 캡슐화하는 선택적 layer입니다.
process *로직이 여러 페이지에 분산될 때* 도입을 권장합니다. ### 추상화·네이밍 가이드[​](#추상화네이밍-가이드 "해당 헤딩으로 이동") 아래에서는 v2 권장 layer·segment 명칭을 이전 명칭과 대응하여 정리했습니다.
추상화·네이밍 관련 상세 가이드는 [명확한 네이밍 권장사항](/documentation/kr/docs/about/understanding/naming.md)을 참고하세요. #### Layer[​](#layer "해당 헤딩으로 이동") * `/app` — **Application init** * *이전 명칭: `app`, `core`,`init`, `src/index` (가끔 사용됨)* * `/processes` — [**Business process**](https://github.com/feature-sliced/documentation/discussions/20) * *이전 명칭: `processes`, `flows`, `workflows`* * `/pages` — **Application page** * *이전 명칭: `pages`, `screens`, `views`, `layouts`, `components`, `containers`* * `/features` — [**Feature module**](https://github.com/feature-sliced/documentation/discussions/23) * *이전 명칭: `features`, `components`, `containers`* * `/entities` — [**Business entity**](https://github.com/feature-sliced/documentation/discussions/18#discussioncomment-422649) * *이전 명칭: `entities`, `models`, `shared`* * `/shared` — [**Infrastructure**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453020) 🔥 * *이전 명칭: `shared`, `common`, `lib`* #### Segment[​](#segment "해당 헤딩으로 이동") * `/ui` — [**UI segment**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453132) 🔥 * *이전 명칭: `ui`, `components`, `view`* * `/model` — [**비즈니스 로직 segment**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-472645) 🔥 * *이전 명칭: `model`, `store`, `state`, `services`, `controller`* * `/lib` — **보조 코드 segment** * *이전 명칭: `lib`, `libs`, `utils`, `helpers`* * `/api` — [**API segment**](https://github.com/feature-sliced/documentation/discussions/66) * *이전 명칭: `api`, `service`, `requests`, `queries`* * `/config` — **애플리케이션 설정 segment** * *이전 명칭: `config`, `env`, `get-env`* ## 낮은 결합 원칙 강화[​](#낮은-결합-원칙-강화 "해당 헤딩으로 이동") 새 layer 규칙 덕분에 [Zero-Coupling, High-Cohesion 원칙](/documentation/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion)을 지키기 쉬워졌습니다. *단, 모듈을 완전히 분리할 수 없는 경우에는 Public API 등 명확한 인터페이스 경계를 정의하고, 해당 의존 코드는 가능한 한 하위 layer에 위치시키는 것을 권장합니다.* ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [React SPB Meetup #1 발표 노트](https://t.me/feature_slices) * [React Berlin Talk - Oleg Isonen Feature Driven Architecture](https://www.youtube.com/watch?v=BWAeYuWFHhs) * [v1↔v2 구조 비교(텔레그램)](https://t.me/feature_sliced/493) * [v2에 대한 새로운 아이디어와 설명 (atomicdesign 채팅)](https://t.me/atomicdesign/18708) * [v2 추상화·네이밍 공식 논의](https://github.com/feature-sliced/documentation/discussions/31) --- # v2.0 -> v2.1 마이그레이션 가이드 v2.1의 핵심 변화는 Page 중심(Page-First) 접근 방식을 통한 인터페이스 구조화입니다. ## v2.0 접근 방식[​](#v20-접근-방식 "해당 헤딩으로 이동") v2.0에서는 **Entity** 와 **Feature** 단위를 중심으로 애플리케이션을 분리했습니다.
화면을 이루는 최소 단위인 entity 표현이나 상호작용 요소까지 모두 세분화한 뒤,
이를 **Widget** 으로 조합하고, 최종적으로 **Page** 를 구성하는 모델이었죠. 이렇게 하면 재사용성과 모듈화 면에서 이점이 있었지만,
실제로는 대부분의 비즈니스 로직이 entity·feature layer에 집중되고,
Page는 단순 조합 계층에 머물러 자신만의 책임이 희미해지는 문제가 발생했습니다. ## v2.1 접근 방식[​](#v21-접근-방식 "해당 헤딩으로 이동") v2.1에서는 **Pages-First** 사고방식을 도입합니다.
대부분의 개발자가 이미 애플리케이션을 Page 단위로 나누는 데 익숙하고,
코드베이스에서 컴포넌트를 찾을 때도 Page가 자연스러운 출발점이기 때문입니다. * **Page 내부에 주요 UI와 비즈니스 로직**을 두고 **Shared** layer는 순수 재사용 요소만 관리 * 공통 로직이 실제로 여러 Page에서 쓰일 때만 하위 layer(Feature·Entity)로 분리 이 접근의 장점은 다음과 같습니다: 1. **Page가 책임 단위**가 되어, 코드 위치와 역할이 명확해집니다. 2. Shared layer는 **유틸·컴포넌트**처럼 순수 재사용 코드만 담아, 의존 경로가 간결해집니다. 3. 공통 로직을 실제로 재사용할 때만 하위 layer로 이동해, 불필요한 추상화와 의존성 얽힘을 방지합니다. 또한 v2.1에서는 **Entity 간 cross-import**를 `@x` 표기법으로 **표준화**했습니다.
이제 다음과 같은 형식으로 명확한 경로를 사용할 수 있습니다: ## 마이그레이션 프로세스[​](#how-to-migrate "해당 헤딩으로 이동") v2.1은 하위 호환성을 보장하므로, 기존 FSD v2.0 프로젝트는 **수정 없이** 동작합니다.
새 모델을 활용하려면 아래 단계를 단계적으로 적용해 보세요. ### 1. Slice 병합[​](#1-slice-병합 "해당 헤딩으로 이동") FSD v2.1의 Page-First 모델에서는 **실제로 여러 Page에서 재사용되지 않는** slice를 굳이 독립 단위로 유지할 필요가 없습니다.
단일 page에서만 사용되는 slice는 해당 page 안으로 병합하면, 코드 탐색과 유지보수가 더 쉬워집니다. #### Steiger로 자동 탐지하기[​](#steiger로-자동-탐지하기 "해당 헤딩으로 이동") 프로젝트 루트에서 [Steiger](https://github.com/feature-sliced/steiger) linter를 실행하세요.
v2.1 mental model에 맞춘 주요 lint 규칙은 다음과 같습니다: * [`insignificant-slice`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice)
단일 Page에서만 참조되는 slice를 찾아냅니다.
→ **해당 slice를 Page 내부로 병합**하도록 제안합니다. * [`excessive-slicing`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/excessive-slicing)
너무 잘게 나뉜 slice를 감지합니다.
→ **유사한 slice를 통합**하거나 **그룹화**해 탐색성을 높이도록 권장합니다. ``` npx steiger src ``` 이 명령으로 `한 번만 쓰이는 slice` 목록이 출력됩니다.
이제 각 slice의 재사용 여부를 검토하고, 과하다면 해당 page로 병합하거나 비슷한 역할끼리 묶어 보세요. Slice 관리 각 계층은 해당 계층에 속한 모든 Slices의 namespace를 관리합니다. 이는 전역 변수를 관리하는 것과 비슷한 개념입니다: * 전역 변수는 꼭 필요한 경우에만 사용하듯이 Slice도 실제로 재사용되는 경우에만 독립적으로 분리하세요 * 한 곳에서만 사용되는 코드는 해당 Page나 Feature 내부로 이동하는 것이 좋습니다 ### 2. Cross Import 표준화[​](#2-cross-import-표준화 "해당 헤딩으로 이동") 새로운 `@x-` 표기법으로 Entity 간 cross-import를 통일합니다: entities/B/some/file.ts ``` // v2.1 권장 cross-import 방식 import type { EntityA } from "entities/A/@x/B"; ``` 자세한 내용은 [Public API for cross-imports](/documentation/kr/docs/reference/public-api.md#public-api-for-cross-imports) 문서를 참고하세요. --- # Electron와 함께 사용하기 Electron 애플리케이션은 역할이 다른 여러 **프로세스**(Main, Renderer, Preload)로 구성됩니다.
따라서 FSD를 적용하려면 Electron 특성에 맞게 구조를 조정해야 합니다. ``` └── src ├── app # Common app layer │ ├── main # Main process │ │ └── index.ts # Main process entry point │ ├── preload # Preload script and Context Bridge │ │ └── index.ts # Preload entry point │ └── renderer # Renderer process │ └── index.html # Renderer process entry point ├── main │ ├── features │ │ └── user │ │ └── ipc │ │ ├── get-user.ts │ │ └── send-user.ts │ ├── entities │ └── shared ├── renderer │ ├── pages │ │ ├── settings │ │ │ ├── ipc │ │ │ │ ├── get-user.ts │ │ │ │ └── save-user.ts │ │ │ ├── ui │ │ │ │ └── user.tsx │ │ │ └── index.ts │ │ └── home │ │ ├── ui │ │ │ └── home.tsx │ │ └── index.ts │ ├── widgets │ ├── features │ ├── entities │ └── shared └── shared # Common code between main and renderer └── ipc # IPC description (event names, contracts) ``` ## Public API 규칙[​](#public-api-규칙 "해당 헤딩으로 이동") * 각 프로세스는 자신만의 Public API를 가져야 합니다. * 예) `renderer` 코드가 `main` 폴더 모듈을 직접 import 하면 안 됩니다. * 단, `src/shared` 폴더는 두 프로세스 모두에게 공개됩니다. * (프로세스 간 통신 계약과 타입 정의를 위해 필요합니다) ## 표준 구조의 추가 변경 사항[​](#표준-구조의-추가-변경-사항 "해당 헤딩으로 이동") * **`ipc` segment**를 새로 만들어, 프로세스 간 통신(채널, 핸들러)을 한곳에 모읍니다. * `src/main`에는 이름 그대로 **`pages`, `widgets` layer를 두지 않습니다.**
대신 `features`, `entities`, `shared`만 사용합니다. * `src/app` layer는 **Main, Renderer entry**와 **IPC initialization code**만 담는 전용 영역입니다. * `app` layer 내부의 각 segment는 서로 **교차 의존**이 발생하지 않도록 구성하는 것이 좋습니다. ## Interaction example[​](#interaction-example "해당 헤딩으로 이동") src/shared/ipc/channels.ts ``` export const CHANNELS = { GET_USER_DATA: 'GET_USER_DATA', SAVE_USER: 'SAVE_USER', } as const; export type TChannelKeys = keyof typeof CHANNELS; ``` src/shared/ipc/events.ts ``` import { CHANNELS } from './channels'; export interface IEvents { [CHANNELS.GET_USER_DATA]: { args: void, response?: { name: string; email: string; }; }; [CHANNELS.SAVE_USER]: { args: { name: string; }; response: void; }; } ``` src/shared/ipc/preload.ts ``` import { CHANNELS } from './channels'; import type { IEvents } from './events'; type TOptionalArgs = T extends void ? [] : [args: T]; export type TElectronAPI = { [K in keyof typeof CHANNELS]: (...args: TOptionalArgs) => IEvents[typeof CHANNELS[K]]['response']; }; ``` src/app/preload/index.ts ``` import { contextBridge, ipcRenderer } from 'electron'; import { CHANNELS, type TElectronAPI } from 'shared/ipc'; const API: TElectronAPI = { [CHANNELS.GET_USER_DATA]: () => ipcRenderer.sendSync(CHANNELS.GET_USER_DATA), [CHANNELS.SAVE_USER]: args => ipcRenderer.invoke(CHANNELS.SAVE_USER, args), } as const; contextBridge.exposeInMainWorld('electron', API); ``` src/main/features/user/ipc/send-user.ts ``` import { ipcMain } from 'electron'; import { CHANNELS } from 'shared/ipc'; export const sendUser = () => { ipcMain.on(CHANNELS.GET_USER_DATA, ev => { ev.returnValue = { name: 'John Doe', email: 'john.doe@example.com', }; }); }; ``` src/renderer/pages/user-settings/ipc/get-user.ts ``` import { CHANNELS } from 'shared/ipc'; export const getUser = () => { const user = window.electron[CHANNELS.GET_USER_DATA](); return user ?? { name: 'John Donte', email: 'john.donte@example.com' }; }; ``` ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [Process Model Documentation](https://www.electronjs.org/docs/latest/tutorial/process-model) * [Context Isolation Documentation](https://www.electronjs.org/docs/latest/tutorial/context-isolation) * [Inter-Process Communication Documentation](https://www.electronjs.org/docs/latest/tutorial/ipc) * [Example](https://github.com/feature-sliced/examples/tree/master/examples/electron) --- # NextJS와 함께 사용하기 NextJS 프로젝트에도 FSD 아키텍처를 적용할 수 있지만, 구조적 차이로 두 가지 충돌이 발생합니다. * **`pages` layer 라우팅 파일** * **NextJS에서 `app` layer의 충돌 또는 미지원** ## `pages` layer 충돌[​](#pages-conflict "해당 헤딩으로 이동") NextJS는 파일 시스템 기반 라우팅을 위해 **`pages` 폴더**의 파일을 URL에 매핑합니다.
그러나 이 방식은 FSD에서 권장하는 **평탄(flat)한 slice 구조**와 맞지 않아 충돌이 발생합니다. ### NextJS `pages` 폴더를 Project Root로 이동 (권장)[​](#nextjs-pages-폴더를-project-root로-이동-권장 "해당 헤딩으로 이동") `pages` 폴더를 **프로젝트 최상위**로 옮긴 뒤,
FSD `src/pages`의 각 페이지 컴포넌트를 `pages` 폴더에서 **re-export** 하면 NextJS 라우팅과 FSD 구조를 모두 유지할 수 있습니다. ``` ├── pages # NextJS 라우팅 폴더 (FSD pages를 재-export) │ └── index.tsx │ └── about.tsx ├── src │ ├── app │ ├── entities │ ├── features │ ├── pages # FSD pages layer │ ├── shared │ └── widgets ``` ### FSD pages layer 이름 변경[​](#fsd-pages-layer-이름-변경 "해당 헤딩으로 이동") FSD의 `pages` layer 이름을 변경해 NextJS `pages` 폴더와 충돌을 방지할 수 있습니다.
예를 들어, `pages`를 `views`로 바꾸면 라우팅 폴더와 FSD 페이지 layer를 동시에 사용할 수 있습니다. ``` ├── app ├── entities ├── features ├── pages # NextJS 라우팅 폴더 ├── views # 변경된 FSD pages layer ├── shared ├── widgets ``` 폴더 이름을 변경했다면 프로젝트 README나 내부 문서에 반드시 기록해야 합니다.
이 내용을 [프로젝트 지식](/documentation/kr/docs/about/understanding/knowledge-types.md)에 포함해 팀원들이 쉽게 확인할 수 있도록 하세요. ## NextJS에서 `app` layer 구현하기[​](#app-absence "해당 헤딩으로 이동") NextJS 13 이전 버전에는 FSD app layer에 대응하는 전용 폴더가 없습니다.
대신 pages/\_app.tsx가 모든 페이지의 wrapping component로 작동합니다.
이 파일에서 전역 상태 관리(global state management)와 레이아웃 구성(layout)을 담당합니다. ### `pages/_app.tsx`에 app layer 기능 통합하기[​](#pages_apptsx에-app-layer-기능-통합하기 "해당 헤딩으로 이동") 먼저 `src/app/providers/index.tsx`에 `App` 컴포넌트를 정의합니다.
이 컴포넌트에서 전체 애플리케이션의 provider와 layout을 설정합니다. ``` // app/providers/index.tsx const App = ({ Component, pageProps }: AppProps) => { return ( ); }; export default App; ``` 다음으로 `pages/_app.tsx`에서 위 `App` 컴포넌트를 export합니다.
이 과정에서 global style도 함께 import할 수 있습니다. ``` // pages/_app.tsx import 'app/styles/index.scss' export { default } from 'app/providers'; ``` ## App Router 사용하기[​](#app-router "해당 헤딩으로 이동") NextJS 13.4부터 `app` 폴더 기반 App Router를 지원합니다.
FSD 아키텍처를 App Router와 함께 사용하려면 다음 구조를 적용하세요. `app` 폴더는 NextJS App Router 전용입니다.
`src/app`은 FSD의 app layer를 유지합니다. 필요에 따라 App Router와 Pages Router를 함께 사용할 수 있습니다. ``` ├── app # NextJS의 App Router용 폴더 ├── pages # NextJS의 Pages Router용 폴더 (선택적) │ ├── README.md # 폴더의 용도 설명 ├── src │ ├── app # FSD의 app layer │ ├── entities │ ├── features │ ├── pages # FSD의 pages layer │ ├── shared │ ├── widgets ``` `app` 폴더에서 `src/pages`의 컴포넌트를 re-export하세요.
App Router만 사용해도 `Pages Router`와의 호환성을 위해 `root pages` 폴더를 유지합니다. [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/stackblitz-starters-aiez55?file=README.md) ## Middleware[​](#middleware "해당 헤딩으로 이동") NextJS middleware 파일은 반드시 프로젝트 root 폴더(`app` 또는 `pages` 폴더와 동일 수준)에 둬야 합니다.
`src` 아래에 두면 NextJS가 인식하지 않으므로, middleware 파일을 root로 이동하세요. ## 참고 자료[​](#see-also "해당 헤딩으로 이동") * [(스레드) NextJS의 pages 폴더에 대한 토론](https://t.me/feature_sliced/3623) --- # NuxtJS와 함께 사용하기 NuxtJS 프로젝트에 FSD(Feature-Sliced Design)를 도입할 때는 기본 구조와 FSD 원칙 간에 다음과 같은 차이를 고려해야 합니다: * NuxtJS는 `src` 폴더 없이 project root에서 파일을 관리합니다. * NuxtJS는 `pages` 폴더 기반 파일 라우팅을 사용하지만, FSD는 slice 관점에서 폴더를 구성합니다. ## `src` 폴더 alias 설정하기[​](#src-폴더-alias-설정하기 "해당 헤딩으로 이동") NuxtJS 프로젝트에도 `src` 폴더를 두고 싶다면, `nuxt.config.ts`의 `alias`에 매핑을 추가하세요. nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // 개발 도구 활성화(선택 사항) alias: { "@": '../src' // root의 src 폴더를 @로 참조 }, }) ``` ## 라우터 설정 방법 선택하기[​](#라우터-설정-방법-선택하기 "해당 헤딩으로 이동") NuxtJS에서는 두 가지 라우팅 방식을 지원합니다: * **파일 기반 라우팅**: `src/app/routes` 폴더 내 `.vue` 파일을 자동으로 라우트로 등록 * **설정 기반 라우팅**: `src/app/router.options.ts`에서 라우트를 직접 정의 ### 설정 기반 라우팅[​](#설정-기반-라우팅 "해당 헤딩으로 이동") `src/app/router.options.ts` 파일을 생성한 뒤, 아래와 같이 `RouterConfig`를 정의하세요: app/router.options.ts ``` import type { RouterConfig } from '@nuxt/schema'; export default { routes: (_routes) => [], }; ``` Home 페이지를 추가하려면 다음 순서로 진행합니다. 1. `pages` layer에 Home page slice를 생성합니다. 2. `app/router.options.ts`에 Home 라우트를 등록합니다. page slice는 [CLI](https://github.com/feature-sliced/cli)를 사용하여 생성할 수 있습니다: ``` fsd pages home ``` `src/pages/home/ui/home-page.vue`를 만든 뒤, Public API로 노출합니다. src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page'; ``` 프로젝트 구조는 다음과 같습니다. ``` |── src │ ├── app │ │ ├── router.options.ts │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.vue │ │ │ ├── index.ts ``` 이제 `router.options.ts`의 routes 배열에 Home 라우트를 추가합니다. app/router.options.ts ``` import type { RouterConfig } from '@nuxt/schema' export default { routes: (_routes) => [ { name: 'home', path: '/', component: () => import('@/pages/home.vue').then(r => r.default || r) } ], } ``` ### 파일 기반 라우팅[​](#파일-기반-라우팅 "해당 헤딩으로 이동") #### `src` 폴더와 라우트 폴더 구성[​](#src-폴더와-라우트-폴더-구성 "해당 헤딩으로 이동") 루트에 `src` 폴더를 만들고 그 안에 `app`과 `pages` layer를 생성합니다. `app` layer에 `routes` 폴더를 추가해 Nuxt 라우트를 관리합니다. ``` ├── src │ ├── app │ │ ├── routes │ ├── pages # FSD Pages layer ``` #### nuxt.config.ts에서 라우트 폴더 변경[​](#nuxtconfigts에서-라우트-폴더-변경 "해당 헤딩으로 이동") `pages` 폴더 대신 `app/routes` 폴더를 라우트 폴더로 사용하도록 설정하려면, `nuxt.config.ts` 파일을 수정해야 합니다. nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // 개발 도구 활성화 (FSD와 무관) alias: { "@": '../src' }, dir: { pages: './src/app/routes' } }) ``` 이제 `app/routes`에서 라우트를 만들고 `pages`의 컴포넌트를 연결할 수 있습니다. `Home` 페이지를 추가하려면: * `pages` layer에 slice를 생성합니다. * `app/routes`에 라우트를 생성합니다. * page slice의 컴포넌트를 라우트에서 사용할 수 있도록 연결합니다. #### 1. page slice 생성[​](#1-page-slice-생성 "해당 헤딩으로 이동") page slice는 [CLI](https://github.com/feature-sliced/cli)를 사용하여 간편하게 생성할 수 있습니다: ``` fsd pages home ``` 이제 `ui` segment 내에 `home-page.vue` 파일을 생성하고, Public API를 통해 이를 노출합니다: src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page'; ``` #### 2. `app/routes` 내에 라우트 추가[​](#2-approutes-내에-라우트-추가 "해당 헤딩으로 이동") 생성한 page를 라우트와 연결하려면, `app/routes/index.vue` 파일을 생성하고 `HomePage` 컴포넌트를 등록해야 합니다. ``` ├── src │ ├── app │ │ ├── routes │ │ │ ├── index.vue │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.vue │ │ │ ├── index.ts ``` #### 3. `index.vue`에서 page 컴포넌트 등록[​](#3-indexvue에서-page-컴포넌트-등록 "해당 헤딩으로 이동") src/app/routes/index.vue ``` ``` 이제 `HomePage`가 Nuxt 라우팅으로 정상 렌더링됩니다. ## `layouts` 관리하기[​](#layouts-관리하기 "해당 헤딩으로 이동") 레이아웃 파일을 `src/app/layouts`에 두고, `nuxt.config.ts`의 `dir.layouts`에 경로를 지정합니다. nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // 개발 도구 활성화 (FSD와 무관) alias: { "@": '../src' }, dir: { pages: './src/app/routes', layouts: './src/app/layouts' } }) ``` ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [NuxtJS dir 설정 문서](https://nuxt.com/docs/api/nuxt-config#dir) * [NuxtJS 라우터 설정 변경 문서](https://nuxt.com/docs/guide/recipes/custom-routing#router-config) * [NuxtJS 별칭(alias) 설정 문서](https://nuxt.com/docs/api/nuxt-config#alias) --- # React Query와 함께 사용하기 ## Query Key 배치 문제[​](#query-key-배치-문제 "해당 헤딩으로 이동") ### entities별 분리[​](#entities별-분리 "해당 헤딩으로 이동") 각 요청이 특정 entity에 대응한다면,
`src/entities/{entity}/api` 폴더에 관련 코드를 모아두세요: ``` └── src/ # ├── app/ # | ... # ├── pages/ # | ... # ├── entities/ # | ├── {entity}/ # | ... └── api/ # | ├── `{entity}.query` # Query Keys와 Query Functions | ├── `get-{entity}` # entity fetch 함수 | ├── `create-{entity}` # entity create 함수 | ├── `update-{entity}` # entity update 함수 | ├── `delete-{entity}` # entity delete 함수 | ... # | # ├── features/ # | ... # ├── widgets/ # | ... # └── shared/ # ... # ``` entities 간에 데이터를 참조해야 하면 [공용 Public API](/documentation/kr/docs/reference/public-api.md#public-api-for-cross-imports)를 사용하거나,
아래 예시처럼 `shared/api/queries`에 모아두는 방법도 있습니다. ### 대안 — shared에 모아두기[​](#대안--shared에-모아두기 "해당 헤딩으로 이동") entity별 분리가 어려울 때는 예시 처럼 `src/shared/api/queries`에 Query Factory를 정의하세요. ``` └── src/ # ... # └── shared/ # ├── api/ # ... ├── `queries` # Query Factories | ├── `document.ts` # | ├── `background-jobs.ts` # | ... # └── index.ts # ``` 이후 `@/shared/api/index.ts`에서 다음과 같이 사용합니다: @/shared/api/index.ts ``` export { documentQueries } from "./queries/document"; ``` ## Mutation 배치 문제[​](#mutation-배치-문제 "해당 헤딩으로 이동") Query와 Mutation을 같은 위치에 두는 것은 권장하지 않습니다.
두 가지 방안을 제안합니다: ### 사용 위치 근처 api 폴더에 Custom Hook 정의[​](#사용-위치-근처-api-폴더에-custom-hook-정의 "해당 헤딩으로 이동") @/features/update-post/api/use-update-title.ts ``` export const useUpdateTitle = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, newTitle }) => apiClient .patch(`/posts/${id}`, { title: newTitle }) .then((data) => console.log(data)), onSuccess: (newPost) => { queryClient.setQueryData(postsQueries.ids(id), newPost); }, }); }; ``` ### entities 또는 shared에 함수만 정의하고, 컴포넌트에서 `useMutation` 사용[​](#entities-또는-shared에-함수만-정의하고-컴포넌트에서-usemutation-사용 "해당 헤딩으로 이동") ``` const { mutateAsync, isPending } = useMutation({ mutationFn: postApi.createPost, }); ``` @/pages/post-create/ui/post-create-page.tsx ``` export const CreatePost = () => { const { classes } = useStyles(); const [title, setTitle] = useState(""); const { mutate, isPending } = useMutation({ mutationFn: postApi.createPost, }); const handleChange = (e: ChangeEvent) => setTitle(e.target.value); const handleSubmit = (e: FormEvent) => { e.preventDefault(); mutate({ title, userId: DEFAULT_USER_ID }); }; return (
Create ); }; ``` ## Request 조직화[​](#request-조직화 "해당 헤딩으로 이동") ### Query Factory[​](#query-factory "해당 헤딩으로 이동") Query Factory는 Query Key와 Query Function을 한곳에서 관리합니다.
다음 예시처럼 객체로 정의하세요: ``` const keyFactory = { all: () => ["entity"], lists: () => [...postQueries.all(), "list"], }; ``` info TanStack Query v5의 `queryOptions` 유틸을 사용하면 타입 안전성과 향후 호환성을 높일 수 있습니다. ``` queryOptions({ queryKey, ...options, }); ``` 자세한 내용은 [Query Options API](https://tkdodo.eu/blog/the-query-options-api#queryoptions)에서 확인하세요. ### Query Factory 생성 예시[​](#query-factory-생성-예시 "해당 헤딩으로 이동") @/entities/post/api/post.queries.ts ``` import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; import { getDetailPost } from "./get-detail-post"; import { PostDetailQuery } from "./query/post.query"; export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }), details: () => [...postQueries.all(), "detail"], detail: (query?: PostDetailQuery) => queryOptions({ queryKey: [...postQueries.details(), query?.id], queryFn: () => getDetailPost({ id: query?.id }), staleTime: 5000, }), }; ``` ### 애플리케이션 코드에서의 Query Factory 사용 예시[​](#애플리케이션-코드에서의-query-factory-사용-예시 "해당 헤딩으로 이동") ``` import { useParams } from "react-router-dom"; import { postApi } from "@/entities/post"; import { useQuery } from "@tanstack/react-query"; type Params = { postId: string; }; export const PostPage = () => { const { postId } = useParams(); const id = parseInt(postId || ""); const { data: post, error, isLoading, isError, } = useQuery(postApi.postQueries.detail({ id })); if (isLoading) { return
Loading...
; } if (isError || !post) { return <>{error?.message}; } return (

Post id: {post.id}

{post.title}

{post.body}

Owner: {post.userId}
); }; ``` ### Query Factory 사용의 장점[​](#query-factory-사용의-장점 "해당 헤딩으로 이동") * **Request 구조화**: 모든 API 호출을 Factory 패턴으로 통합 관리해, 코드 가독성과 유지보수성을 개선합니다. * **Query와 Key에 대한 편리한 접근**: 다양한 Query Type과 해당 Key를 메서드로 제공해, 언제든 간편하게 참조할 수 있습니다. * **Query Invalidation 용이성**: Query Key를 직접 수정하지 않고도 원하는 Query를 손쉽게 무효화할 수 있습니다. ## Pagination[​](#pagination "해당 헤딩으로 이동") Pagination을 적용해 `getPosts` 함수로 게시물 목록을 가져오는 과정을 설명합니다. ### `getPosts` 함수 생성하기[​](#getposts-함수-생성하기 "해당 헤딩으로 이동") `src/pages/post-feed/api/get-posts.ts` 파일에 다음과 같이 정의됩니다. @/pages/post-feed/api/get-posts.ts ``` import { apiClient } from "@/shared/api/base"; import { PostWithPaginationDto } from "./dto/post-with-pagination.dto"; import { PostQuery } from "./query/post.query"; import { mapPost } from "./mapper/map-post"; import { PostWithPagination } from "../model/post-with-pagination"; const calculatePostPage = (totalCount: number, limit: number) => Math.floor(totalCount / limit); export const getPosts = async ( page: number, limit: number, ): Promise => { const skip = page * limit; const query: PostQuery = { skip, limit }; const result = await apiClient.get("/posts", query); return { posts: result.posts.map((post) => mapPost(post)), limit: result.limit, skip: result.skip, total: result.total, totalPages: calculatePostPage(result.total, limit), }; }; ``` ### 페이지네이션용 Query Factory 정의[​](#페이지네이션용-query-factory-정의 "해당 헤딩으로 이동") 페이지 번호(`page`)와 한도(`limit`)를 인자로 받아 게시물 목록을 가져오는 Query를 설정합니다. ``` import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }), }; ``` ### 애플리케이션 코드 사용 예시[​](#애플리케이션-코드-사용-예시 "해당 헤딩으로 이동") 페이지네이션된 게시물을 화면에 렌더링하는 방법입니다.
`useQuery` 훅으로 `postQueries.list`를 호출하고, `Pagination` 컴포넌트와 연동하세요. @/pages/home/ui/index.tsx ``` export const HomePage = () => { const itemsOnScreen = DEFAULT_ITEMS_ON_SCREEN; const [page, setPage] = usePageParam(DEFAULT_PAGE); const { data, isFetching, isLoading } = useQuery( postApi.postQueries.list(page, itemsOnScreen), ); return ( <> setPage(page)} page={page} count={data?.totalPages} variant="outlined" color="primary" /> ); }; ``` note 전체 코드는 [GitHub FSD React Query](https://github.com/ruslan4432013/fsd-react-query-example) 예제에서 확인할 수 있습니다. ## Query 관리를 위한 QueryProvider[​](#query-관리를-위한-queryprovider "해당 헤딩으로 이동") QueryProvider 구성 방법을 안내합니다. ### `QueryProvider` 생성하기[​](#queryprovider-생성하기 "해당 헤딩으로 이동") `src/app/providers/query-provider.tsx`에 QueryProvider 컴포넌트를 정의합니다. @/app/providers/query-provider.tsx ``` import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactNode } from "react"; type Props = { children: ReactNode; client: QueryClient; }; export const QueryProvider = ({ client, children }: Props) => { return ( {children} ); }; ``` ### 2. `QueryClient` 생성[​](#2-queryclient-생성 "해당 헤딩으로 이동") React Query의 캐싱과 기본 옵션을 설정할 `QueryClient` 인스턴스를 만듭니다.
아래 코드를 `@/shared/api/query-client.ts`에 정의하세요. @/shared/api/query-client.ts ``` import { QueryClient } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000, }, }, }); ``` ## 코드 자동 생성[​](#코드-자동-생성 "해당 헤딩으로 이동") API 코드 자동 생성 도구를 사용하면 반복 작업을 줄일 수 있습니다.
다만, 직접 작성하는 방식보다 유연성이 떨어질 수 있습니다.
Swagger 파일이 잘 정의되어 있다면 자동 생성 도구를 활용해 코드를 생성하세요.
생성된 코드는 `@/shared/api` 디렉토리에 배치해 일관되게 관리합니다. ## React Query를 조직화하기 위한 추가 조언[​](#react-query를-조직화하기-위한-추가-조언 "해당 헤딩으로 이동") ### API Client[​](#api-client "해당 헤딩으로 이동") `shared/api`에 커스텀 APIClient 클래스를 정의하면 다음 기능을 한곳에서 일괄 설정할 수 있습니다: * response, request 로깅 및 에러 처리를 일관되게 적용 * 공통 헤더와 인증 설정, 데이터 직렬화 방식을 한곳에서 설정 * API endpoint 변경이나 옵션 업데이트를 단일 수정 지점에서 반영 @/shared/api/api-client.ts ``` import { API_URL } from "@/shared/config"; export class ApiClient { private baseUrl: string; constructor(url: string) { this.baseUrl = url; } async handleResponse(response: Response): Promise { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } try { return await response.json(); } catch (error) { throw new Error("Error parsing JSON response"); } } public async get( endpoint: string, queryParams?: Record, ): Promise { const url = new URL(endpoint, this.baseUrl); if (queryParams) { Object.entries(queryParams).forEach(([key, value]) => { url.searchParams.append(key, value.toString()); }); } const response = await fetch(url.toString(), { method: "GET", headers: { "Content-Type": "application/json", }, }); return this.handleResponse(response); } public async post>( endpoint: string, body: TData, ): Promise { const response = await fetch(`${this.baseUrl}${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); return this.handleResponse(response); } } export const apiClient = new ApiClient(API_URL); ``` ## 참고 자료[​](#see-also "해당 헤딩으로 이동") * [(GitHub) 예제 프로젝트](https://github.com/ruslan4432013/fsd-react-query-example) * [(CodeSandbox) 예제 프로젝트](https://codesandbox.io/p/github/ruslan4432013/fsd-react-query-example/main) * [Query Options 가이드](https://tkdodo.eu/blog/the-query-options-api) --- # SvelteKit와 함께 사용하기 SvelteKit 프로젝트에 FSD(Feature-Sliced Design)를 적용할 때는 다음 차이를 유의하세요: * SvelteKit은 routing 파일을 `src/routes`에 두지만, FSD는 routing을 `app` 레이어에 포함합니다. * SvelteKit은 라우트 외 파일을 `src/lib`에 두도록 권장합니다. ## 구성 설정[​](#구성-설정 "해당 헤딩으로 이동") `svelte.config.ts`에서 기본 경로를 변경해 `app` layer로 라우팅과 템플릿을 이동하고, `src/lib`를 설정합니다. svelte.config.ts ``` import adapter from '@sveltejs/adapter-auto'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config}*/ const config = { preprocess: [vitePreprocess()], kit: { adapter: adapter(), files: { routes: 'src/app/routes', // routing을 app layer로 이동 lib: 'src', appTemplate: 'src/app/index.html', // application entry point를 app layer로 이동 assets: 'public' }, alias: { '@/*': 'src/*' // src directory alias 설정 } } }; export default config; ``` ## File Routing을 `src/app`으로 이동[​](#file-routing을-srcapp으로-이동 "해당 헤딩으로 이동") 설정 변경 후 폴더 구조는 다음과 같습니다: ``` ├── src │ ├── app │ │ ├── index.html │ │ ├── routes │ ├── pages # FSD pages Layer ``` 이제 `app/routes` 폴더에 라우트 파일을 두고, `pages` layer의 컴포넌트를 연결할 수 있습니다. 예시) Home 페이지 추가 예시 1. pages layer에 새 page slice 생성 2. `app/routes`에 route 파일 추가 3. page component를 route와 연결 [CLI 도구](https://github.com/feature-sliced/cli)로 page slice를 생성합니다: ``` fsd pages home ``` `pages/home/ui/home-page.svelte`를 생성하고 public API로 노출하세요: src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page'; ``` `app/routes`에 route 파일을 추가합니다: ``` ├── src │ ├── app │ │ ├── routes │ │ │ ├── +page.svelte │ │ ├── index.html │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.svelte │ │ │ ├── index.ts ``` `+page.svelte`에서 page component를 import 후 렌더링합니다: src/app/routes/+page.svelte ``` ``` ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [SvelteKit Directory Structure 문서](https://kit.svelte.dev/docs/configuration#files) --- # Docs for LLMs This page provides links and guidance for LLM crawlers. * Spec: ### Files[​](#files "해당 헤딩으로 이동") * [llms.txt](/documentation/kr/llms.txt) * [llms-full.txt](/documentation/kr/llms-full.txt) ### Notes[​](#notes "해당 헤딩으로 이동") * Files are served from the site root, regardless of the current page path. * In deployments with a non-root base URL (e.g., `/documentation/`), the links above are automatically prefixed. --- # Layer Layer는 Feature-Sliced Design에서 **코드를 구분하는 가장 큰 범위**입니다.
코드를 나눌 때는 각 부분이 **어떤 기능을 담당하는지**와 **다른 코드에 얼마나 의존하는지**를 기준으로 합니다.
Layer마다 **어떤 역할을 맡는지**에 대한 공통된 의미가 있습니다. 총 **7개의 Layer**가 있으며, **담당 기능과 의존성이 많은 것**부터 **적은 것** 순으로 나열합니다. ![A file system tree, with a single root folder called src and then seven subfolders: app, processes, pages, widgets, features, entities, shared. The processes folder is slightly faded out.](/documentation/kr/img/layers/folders-graphic-light.svg#light-mode-only) ![A file system tree, with a single root folder called src and then seven subfolders: app, processes, pages, widgets, features, entities, shared. The processes folder is slightly faded out.](/documentation/kr/img/layers/folders-graphic-dark.svg#dark-mode-only) 1. App 2. Processes (deprecated) 3. Pages 4. Widgets 5. Features 6. Entities 7. Shared > 모든 Layer를 꼭 써야 하는 건 아닙니다. **필요할 때만** 추가하세요.
대부분의 프론트엔드 프로젝트는 최소한 `shared`, `page`, `app`은 포함합니다. 실무에서는 폴더명을 소문자로 작성합니다(예: `📁 shared`, `📁 page`, `📁 app`).
**새로운 Layer를 만드는 것은 권장하지 않습니다**(역할이 이미 표준화되어 있음). ## Import 규칙[​](#import-규칙 "해당 헤딩으로 이동") Layer는 **Slice(서로 밀접하게 연관된 모듈 묶음)** 로 구성됩니다.
Slice 간 연결은 **Layer Import 규칙**으로 제한합니다. > **규칙**: Slice 안의 코드는 **자신보다 아래 Layer**의 *다른 Slice*만 Import할 수 있습니다. 예: `📁 ~/features/aaa/api/request.ts`는 * 같은 Layer의 `📁 ~/features/bbb` → **불가능** * 더 아래 Layer(`📁 ~/entities`, `📁 ~/shared`) → **가능** * 같은 Slice(`📁 ~/features/aaa/lib/cache.ts`) → **가능** `app`과 `shared`는 **예외**입니다.
두 Layer는 **Layer이면서 동시에 하나의 큰 Slice처럼 동작**하고, 내부는 **Segment**로 나눕니다.
이 경우 Segment끼리 자유롭게 Import할 수 있습니다.
(`shared`는 비즈니스 도메인이 없고, `app`은 모든 도메인을 묶는 역할을 합니다.) ## Layer별 역할[​](#layer별-역할 "해당 헤딩으로 이동") 각 Layer가 어떤 의미를 가지며, 어떤 코드를 포함하는지 정리했습니다. ### Shared[​](#shared "해당 헤딩으로 이동") 앱의 **기본 구성 요소**를 모아둡니다.
백엔드, 서드파티 라이브러리, 실행 환경과의 연결, 그리고 **높은 응집도의 내부 라이브러리**를 포함합니다. * `app`과 마찬가지로 **Slice 없이 Segment로만 구성**합니다. * 비즈니스 도메인이 없으므로 **Shared 안의 파일끼리는 자유롭게 Import**할 수 있습니다. Segment 예시 * `📁 api` — API 클라이언트와 백엔드 요청 함수 * `📁 ui` — 공통 UI 컴포넌트 * **비즈니스 로직 제외**, **브랜드 테마 적용 가능** * 로고, 레이아웃, 자동완성/검색창 등 UI 로직 포함 가능 * 자동완성/검색창 등 **UI 로직 컴포넌트**도 가능 * `📁 lib` — 내부 라이브러리 * 단순 `utils/helpers` 모음이 아님 ([이 글 참고](https://dev.to/sergeysova/why-utils-helpers-is-a-dump-45fo)) * 하나의 주제(날짜, 색상, 텍스트 등)에 집중 * README로 역할과 범위를 문서화 * `📁 config` — 환경변수, 전역 Feature Flag * `📁 routes` — 라우트 상수/패턴 * `📁 i18n` — 번역 설정, 전역 문자열 > Segment 이름은 **무엇을 하는 폴더인지** 드러나야 합니다.
`components`, `hooks`, `types`처럼 역할이 불명확한 이름은 피하세요. ### Entities[​](#entities "해당 헤딩으로 이동") 프로젝트에서 다루는 **핵심 데이터 개념**을 나타냅니다.
비즈니스 용어(예: `User`, `Post`, `Product`)와 일치하는 경우가 많습니다. Entity Slice에는 다음을 포함할 수 있습니다: 구성 * `📁 model` — 데이터 상태와 검증 스키마 * `📁 api` — 해당 Entity의 API 요청 * `📁 ui` — Entity의 시각적 표현 * 완전한 UI 블록이 아니어도 됨 * 여러 페이지에서 재사용 가능하게 설계 * 비즈니스 로직은 props/slot으로 연결 권장 #### Entity 간 관계[​](#entity-간-관계 "해당 헤딩으로 이동") 원칙적으로 Slice끼리는 서로 모르면 좋습니다.
하지만 현실적으로 **다른 Entity를 포함**하거나 **상호작용**하는 경우가 있습니다.
이때는 로직을 **상위 Layer(Feature/Page)** 로 올려 처리하세요. 만약 한 Entity 데이터에 다른 Entity가 포함된다면,
`@x` 표기를 사용해 **교차 Public API**로 연결을 명시적으로 드러내세요. entities/artist/model/artist.ts ``` import type { Song } from "entities/song/@x/artist"; export interface Artist { name: string; songs: Array; } ``` 자세한 내용은 [Cross-Import를 위한 Public API](/documentation/kr/docs/reference/public-api.md#public-api-for-cross-imports)를 참고하세요. ### Feature[​](#feature "해당 헤딩으로 이동") 사용자가 앱에서 수행하는 주요 기능을 담습니다. 보통 특정 Entity와 연결됩니다. * 모든 기능을 Feature로 만들 필요는 없습니다. 여러 페이지에서 재사용되는 경우에만 고려하세요. * 예: 여러 에디터에서 같은 댓글 기능을 쓴다면, comments를 Feature로 만듭니다. * Feature가 너무 많으면 중요한 기능을 찾기 어려워집니다. 구성 * 📁 ui — 상호작용 UI(예: 폼) * 📁 api — 기능 관련 API 요청 * 📁 model — 검증, 내부 상태 * 📁 config — Feature Flag > 새로운 팀원이 들어왔을 때 Page와 Feature만 봐도 앱 기능 구조를 파악할 수 있도록 구성하세요. ### Widget[​](#widget "해당 헤딩으로 이동") 독립적으로 동작하는 큰 UI 블록입니다.
여러 페이지에서 재사용되거나, 한 페이지에 큰 블록이 여럿 있을 때 유용합니다. tip * 재사용되지 않고 특정 페이지의 핵심 콘텐츠라면 Widget으로 분리하지 말고 Page 안에 두세요. * Nested Routing(예: [Remix](https://remix.run)) 환경에서는 Widget이 **Page와 유사한 방식**으로 동작합니다.
예를 들어, 데이터 로딩, 로딩 상태 표시, 에러 처리 등을 포함해 **하나의 라우터 단위 블록**을 구성할 수 있습니다. ### Page[​](#page "해당 헤딩으로 이동") 웹/앱의 **화면(screen) 또는 액티비티(activity)** 에 해당합니다. 대부분 페이지 1개 = Slice 1개이지만, 유사한 페이지는 하나의 Slice로 묶을 수 있습니다. * 코드 찾기만 쉽다면 Page Slice 크기 제한은 없습니다. * 재사용되지 않는 UI는 Page 안에 둡니다. * 보통 전용 모델은 없고, 간단한 상태만 컴포넌트 내부에서 관리합니다. 구성 예 * 📁 ui — UI, 로딩 상태, 에러 처리 * 📁 api — 데이터 패칭/변경 요청 ### Process[​](#process "해당 헤딩으로 이동") caution Deprecated — 기존 코드는 feature나 app으로 옮기세요. 과거에는 여러 페이지를 넘나드는 기능을 위한 탈출구 역할이었지만, 정의가 모호해 대부분의 앱에서는 사용하지 않습니다.
라우터, 서버 레벨 로직은 App Layer에 두고, App이 너무 커질 때만 제한적으로 고려하세요. ### App[​](#app "해당 헤딩으로 이동") 앱 전역에서 동작하는 **환경 설정**과 **공용 로직**을 관리하는 Layer입니다.
예를 들어, 라우터 설정, 전역 상태 관리, 글로벌 스타일, 진입점 설정 등 **앱 전체에 영향을 주는 코드**를 둡니다. * shared처럼 Slice 없이 Segment로 구성합니다. * 대표 Segment: * `📁 routes` — Router 설정 * `📁 store` — Global State Store 설정 * `📁 styles` — Global Style * `📁 entrypoint` — Application Entry Point와 Framework 설정 --- # Public API Public API는 **Slice 기능을 외부에서 사용할 수 있는 공식 경로**입니다.
외부 코드는 반드시 이 경로를 통해서만 Slice 내부의 특정 객체에 접근할 수 있습니다.
즉, **Slice와 외부 코드 간의 계약(Contract)** 이자 **접근 게이트(Gate)** 역할을 합니다. 일반적으로 Public API는 **Re-export를 모아둔 index 파일**로 만듭니다. pages/auth/index.js ``` export { LoginPage } from "./ui/LoginPage"; export { RegisterPage } from "./ui/RegisterPage"; ``` ## 좋은 Public API의 조건[​](#좋은-public-api의-조건 "해당 헤딩으로 이동") 좋은 Public API는 Slice를 **다른 코드와 통합하기 쉽고, 안정적으로 유지보수**할 수 있게 해줍니다.
이를 위해 다음 세 가지 목표를 충족하는 것이 이상적입니다. 1. **내부 구조 변경에 영향 없음** — Slice 내부 폴더 구조를 바꿔도 외부 코드는 그대로 동작해야 합니다. 2. **주요 동작 변경 = API 변경** — Slice의 동작이 크게 바뀌어 기존 기대를 깨면, Public API도 변경되어야 합니다. 3. **필요한 부분만 노출** — Slice 전체가 아니라 꼭 필요한 기능만 외부에 공개합니다. ### 안 좋은 예: 무분별한 Wildcard Re-export[​](#안-좋은-예-무분별한-wildcard-re-export "해당 헤딩으로 이동") 개발 초기에는 편의상 모든 `export *`를 한 번에 노출하고 싶을 때가 있습니다.
이 경우 `export *` 같은 와일드카드 Re-export를 쓰기 쉽지만, 이는 Slice의 인터페이스를 불명확하게 만듭니다. Bad practice, features/comments/index.js ``` // ❌ 이렇게 하지 마세요 export * from "./ui/Comment"; // 👎 무분별한 UI export export * from "./model/comments"; // 💩 내부 모델 노출 ``` 문제가 되는 이유: * **발견 가능성 저하** — Public API에서 어떤 기능을 제공하는지 한눈에 알기 어렵습니다. * **내부 구현 노출** — 의도치 않게 Slice 내부 코드를 외부에서 사용하게 되고, 그 코드에 의존하면 리팩터링이 매우 어려워집니다. ## Cross-Import를 위한 Public API[​](#public-api-for-cross-imports "해당 헤딩으로 이동") **Cross-import**는 같은 Layer 안에서 한 Slice가 다른 Slice를 import하는 것을 말합니다.
[Layer Import Rule](/documentation/kr/docs/reference/layers.md#import-rule-on-layers)에 따라 원칙적으로 금지되지만, **Entity 간 참조**처럼 불가피한 경우가 있습니다. 예를 들어, 비즈니스 도메인에서 `Artist`와 `Song`이 서로 연결되는 관계가 있다면, 우회하기보다 코드에 그대로 반영하는 것이 좋습니다. 이때는 `@x` 표기를 사용해 **전용 Public API**를 명시적으로 만듭니다. * `📂 entities` * `📂 A` * `📂 @x` * `📄 B.ts` — `entities/B/` 전용 Public API * `📄 index.ts` — 일반 Public API `entities/song`에서는 이렇게 import합니다. ``` import type { Artist } from "entities/artist/@x/song"; ``` `artist/@x/song`은 **Artist와 Song의 교차 지점** 을 의미합니다. note Cross-import는 최소화해야 하며, **Entity Layer**에서만 사용하세요.
다른 Layer에서는 의존 관계를 제거하는 것이 좋습니다. ## Index File 사용 시 주의사항[​](#index-file-사용-시-주의사항 "해당 헤딩으로 이동") ### Circular Import (순환 참조)[​](#circular-import-순환-참조 "해당 헤딩으로 이동") Circular Import는 두 개 이상의 파일이 서로를 참조하는 구조를 말합니다.
이 구조는 Bundler가 처리하기 어렵고, 디버그하기 힘든 런타임 오류를 유발할 수 있습니다. 순환 참조는 Index 파일 없이도 발생할 수 있지만, Index 파일은 특히 이런 실수를 만들기 쉽습니다.
예를 들어, Slice의 Public API에서 `HomePage`와 `loadUserStatistics`를 export하고, `HomePage`가 다시 Public API를 통해 `loadUserStatistics`를 가져오면 다음과 같이 순환이 생깁니다. ![세 파일이 서로 원형으로 import하는 모습](/documentation/kr/img/circular-import-light.svg#light-mode-only)![세 파일이 서로를 원형으로 import하고 있는 예시입니다.](/documentation/kr/img/circular-import-dark.svg#dark-mode-only) 위 그림: `fileA.js`, `fileB.js`, `fileC.js` 파일의 Circular Import 예시 pages/home/ui/HomePage.jsx ``` import { loadUserStatistics } from "../"; // pages/home/index.js에서 import export function HomePage() { /* … */ } ``` pages/home/index.js ``` export { HomePage } from "./ui/HomePage"; export { loadUserStatistics } from "./api/loadUserStatistics"; ``` 위 구조에서는 `index.js`가 `HomePage`를 가져오고, `HomePage.jsx`는 다시 `index.js`를 통해 `loadUserStatistics`를 가져오면서 순환이 발생합니다. 여기서는 `index.js` → `HomePage.jsx`→ `index.js` 순환이 발생합니다. #### 예방 원칙[​](#예방-원칙 "해당 헤딩으로 이동") * 같은 Slice 내부: 상대 경로(`../api/loadUserStatistics`)로 import하고, 경로를 명확히 작성 * 다른 Slice: 절대 경로(예: `@/features/...`)나 Alias를 사용 Index에서 export한 모듈이 다시 Index를 참조하지 않도록 주의 ### Large Bundle & Tree-shaking 문제[​](#large-bundles "해당 헤딩으로 이동") 일부 Bundler는 Index 파일에서 모든 모듈을 export할 때 **미사용 코드**를 제대로 제거(Tree-shaking)하지 못할 수 있습니다. 대부분의 Public API에서는 모듈 간 연관성이 높아 문제가 되지 않지만, `shared/ui`와 `shared/lib`처럼 **서로 관련 없는 모듈 집합**에서는 문제가 심각해집니다. * `📂 shared/ui/` * `📁 button` * `📁 text-field` * `📁 carousel` * `📁 accordion` 여기서 `Button` 하나만 사용해도 `carousel`이나 `accordion` 같은 무거운 의존성이 번들에 포함될 수 있습니다.
특히 Syntax Highlighter, Drag-and-Drop 라이브러리처럼 용량이 큰 의존성은 영향을 크게 줍니다. #### 해결 방법[​](#해결-방법 "해당 헤딩으로 이동") * 각 컴포넌트/라이브러리별로 별도 Index 파일 생성 * `📂 shared/ui/` * `📂 button` * `📄 index.js` * `📂 text-field` * `📄 index.js` * 직접 import pages/sign-in/ui/SignInPage.jsx ``` import { Button } from '@/shared/ui/button'; import { TextField } from '@/shared/ui/text-field'; ``` 이렇게 하면 필요한 코드만 번들에 포함되어 Tree-shaking이 잘 동작합니다. ### Public API 우회 방지의 한계[​](#public-api-우회-방지의-한계 "해당 헤딩으로 이동") Slice에 Index 파일을 만들어도 직접 경로 import를 완전히 막을 수 없습니다.
특히 IDE의 Auto Import 기능이 잘못된 경로를 선택해 Public API 규칙을 어길 수 있습니다. #### 해결 방법[​](#해결-방법-1 "해당 헤딩으로 이동") * [Steiger](https://github.com/feature-sliced/steiger)와 같은 FSD 전용 아키텍처 린터로 import 경로를 검사·강제 ### 대규모 프로젝트에서의 Bundler 성능 문제[​](#대규모-프로젝트에서의-bundler-성능-문제 "해당 헤딩으로 이동") [TkDodo 글](https://tkdodo.eu/blog/please-stop-using-barrel-files)에서도 지적했듯,
Index 파일이 많아지면 개발 서버(HMR) 속도가 느려질 수 있습니다. #### 최적화 방법[​](#최적화-방법 "해당 헤딩으로 이동") 1. [Large Bundle & Tree-shaking 문제](#large-bundles) 방식 적용 — `shared/ui`와 `shared/lib`에 대형 Index 대신 컴포넌트별 Index 사용 2. Segment 단위의 불필요한 Index 파일 생성 방지
예: `📄 features/comments/index.js`가 있다면, `📄 features/comments/ui/index.js` 같은 중첩 Index는 불필요 3. 큰 프로젝트는 기능 단위로 여러 Chunk(또는 패키지)로 나누기 * Google Docs처럼 Document Editor와 File Browser를 분리 * Monorepo에서 각 패키지를 독립 FSD Root로 구성 * 일부 패키지는 Shared·Entity Layer만 포함 * 다른 패키지는 Page·App Layer만 포함 * 필요한 경우 작은 Shared를 갖고 다른 패키지의 큰 Shared를 참조 --- # Slices and segments ## Slice[​](#slice "해당 헤딩으로 이동") Slice는 Feature-Sliced Design 조직 구조의 **두 번째 계층**입니다.
주 목적은 제품, 비즈니스, 또는 단순히 애플리케이션 관점에서 **관련 있는 코드를 하나로 묶는 것**입니다. Slice의 이름은 표준화되어 있지 않고, 애플리케이션의 비즈니스 도메인에 따라 결정됩니다. 예를 들어: * 사진 갤러리: `photo`, `effects`, `gallery-page` * 소셜 네트워크: `post`, `comments`, `news-feed` `Shared`와 `App` Layer는 Slice를 포함하지 않습니다.
Shared는 비즈니스 로직이 전혀 없어 제품 관점에서 의미가 없고, App은 애플리케이션 전체를 다루기 때문에 별도로 나눌 필요가 없습니다. ### Zero 결합도와 높은 응집도[​](#zero-coupling-high-cohesion "해당 헤딩으로 이동") Slice는 **다른 Slice와 독립적**이며, **자신의 핵심 목적과 관련된 대부분의 코드**를 포함해야 합니다.
아래 그림은 **응집도(cohesion)** 와 **결합도(coupling)** 개념을 시각적으로 설명합니다. ![](/documentation/kr/img/coupling-cohesion-light.svg#light-mode-only)![](/documentation/kr/img/coupling-cohesion-dark.svg#dark-mode-only) Image inspired by Slice의 독립성은 [Layer Import Rule](/documentation/kr/docs/reference/layers.md#import-rule-on-layers)로 보장됩니다. > *Slice 내부의 모듈(파일)은 자신보다 아래 계층(Layer)에 있는 Slice만 import할 수 있습니다.* ### Slice의 Public API 규칙[​](#slice의-public-api-규칙 "해당 헤딩으로 이동") Slice 내부 구조는 **팀이 원하는 방식대로 자유롭게** 구성할 수 있습니다.
단, 다른 Slice가 사용할 수 있도록 **명확한 Public API**를 반드시 제공해야 합니다.
이 규칙은 **Slice Public API Rule**로 강제됩니다. > *모든 Slice(또는 Slice가 없는 Layer의 Segment)는 Public API를 정의해야 합니다.*
*외부 모듈은 Slice/Segment의 내부 구조가 아니라 Public API를 통해서만 접근할 수 있습니다.* Public API의 목적과 작성 방법은 [Public API Reference](/documentation/kr/docs/reference/public-api.md)에서 자세히 설명합니다. ### Slice Group[​](#slice-group "해당 헤딩으로 이동") 연관성이 높은 Slice는 폴더로 묶어서 관리할 수 있습니다.
단, 다른 Slice와 동일하게 **격리 규칙**을 적용해야 하며, 그룹 내부에서도 **코드 공유는 불가능**합니다. ![Features \"compose\", \"like\" 그리고 \"delete\"가 \"post\" 폴더에 그룹화되어 있습니다. 해당 폴더에는 허용되지 않음을 나타내기 위해 취소선이 그어진 \"some-shared-code.ts\" 파일도 있습니다.](/documentation/kr/assets/images/graphic-nested-slices-b9c44e6cc55ecdbf3e50bf40a61e5a27.svg) ## Segment[​](#segment "해당 헤딩으로 이동") Segment는 FSD 구조에서 **세 번째이자 마지막 계층**이며,
코드를 **기술적 성격**에 따라 그룹화합니다. 표준 Segment: * `ui` — UI 관련: Component, Date Formatter, Style 등 * `api` — Backend 통신: Request Function, Data Type, Mapper 등 * `model` — Data Model: Schema, Interface, Store, Business Logic * `lib` — Slice 내부 Library 코드 * `config` — Configuration과 Feature Flag 각 Layer에서 Segment를 어떻게 활용하는지는 [Layer 페이지](/documentation/kr/docs/reference/layers.md#layer-definitions)를 참고하세요. 또한 **커스텀 Segment**를 만들 수 있습니다.
특히 `App` Layer와 `Shared` Layer는 Slice가 없기 때문에, 커스텀 Segment가 자주 사용됩니다. Segment 이름은 **내용의 본질(components/hooks/types)** 이 아니라 **목적**을 설명해야 합니다.
예를 들어 `components`, `hooks`, `types` 같은 이름은 찾을 때 도움이 되지 않으므로 피하세요. --- ### 명시적 비즈니스 로직 도메인별로 코드를 구분해 필요한 로직을 즉시 찾을 수 있습니다. ---