# Feature-Sliced Design ## kr - [예제](/examples.md): Feature‑Sliced Design으로 제작된 웹사이트 모음 - [🧭 내비게이션](/nav.md): Feature-Sliced Design Navigation help page - [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 같은 정적 타입 언어에서 데이터를 어떻게 정의하고 활용할지, - [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 ![](/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](/kr/docs/get-started/tutorial.md) [**old**:](/kr/docs/get-started/tutorial.md) [/docs/get-started/quick-start](/kr/docs/get-started/tutorial.md) [**new**: ](/kr/docs/get-started/tutorial.md) [/docs/get-started/tutorial](/kr/docs/get-started/tutorial.md) [Basics](/kr/docs/get-started/overview.md) [**old**:](/kr/docs/get-started/overview.md) [/docs/get-started/basics](/kr/docs/get-started/overview.md) [**new**: ](/kr/docs/get-started/overview.md) [/docs/get-started/overview](/kr/docs/get-started/overview.md) [Decompose Cheatsheet](/kr/docs/get-started/cheatsheet.md) [**old**:](/kr/docs/get-started/cheatsheet.md) [/docs/get-started/tutorial/decompose; /docs/get-started/tutorial/design-mockup; /docs/get-started/onboard/cheatsheet](/kr/docs/get-started/cheatsheet.md) [**new**: ](/kr/docs/get-started/cheatsheet.md) [/docs/get-started/cheatsheet](/kr/docs/get-started/cheatsheet.md) ### 🍰 Alternatives ⚡️ Moved and merged to /about/alternatives as advanced materials [Architecture approaches alternatives](/kr/docs/about/alternatives.md) [**old**:](/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](/kr/docs/about/alternatives.md) [**new**: ](/kr/docs/about/alternatives.md) [/docs/about/alternatives](/kr/docs/about/alternatives.md) ### 🍰 Promote & Understanding ⚡️ Moved to /about as advanced materials [Knowledge types](/kr/docs/about/understanding/knowledge-types.md) [**old**:](/kr/docs/about/understanding/knowledge-types.md) [/docs/reference/knowledge-types](/kr/docs/about/understanding/knowledge-types.md) [**new**: ](/kr/docs/about/understanding/knowledge-types.md) [/docs/about/understanding/knowledge-types](/kr/docs/about/understanding/knowledge-types.md) [Needs driven](/kr/docs/about/understanding/needs-driven.md) [**old**:](/kr/docs/about/understanding/needs-driven.md) [/docs/concepts/needs-driven](/kr/docs/about/understanding/needs-driven.md) [**new**: ](/kr/docs/about/understanding/needs-driven.md) [/docs/about/understanding/needs-driven](/kr/docs/about/understanding/needs-driven.md) [About architecture](/kr/docs/about/understanding/architecture.md) [**old**:](/kr/docs/about/understanding/architecture.md) [/docs/concepts/architecture](/kr/docs/about/understanding/architecture.md) [**new**: ](/kr/docs/about/understanding/architecture.md) [/docs/about/understanding/architecture](/kr/docs/about/understanding/architecture.md) [Naming adaptability](/kr/docs/about/understanding/naming.md) [**old**:](/kr/docs/about/understanding/naming.md) [/docs/concepts/naming-adaptability](/kr/docs/about/understanding/naming.md) [**new**: ](/kr/docs/about/understanding/naming.md) [/docs/about/understanding/naming](/kr/docs/about/understanding/naming.md) [Signals of architecture](/kr/docs/about/understanding/signals.md) [**old**:](/kr/docs/about/understanding/signals.md) [/docs/concepts/signals](/kr/docs/about/understanding/signals.md) [**new**: ](/kr/docs/about/understanding/signals.md) [/docs/about/understanding/signals](/kr/docs/about/understanding/signals.md) [Abstractions of architecture](/kr/docs/about/understanding/abstractions.md) [**old**:](/kr/docs/about/understanding/abstractions.md) [/docs/concepts/abstractions](/kr/docs/about/understanding/abstractions.md) [**new**: ](/kr/docs/about/understanding/abstractions.md) [/docs/about/understanding/abstractions](/kr/docs/about/understanding/abstractions.md) ### 📚 Reference guidelines (isolation & units) ⚡️ Moved to /reference as theoretical materials (old concepts) [Decouple of entities](/kr/docs/reference/layers.md#import-rule-on-layers) [**old**:](/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/decouple-entities](/kr/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/kr/docs/reference/layers.md#import-rule-on-layers) [Low Coupling & High Cohesion](/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**old**:](/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/concepts/low-coupling](/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**new**: ](/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/reference/slices-segments#zero-coupling-high-cohesion](/kr/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [Cross-communication](/kr/docs/reference/layers.md#import-rule-on-layers) [**old**:](/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/cross-communication](/kr/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/kr/docs/reference/layers.md#import-rule-on-layers) [App splitting](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/concepts/app-splitting](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Decomposition](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units/decomposition](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Units](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Layers](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units/layers](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Layer overview](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/layers/overview](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [App layer](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units/layers/app](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Processes layer](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units/layers/processes](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Pages layer](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units/layers/pages](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Widgets layer](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units/layers/widgets](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Widgets layer](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/layers/widgets](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Features layer](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units/layers/features](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Entities layer](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units/layers/entities](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Shared layer](/kr/docs/reference/layers.md) [**old**:](/kr/docs/reference/layers.md) [/docs/reference/units/layers/shared](/kr/docs/reference/layers.md) [**new**: ](/kr/docs/reference/layers.md) [/docs/reference/layers](/kr/docs/reference/layers.md) [Segments](/kr/docs/reference/slices-segments.md) [**old**:](/kr/docs/reference/slices-segments.md) [/docs/reference/units/segments](/kr/docs/reference/slices-segments.md) [**new**: ](/kr/docs/reference/slices-segments.md) [/docs/reference/slices-segments](/kr/docs/reference/slices-segments.md) ### 🎯 Bad Practices handbook ⚡️ Moved to /guides as practice materials [Cross-imports](/kr/docs/guides/issues/cross-imports.md) [**old**:](/kr/docs/guides/issues/cross-imports.md) [/docs/concepts/issues/cross-imports](/kr/docs/guides/issues/cross-imports.md) [**new**: ](/kr/docs/guides/issues/cross-imports.md) [/docs/guides/issues/cross-imports](/kr/docs/guides/issues/cross-imports.md) [Desegmented](/kr/docs/guides/issues/desegmented.md) [**old**:](/kr/docs/guides/issues/desegmented.md) [/docs/concepts/issues/desegmented](/kr/docs/guides/issues/desegmented.md) [**new**: ](/kr/docs/guides/issues/desegmented.md) [/docs/guides/issues/desegmented](/kr/docs/guides/issues/desegmented.md) [Routes](/kr/docs/guides/issues/routes.md) [**old**:](/kr/docs/guides/issues/routes.md) [/docs/concepts/issues/routes](/kr/docs/guides/issues/routes.md) [**new**: ](/kr/docs/guides/issues/routes.md) [/docs/guides/issues/routes](/kr/docs/guides/issues/routes.md) ### 🎯 Examples ⚡️ Grouped and simplified into /guides/examples as practical examples [Viewer logic](/kr/docs/guides/examples/auth.md) [**old**:](/kr/docs/guides/examples/auth.md) [/docs/guides/examples/viewer](/kr/docs/guides/examples/auth.md) [**new**: ](/kr/docs/guides/examples/auth.md) [/docs/guides/examples/auth](/kr/docs/guides/examples/auth.md) [Monorepo](/kr/docs/guides/examples/monorepo.md) [**old**:](/kr/docs/guides/examples/monorepo.md) [/docs/guides/monorepo](/kr/docs/guides/examples/monorepo.md) [**new**: ](/kr/docs/guides/examples/monorepo.md) [/docs/guides/examples/monorepo](/kr/docs/guides/examples/monorepo.md) [White Labels](/kr/docs/guides/examples/white-labels.md) [**old**:](/kr/docs/guides/examples/white-labels.md) [/docs/guides/white-labels](/kr/docs/guides/examples/white-labels.md) [**new**: ](/kr/docs/guides/examples/white-labels.md) [/docs/guides/examples/white-labels](/kr/docs/guides/examples/white-labels.md) ### 🎯 Migration ⚡️ Grouped and simplified into /guides/migration as migration guidelines [Migration from V1](/kr/docs/guides/migration/from-v1.md) [**old**:](/kr/docs/guides/migration/from-v1.md) [/docs/guides/migration-from-v1](/kr/docs/guides/migration/from-v1.md) [**new**: ](/kr/docs/guides/migration/from-v1.md) [/docs/guides/migration/from-v1](/kr/docs/guides/migration/from-v1.md) [Migration from Legacy](/kr/docs/guides/migration/from-custom.md) [**old**:](/kr/docs/guides/migration/from-custom.md) [/docs/guides/migration-from-legacy](/kr/docs/guides/migration/from-custom.md) [**new**: ](/kr/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/kr/docs/guides/migration/from-custom.md) ### 🎯 Tech ⚡️ Grouped into /guides/tech as tech-specific usage guidelines [Usage with NextJS](/kr/docs/guides/tech/with-nextjs.md) [**old**:](/kr/docs/guides/tech/with-nextjs.md) [/docs/guides/usage-with-nextjs](/kr/docs/guides/tech/with-nextjs.md) [**new**: ](/kr/docs/guides/tech/with-nextjs.md) [/docs/guides/tech/with-nextjs](/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](/kr/docs/guides/migration/from-custom.md) [**old**:](/kr/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-legacy](/kr/docs/guides/migration/from-custom.md) [**new**: ](/kr/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/kr/docs/guides/migration/from-custom.md) ### Deduplication of Reference ⚡️ Cleaned up the Reference section and deduplicated the material [Isolation of modules](/kr/docs/reference/layers.md#import-rule-on-layers) [**old**:](/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/isolation](/kr/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/kr/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/kr/docs/reference/layers.md#import-rule-on-layers) --- # Feature‑Sliced Design 버전 ### Feature-Sliced Design v2.1 (Current) 현재 배포된 문서는 여기에서 확인할 수 있습니다. | v2.1 | [Release Notes](https://github.com/feature-sliced/documentation/releases/tag/v2.1) | [Documentation](/kr/docs/get-started/overview.md) | [Migration from v1](/kr/docs/guides/migration/from-v1.md) | [Migration from v2.0](/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](/kr/community/team.md) [Core-team, Champions, Contributors, Companies](/kr/community/team.md) [Brandbook](/kr/docs/branding.md) [Recommendations for FSD's branding usage](/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)](/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 이 문서는 우리가 이 방법론을 설계할 때 어떤 **목표**를 가지고 있는지,
그리고 실제로 적용할 때 어떤 **한계**가 있는지를 설명합니다. 이 방법론의 목표는 **이념적 완벽함과 실용적인 단순성 사이의 균형**을 맞추는 것입니다.
모든 사람과 모든 프로젝트에 100% 들어맞는 만능 해결책은 존재하지 않습니다. 그럼에도 불구하고, 이 방법론은 **다양한 개발자들이 쉽게 접근할 수 있고, 실제 업무에서 충분히 쓸 만해야 합니다.** ## 목표[​](#목표 "해당 헤딩으로 이동") ### 다양한 개발자에게 직관적이고 명확하게[​](#다양한-개발자에게-직관적이고-명확하게 "해당 헤딩으로 이동") 방법론은 프로젝트에 참여하는 **대부분의 팀원들이 쉽게 이해하고 사용할 수 있도록** 설계되어야 합니다. *새로운 도구나 개념이 추가되었을 때, 시니어나 리더급 개발자만 이해할 수 있다면 그 방법론은 충분하지 않습니다.* ### 일상적인 문제 해결[​](#일상적인-문제-해결 "해당 헤딩으로 이동") 방법론은 실제 개발 과정에서 자주 맞닥뜨리는 문제들에 대해
**명확한 기준과 해결책**을 제시해야 합니다. 이를 위해 **CLI, 린터(linter)** 같은 도구도 함께 제공하는 것이 중요합니다. 이런 도구들을 통해 개발자들은 * 아키텍처 설계나 구현 과정에서 반복적으로 발생하는 문제를 줄이고, * 이미 검증된 접근 방식을 자연스럽게 사용할 수 있습니다. > *@sergeysova: 방법론을 기반으로 코드를 작성하는 개발자는
이미 많은 문제에 대한 “해법 세트”를 가지고 시작한다고 상상해 보세요.
문제 발생 빈도가 10배 정도 줄어든다고 생각할 수 있습니다.* ## 한계[​](#한계 "해당 헤딩으로 이동") 우리는 *특정 관점을 강요하지 않으려* 하면서도,
동시에 *개발자로서 기존 습관이 오히려 문제 해결을 방해할 수 있다는 점*도 인지하고 있습니다. 개발자마다 시스템 설계 경험과 개발 경력이 다르기 때문에,
아래 내용을 이해하는 것이 중요합니다. * **항상 통하지는 않음**
단순하고 명확한 접근법이라고 해서
모든 상황, 모든 사람에게 항상 효과적이라고 볼 수는 없습니다. > *@sergeysova: 어떤 개념은 직접 문제를 겪고,
오랜 시간 고민하며 해결해 보는 과정을 거쳐야만
비로소 직관적으로 이해할 수 있습니다.* > > * *수학: 그래프 이론* > * *물리학: 양자 역학* > * *프로그래밍: 애플리케이션 아키텍처* * **가능하고 바람직한 방향**
이 방법론은 **단순함**과 **확장 가능성**을 지향하는 방향으로 설계되었습니다. ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [아키텍쳐 문제들](/kr/docs/about/understanding/architecture.md#problems) --- # Motivation **Feature-Sliced Design**은 [여러 개발자들의 연구와 경험을 결합해](https://github.com/feature-sliced/documentation/discussions)
복잡하고 점점 더 커지는 프로젝트를 더 단순하게 개발하고, 비용을 줄이려는 아이디어에서 출발했습니다. 물론 이 방법론이 모든 문제를 해결하는 만능 열쇠는 아니며, [적용상의 한계](/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를 일부라도 적용하면, 제한된 시간 안에서도
**나중에 완전히 갈아엎지 않아도 되는 구조**에 가까운 타협점을 찾을 수 있습니다. *테스팅에도 비슷한 원리가 적용됩니다.* ## 방법론이 필요하지 않은 경우[​](#방법론이-필요하지-않은-경우 "해당 헤딩으로 이동") 다음과 같은 경우에는 이 방법론이 꼭 필요하지 않을 수 있습니다. * 프로젝트 수명이 짧은 경우 * 지속적인 아키텍처 관리가 필요 없는 경우 * 비즈니스가 “코드 품질과 전달 속도 사이의 연관성”을 중요하게 보지 않는 경우 * 사후 지원보다 **빠른 납품**이 더 우선인 경우 ### 비즈니스 규모[​](#비즈니스-규모 "해당 헤딩으로 이동") * **소규모** → 지금 당장 사용할 수 있는 빠른 솔루션이 중요합니다.
시간이 지나 성장하면서, 품질과 안정성에 대한 투자의 필요성을 점차 인식하게 됩니다. * **중간 규모** → 기능 경쟁 속에서도 품질 개선, 리팩토링, 테스트 등에 투자하며
**장기적으로 확장 가능한 아키텍처**를 중요하게 생각합니다. * **대규모** → 이미 자체적인 아키텍처 접근 방식을 가지고 있을 가능성이 크며,
외부 방법론을 새로 도입할 가능성은 상대적으로 낮습니다. ## 계획[​](#계획 "해당 헤딩으로 이동") 이 방법론의 주요 목표는 [여기](/kr/docs/about/mission.md#goals)에 정의되어 있으며,
앞으로 이 방법론이 어떤 방향으로 발전해야 할지에 대해서도 함께 고민하고 있습니다. ### 경험 결합[​](#경험-결합 "해당 헤딩으로 이동") 현재 우리는 `core-team`의 다양한 경험을 모아
**더 단단한 방법론**을 만드는 작업을 진행 중입니다. 물론 그 결과가 어쩌면 Angular 3.0처럼 평가될 수도 있습니다.
하지만 중요한 것은, **복잡한 아키텍처 설계 문제를 진지하게 탐구하는 과정 자체**입니다. *커뮤니티 경험 또한 적극적으로 반영해,
최대한 많은 사람들이 납득할 수 있는 최적의 합의점을 찾는 것이 목표입니다.* ### 사양을 넘어선 생명력[​](#사양을-넘어선-생명력 "해당 헤딩으로 이동") 모든 것이 계획대로 진행된다면, 이 방법론은 단순히
“사양과 툴킷” 수준에 머무르지 않을 것입니다. * 관련 발표나 보고서, 기사 * 다른 기술 스택으로 마이그레이션할 수 있는 CODE\_MODE * 대규모 솔루션을 유지보수하는 개발자들에게 적용할 기회 * *특히 React는 다른 프레임워크에 비해 구체적인 해결책을 거의 제공하지 않는다는 점이 문제입니다.* ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [(토론) 방법론이 필요하지 않나요?](https://github.com/feature-sliced/documentation/discussions/27) * [방법론의 목표와 한계](/kr/docs/about/mission.md) * [프로젝트에서 다루는 지식의 유형](/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](/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` 폴더로 옮겨져,
**실제로는 재사용되지 않는 “공용 모듈”이 계속 쌓입니다.** **예시:** * 동일한 계산/검증 로직이 여러 군데에서 **복붙**으로 반복 구현되어,
수정할 때는 모든 위치를 하나씩 찾아 고쳐야 합니다. * 버튼, 팝업 같은 컴포넌트가 스타일/동작이 약간씩 다른 여러 버전으로 중복 존재합니다. * 유틸 함수들이 규칙 없이 `utils.ts`, `helpers.ts` 등에 계속 쌓여,
어떤 함수가 있는지 찾기 어렵고 중복도 쉽게 생깁니다. ## 요구사항[​](#요구사항 "해당 헤딩으로 이동") “이상적인” 아키텍처를 이야기할 때, 현실적인 관점에서 다음과 같은 요구사항을 생각해 볼 수 있습니다. note 여기서 “쉽다”라는 말은
**“대부분의 개발자가 합리적인 시간 안에 이해하고 실제로 적용할 수 있다”** 는 의미입니다. [모든 상황에 완벽히 들어맞는 아키텍처는 없기 때문에](/kr/docs/about/mission.md#limitations),
실제 팀과 프로젝트에 맞는 **실용적인 합의**가 더 중요합니다. ### 명시성[​](#명시성 "해당 헤딩으로 이동") * 프로젝트의 구조와 아키텍처를 **누구나 쉽게 이해하고, 다른 사람에게 설명할 수 있어야** 합니다. * 아키텍처는 프로젝트의 **비즈니스 도메인과 핵심 가치**를 자연스럽게 드러내야 합니다. * 각 계층(layer)과 모듈 간의 **의존 관계와 영향 범위**가 명확해야 합니다. * **중복된 로직을 쉽게 찾아내고 제거**할 수 있어야 합니다. * 중요한 비즈니스 로직이 프로젝트 전반에 **얇게 흩어져 있지 않도록** 관리해야 합니다. * 규칙이나 추상화는 **필요 이상으로 복잡하지 않게**, 최소한으로 유지해야 합니다. ### 제어[​](#제어 "해당 헤딩으로 이동") * 새로운 기능을 **빠르게 추가하고**, 발생한 문제를 **쉽게 찾아 해결**할 수 있어야 합니다. * 프로젝트 전체의 개발 흐름을 **계획하고 조정할 수 있는 구조**여야 합니다. * 코드가 **확장하기 쉽고, 유지보수하기 편하며, 필요할 때 제거도 수월**해야 합니다. * 기능 단위(feature 단위)로 **경계와 격리 수준이 명확**해야 합니다. * 컴포넌트나 모듈은 **교체/삭제하기 쉬운 형태**여야 합니다. * [미래의 변경을 과하게 예측해 과도하게 최적화하는 설계는 지양합니다](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 "해당 헤딩으로 이동") 프로젝트를 실제로 개발할 때 사용하는 **도구들에 대한 지식**입니다. * 프로그래밍 언어와 프레임워크 * 라이브러리와 개발 도구 * (선택적으로) 개발 환경, 배포 도구, CI/CD 설정 등 ### 프로젝트 도메인 지식 (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(FSD)에서 사용하는 **표준 네이밍 규칙**을 정리하고,
프로젝트 내 다른 용어들과 충돌할 때 어떻게 다루면 좋을지 설명합니다. ## Feature-Sliced Design의 표준 네이밍[​](#naming-in-fsd "해당 헤딩으로 이동") FSD는 layer와 segment에 대해 다음과 같은 **공통된 네이밍 규칙**을 사용합니다. ### 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`, `page`, `model` 같은 단어를 보았을 때
**지금 이게 FSD의 용어인지, 비즈니스 도메인 용어인지**를 먼저 구분해야 하므로
짧게나마 해석 비용이 추가됩니다. 이런 **용어 충돌은 개발 효율을 떨어뜨릴 수 있습니다.** 따라서 프로젝트 용어집(glossary)에 FSD 특유의 용어가 포함되어 있다면,
팀원뿐 아니라 **비기술적 이해관계자(기획, 디자이너 등)** 와 이야기할 때도
이 용어가 어떤 의미인지 혼동되지 않도록 특히 신경 써야 합니다. ### 용어 사용 가이드[​](#용어-사용-가이드 "해당 헤딩으로 이동") 1. **기술적 커뮤니케이션** * 개발자끼리 FSD 관점에서 이야기할 때는,
가능한 한 **FSD 용어라는 것을 분명히 드러내며** 사용하는 것을 권장합니다. * 예: > “이 기능은 FSD `features` layer로 올리는 게 좋겠습니다.”
“이 부분은 `entities`로 분리하는 쪽이 구조상 더 자연스러워 보여요.” 2. **비기술적 커뮤니케이션** * 비개발자나 비즈니스 이해관계자와의 대화에서는
가능한 한 FSD 관련 용어 사용을 줄이고, **일반적인 비즈니스 언어**를 사용하는 편이 좋습니다. * 예: * “코드 구조” 대신 “기능 단위로 나누어 개발하고 있습니다.” * “`entities` layer” 대신 “사용자/상품 같은 핵심 데이터 단위” 등으로 풀어서 설명 ## 참고 자료[​](#see-also "해당 헤딩으로 이동") FSD 네이밍과 관련된 더 깊은 논의는 아래 토론 스레드들을 참고하세요. * [(토론) 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가 정확히 어떤 상황에서 사용되는지 * 어떤 사용자 작업 범위를 구현하는지 * 다른 작업/Entity와 어떤 연관성이 있는지 결국, **이름을 고민하는 과정에서 애초에 모호했던 작업 자체를 발견해낼 수 있습니다.** > Entity의 이름을 정의하려면,
먼저 그 Entity가 해결할 “작업”이 무엇인지 명확히 이해해야 합니다. ## 어떻게 정의할 것인가?[​](#어떻게-정의할-것인가 "해당 헤딩으로 이동") **Feature가 해결하려는 작업을 정의하려면,
그 작업의 본질을 먼저 파악해야 합니다.** 이 역할은 주로 프로젝트 관리자와 분석가가 담당합니다. *방법론은 그 위에서 **개발자에게 구체적인 방향을 제시**할 뿐입니다.* > *@sergeysova: 프론트엔드는 단순히 “무언가를 화면에 보여주는 것”이 아닙니다.
“왜 이걸 보여줘야 하는가?”를 스스로 묻고, 그 안에서 사용자의 실제 필요를 이해해야 합니다.* 사용자의 필요를 제대로 이해하면,
**제품이 사용자의 목표 달성에 어떻게 도움을 주는지 더 구체적으로 설계**할 수 있습니다. 모든 새로운 작업은 비즈니스와 사용자의 문제를 동시에 다뤄야 하며,
분명한 목적을 가져야 합니다. ***개발자는 자신이 맡은 작업의 목표를 분명히 이해해야 합니다.**
완벽한 프로세스가 없더라도, 관리/기획 담당자와의 커뮤니케이션을 통해 목표를 파악하고
이를 코드로 효과적으로 구현할 수 있어야 합니다.* ## 이점[​](#이점 "해당 헤딩으로 이동") 이런 전체적인 Process를 거쳤을 때 얻을 수 있는 이점을 정리해 봅니다. ### 1. 사용자 작업 이해[​](#1-사용자-작업-이해 "해당 헤딩으로 이동") 사용자의 문제와 비즈니스 요구를 충분히 이해하면,
기술적인 제약 안에서도 **더 나은 해결 방식**을 제안할 수 있습니다. > *이 모든 것은 개발자가 자신의 역할과 목표에
“얼마나 적극적으로 관심을 가지는지”에 달려 있습니다.
그렇지 않다면, 어떤 방법론도 큰 의미를 가지지 못합니다.* ### 2. 구조화와 체계화[​](#2-구조화와-체계화 "해당 헤딩으로 이동") 작업을 제대로 이해하고 나면,
**사고 과정이 자연스럽게 정리되고, 코드 구조도 함께 체계화**됩니다. ### 3. 기능과 그 구성 요소 이해[​](#3-기능과-그-구성-요소-이해 "해당 헤딩으로 이동") 각 Feature는 사용자에게 **명확한 가치를 제공**해야 합니다.
여러 기능이 한 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를 구성하는 명확한 방법 제공**[​](#1-feature-process-entity를-구성하는-명확한-방법-제공 "해당 헤딩으로 이동") * 코드 분할 기준과 명명 규칙 정의 #### 2. **[변화하는 요구사항에 유연한 아키텍처 제공](/kr/docs/about/understanding/architecture.md#adaptability)**[​](#2-변화하는-요구사항에-유연한-아키텍처-제공 "해당 헤딩으로 이동") ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") * [(포스트) 명확한 작업 정의 가이드 (+ 토론)](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](/kr/img/brand/logo-primary.png)](/kr/img/brand/logo-primary.png) | Preferred in most cases | | flat
(#3193FF) | [![logo-flat](/kr/img/brand/logo-flat.png)](/kr/img/brand/logo-flat.png) | For one-color context | | monochrome
(#FFF) | [![logo-monocrhome](/kr/img/brand/logo-monochrome.png)](/kr/img/brand/logo-monochrome.png) | For grayscale context | | square
(#3193FF) | [![logo-square](/kr/img/brand/logo-square.png)](/kr/img/brand/logo-square.png) | For square boundaries | ## Banners & Schemes[​](#banners--schemes "해당 헤딩으로 이동") [![banner-primary](/kr/img/brand/banner-primary.jpg)](/kr/img/brand/banner-primary.jpg) [![banner-monochrome](/kr/img/brand/banner-monochrome.jpg)](/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](/kr/assets/files/choosing-a-layer-en-12fdf3265c8fc4f6b58687352b81fce7.pdf) ![Definitions of all layers and self-check questions](/kr/assets/images/choosing-a-layer-en-5b67f20bb921ba17d78a56c0dc7654a9.jpg) ## Examples[​](#examples "해당 헤딩으로 이동") ### Tweet[​](#tweet "해당 헤딩으로 이동") ![decomposed-tweet-bordered-bgLight](/kr/assets/images/decompose-twitter-7b9a50f879d763c49305b3bf0751ee35.png) ### GitHub[​](#github "해당 헤딩으로 이동") ![decomposed-github-bordered](/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 Linter](https://github.com/feature-sliced/steiger)** 가 있습니다.
또한 CLI나 IDE 확장을 통해 사용할 수 있는 **[FSD 구조 생성 도구](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools)** 도 제공합니다. ### Page Layout / Template은 어디에 보관해야 하나요?[​](#page-layout--template은-어디에-보관해야-하나요 "해당 헤딩으로 이동") 단순한 마크업이라면 `shared/ui`에 두는 것이 일반적입니다.
레이아웃이 간단하다면 **별도 추상화 없이 각 페이지에 직접 작성해도 됩니다.**
복잡한 구조라면 별도 **Widget**이나 **Page**로 분리해 App Router(Nested Routing 포함)에서 조합하세요. ### Feature와 Entity의 차이는 무엇인가요?[​](#feature와-entity의-차이는-무엇인가요 "해당 헤딩으로 이동") | 구분 | 정의 | 예시 | | ----------- | --------------------------------------- | --------------------- | | **Entity** | 애플리케이션이 다루는 **비즈니스 개체** | `user`, `product` | | **Feature** | 사용자가 Entity로 수행하는 **상호작용** | 로그인, 장바구니 담기 | 더 자세한 설명과 코드 예시는 [Slices](/kr/docs/reference/layers.md#entities) 문서에서 확인할 수 있습니다. ### Pages, Features, Entities를 서로 포함할 수 있나요?[​](#pages-features-entities를-서로-포함할-수-있나요 "해당 헤딩으로 이동") 가능합니다. 다만 **상위 Layer**에서만 조합해야 합니다.
예를 들어, Widget 내부에서는 여러 Feature를 **props**나 **children** 형태로 조합할 수 있습니다.
하지만 한 Feature가 다른 Feature를 직접 import 하는 것은 [**Layer Import 규칙**](/kr/docs/reference/layers.md#import-rule-on-layers)에 따라 금지됩니다. ### Atomic Design을 함께 사용할 수 있나요?[​](#atomic-design을-함께-사용할-수-있나요 "해당 헤딩으로 이동") 궁금하다면 [예시](https://t.me/feature_sliced/1653)를 참고하세요.
FSD는 Atomic Design 사용을 **제한하지 않습니다.**
필요하다면 `ui` Segment 안에서 Atomic 분류를 적용할 수 있습니다. ### FSD 관련 참고 자료가 더 있나요?[​](#fsd-관련-참고-자료가-더-있나요 "해당 헤딩으로 이동") 더 다양한 예제와 자료는 [feature-sliced/awesome](https://github.com/feature-sliced/awesome)에서 확인할 수 있습니다. ### Feature-Sliced Design이 필요한 이유는 무엇인가요?[​](#feature-sliced-design이-필요한-이유는-무엇인가요 "해당 헤딩으로 이동") FSD는 프로젝트를 **핵심 기능 단위로 명확하게 구조화**할 수 있도록 돕습니다.
표준화된 구조는 온보딩 속도를 높이고, 폴더 구조에 대한 불필요한 논쟁을 줄여 줍니다.
자세한 배경은 [Motivation](/kr/docs/about/motivation.md) 페이지를 참고하세요. ### 주니어 개발자도 아키텍처 방법론이 필요할까요?[​](#주니어-개발자도-아키텍처-방법론이-필요할까요 "해당 헤딩으로 이동") 필요합니다. 혼자 개발할 때는 구조의 중요성이 잘 느껴지지 않지만,
새로운 팀원이 합류하거나 개발이 일시적으로 중단되더라도, **명확한 구조 덕분에 프로젝트를 쉽게 이어갈 수 있습니다.** ### 인증(Auth) Context는 어떻게 다루나요?[​](#인증auth-context는-어떻게-다루나요 "해당 헤딩으로 이동") 관련 예시는 [Auth 예제 가이드](/kr/docs/guides/examples/auth.md)에서 확인할 수 있습니다. --- # 개요 **Feature-Sliced Design (FSD)** 는 프론트엔드 애플리케이션의 코드를 구조화하기 위한 아키텍처 방법론입니다.
이 방법론의 목적은 **요구사항이 바뀌어도 코드 구조가 무너지지 않고, 새 기능을 쉽게 추가할 수 있는 프로젝트를 만드는 것**입니다.
FSD는 코드를 **얼마나 많은 책임을 가지는지**와 **다른 모듈에 얼마나 의존하는지**에 따라 계층화합니다. FSD는 단순한 폴더 규칙이 아닙니다.
실제 개발 환경에서 구조를 설계하고 유지하기 위한 도구도 함께 제공합니다. * [Steiger](https://github.com/feature-sliced/steiger) — 프로젝트 구조가 FSD 기준에 맞는지 검사합니다. * [Awesome](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools) — FSD 예제와 도구를 모아둔 참고 리스트입니다. * [예제 모음](/kr/examples.md) — 다양한 프로젝트에서 사용된 폴더 구조 예시를 볼 수 있습니다. ## 내 프로젝트에 적합할까요?[​](#is-it-right-for-me "해당 헤딩으로 이동") FSD는 웹, 모바일, 데스크톱 등 **프론트엔드 애플리케이션을 만드는 프로젝트에 잘 어울립니다.**
단순한 라이브러리보다는 **애플리케이션**에 더 적합합니다. 그리고 특정 언어나 프레임워크에 제한이 없고, Monorepo 환경에서도 단계적으로 적용할 수 있습니다. > 지금 구조에 특별한 문제가 없다면 굳이 바꿀 필요는 없습니다.
하지만 다음과 같은 상황이라면 FSD가 도움이 될 수 있습니다: > > * 프로젝트가 커지면서 구조가 얽히고, 유지보수 속도가 느려졌을 때 > * 새로 합류한 팀원이 폴더 구조를 이해하기 힘들어할 때 다만 모든 프로젝트가 FSD에 꼭 맞는 것은 아닙니다.
예시로 각 페이지가 독립적인 특성을 가진 프로젝트에서는 오히려 구조가 복잡해질 수 있습니다.
따라서 도입 전에는 **파일럿 프로젝트로 먼저 검증해보는 것**을 적극 추천합니다. 구조를 전환하기로 했다면 [Migration 가이드](/kr/docs/guides/migration/from-custom.md)를 참고하세요. ## 구조 예시[​](#basic-example "해당 헤딩으로 이동") 간단한 FSD 구조는 다음과 같습니다: ``` - 📁 app - 📁 pages - 📁 shared ``` 이 상위 폴더들이 **Layer**입니다.
Layer는 표준화된 이름을 가지며, 각각 명확한 역할을 담당합니다. ``` - 📂 app - 📁 routes - 📁 analytics - 📂 pages - 📁 home - 📂 article-reader - 📁 ui - 📁 api - 📁 settings - 📂 shared - 📁 ui - 📁 api ``` 📂 pages 내부의 *home*, *article-reader*, *settings*는 **Slice**입니다.
Slice는 비즈니스 도메인(이 예시에서는 각 페이지) 단위로 코드를 구분합니다. 각 Slice 안에는 ui, api, model 등의 **Segment**가 있습니다.
Segment는 코드의 역할이나 기능에 따라 분류됩니다. * **ui** - UI Components * **api** - REST/GraphQL Client, Fetchers * **model** - State, Types, Selectors 예를 들어 UI 구성 요소, 서버 연동 등이 이에 해당합니다.
동일한 구조는 app과 shared Layer에도 적용할 수 있습니다. ## 개념[​](#concepts "해당 헤딩으로 이동") FSD는 다음과 같은 3단계 계층 구조를 따릅니다: ![아래에 설명된 FSD 개념의 계층 구조](/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는 비즈니스 도메인별(user, post, comment)로 나뉘며, 각 Slice 안의 Segment들은 코드의 역할(예: UI, 데이터, 상태) 에 따라 구성됩니다.
예시로 `post` Slice에는 `ui`, `model`, `api` Segment가 포함됩니다. ### Layer[​](#layers "해당 헤딩으로 이동") Layer는 모든 FSD 프로젝트의 표준 최상위 폴더입니다. 1. **App** - Routing, Entrypoint, Global Styles, Provider 등 앱을 실행하는 모든 요소 2. **Processes** - 더 이상 사용되지 않음 3. **Pages** - Route 기준으로 구성된 주요 화면 단위 4. **Widgets** - 크고 독립적으로 동작하는 UI 구성 단위, 일반적으로 하나의 완결된 화면 기능(use case)을 제공합니다. 5. **Features** - 사용자에게 비즈니스 가치를 제공하는 액션을 구현한 재사용 가능한 제품 기능 단위 6. **Entities** - 프로젝트가 다루는 비즈니스 Entity 7. **Shared** - 모든 Layer에서 재사용되는 코드(라이브러리, 유틸리티 등) **App/Shared** Layer는 Slice 없이 Segment로 구성됩니다.
상위 Layer는 자신보다 하위 Layer를 참조 할 수 있지만, 하위 Layer가 상위 Layer를 참조하는 것은 허용되지 않습니다.
예를 들어 pages는 features나 entities의 모듈을 참조할 수 있지만, features가 pages를 참조하는 것은 금지됩니다. ### Slice[​](#slices "해당 헤딩으로 이동") Slice는 Layer 내부를 비즈니스 도메인별로 나눕니다.
이름/개수에 제한이 없으며, 같은 Layer 내 다른 Slice를 참조할 수 없습니다.
이 규칙이 높은 응집도와 낮은 결합도를 보장합니다. ### Segment[​](#segments "해당 헤딩으로 이동") Slice와 App/Shared Layer는 Segment로 세분화되어, 코드의 역할(예: UI, 데이터 처리, 상태 관리 등)에 따라 코드를 그룹화합니다.
일반적으로 다음과 같은 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나 복잡한 기능을 추가하지 않는 것이 좋습니다.
구조를 안정적으로 정리하는 데 집중하는 것이 우선입니다.
자세한 절차는 [Migration 가이드](/kr/docs/guides/migration/from-custom.md)를 참고하세요. ## 다음 단계[​](#next-steps "해당 헤딩으로 이동") * [Tutorial](/kr/docs/get-started/tutorial.md)을 통해 FSD 방식의 사고를 익혀보세요. * 다양한 [예제](/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](/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](/kr/assets/images/realworld-feed-anonymous-8cbba45f488931979f6c8da8968ad685.jpg) *익명 사용자의 관점* ![Authenticated user’s perspective](/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](/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](/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.](/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](/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”.](/kr/assets/images/realworld-article-editor-bc3ee45c96ae905fdbb54d6463d12723.jpg) 제목 필드에 "새 글"이라고 쓰여 있고 나머지 필드는 비어 있는 Conduit 글 편집기. 폼 위에 두 개의 오류가 있습니다. **"이 글이 무엇에 관한 것인지 설명해주세요"**, **"글 본문을 작성해주세요"**. 프로필과 설정 페이지는 글 읽기와 편집기 페이지와 매우 유사하므로, 독자인 여러분의 연습 과제로 남겨두겠습니다 :) --- # Handling API Requests ## Shared API Requests[​](#shared-api-requests "해당 헤딩으로 이동") 공통적으로 사용하는 API 요청 로직은 `shared/api` 폴더에 보관하는 것을 권장합니다.
이렇게 하면 애플리케이션 전체에서 **일관된 방식으로 재사용**할 수 있고,
초기 구현 속도(프로토타이핑)도 빠르게 유지할 수 있습니다. 대부분의 프로젝트는 다음 구조와 `client.ts` 설정만으로 충분합니다. 일반적인 파일 구조 예시: ``` - 📂 shared - 📂 api - 📄 client.ts - 📄 index.ts - 📂 endpoints - 📄 login.ts ``` `client.ts` 파일은 모든 HTTP request 관련 설정을 **한 곳에서** 관리합니다.
즉, 공통 설정을 client에 모아두면 개별 endpoint 로직에서는 **request를 보내는 데만** 집중할 수 있습니다. `client.ts`에서는 다음 항목들을 설정합니다: * 백엔드 기본 URL * Default headers (예: 인증 header) * JSON 직렬화/파싱 아래 예시에서 axios 버전과 fetch 버전 모두 확인할 수 있습니다. * 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` 폴더 안에 API endpoint별 request 함수를 작성합니다.
이렇게 endpoint 단위로 분리해두면 API 변경이 있을 때 유지보수가 매우 쉬워집니다. note 예제를 단순화하기 위해 form handling이나 입력 검증(Zod/Valibot)은 생략했습니다.
필요하다면 아래 문서를 참고하세요:
[Type Validation and Schemas](/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 함수와 타입들을 공개 API로 내보냅니다: 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); } ``` 이 함수는 **로그인 페이지 내부에서만 사용하는 API 요청** 이므로
slice의 public API(`index.ts`)로 다시 export할 필요는 없습니다. note entities layer에는 **backend response 타입**이나 **API 요청 함수**를 직접 두지 마세요.
그 이유는 backend 구조와 `entities` 구조가 서로 **역할과 책임이 다르기 때문입니다**. * `shared/api` 또는 각 slice의 `api` 폴더에서는 **백엔드에서 온 데이터**를 다루고, * `entities`에서는 **프론트엔드 관점에서 필요한 데이터 구조**에 집중해야 합니다.
(예: UI 표시용 데이터, 도메인 로직 처리용 데이터 등) ## 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-입력-수집 "해당 헤딩으로 이동") 이 단계에서는 사용자가 로그인에 필요한 정보를 입력할 수 있는 UI를 준비합니다. > OAuth 로그인만 사용한다면, **2단계(credential 전송)** 에서 별도로 아이디/비밀번호를 보내지 않습니다.
이 경우 바로 [token 저장](#how-to-store-the-token-for-authenticated-requests) 단계로 넘어갑니다. ### 1-1. 로그인 전용 페이지[​](#1-1-로그인-전용-페이지 "해당 헤딩으로 이동") 웹 애플리케이션에서는 일반적으로 **/login** 같은 로그인 Form 전용 페이지를 만들어, 사용자가 **사용자 이름 / 이메일, 비밀번호**를 입력하도록 합니다. 이 페이지는 하는 일이 단순하기 때문에, 추가적인 **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 element와 form submit handler만 포함하도록 해서,
복잡한 비즈니스 로직은 다른 segment로 분리하고 UI는 단순하게 유지합니다. ### 1-2. 로그인 dialog 만들기[​](#1-2-로그인-dialog-만들기 "해당 헤딩으로 이동") 어떤 페이지에서든 공통으로 사용할 수 있는 로그인 dialog가 필요하다면, 이를 **재사용 가능한 widget**으로 구현하는 것이 좋습니다.
**widget**으로 구현하면 페이지마다 로그인 로직을 따로 만들 필요 없이, 필요한 곳에서 동일한 dialog를 불러와 사용할 수 있고,
구조를 과하게 쪼개지 않으면서도 재사용성을 확보할 수 있습니다. ``` - 📂 widgets - 📂 login-dialog - 📂 ui - 📄 LoginDialog.tsx - 📄 index.ts - other widgets… ``` > 이후 설명은 **로그인 전용 페이지** 를 기준으로 진행하지만,
여기서 다루는 원칙은 login dialog widget에도 동일하게 적용됩니다. ### 1-3. Client-side Validation[​](#1-3-client-side-validation "해당 헤딩으로 이동") 회원가입 페이지에서 잘못된 입력을 즉시 알려주면 UX가 훨씬 좋아집니다.
이를 위해 client-side validation을 적용할 수 있습니다. 검증 규칙은 `pages/login/model` segment에 schema 형태로 정의하고,`ui` segment에서는 이 schema를 불러와 재사용합니다.
아래 예시는 [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를 사용해 form으로부터 받은 데이터를 검증할 수 있습니다: 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 * 기타 state 관리/요청 로직 즉, **어디에서나 재사용 가능한 로그인 요청 함수** 를 만든다고 보면 됩니다. ### 2-1. 함수 placement[​](#2-1-함수-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` 등)를 감싼 공용 API client로,
**기본 URL, 공통 헤더, request/response 직렬화** 등을 처리합니다. 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에-저장하기 "해당 헤딩으로 이동") 로그인 request가 로그인 페이지에서만 사용된다면,
해당 페이지의 api segment에 login 함수를 두는 것도 가능합니다. ``` - 📂 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)[​](#two-factor-auth-2fa "해당 헤딩으로 이동") 2단계 인증(2FA)을 사용하는 경우에는 로그인 플로우에 한 단계가 더 추가됩니다. 1. `/login` 응답에 `has2FA` 플래그가 있으면, `/login/2fa` 페이지로 redirect 합니다. 2. 2FA 페이지와 관련 API들은 모두 `pages/login` slice에 함께 둡니다. 3. `/2fa/verify`와 같이 별도의 endpoint를 호출하는 함수는 `shared/api` 또는 `pages/login/api`에 배치합니다. 이렇게 하면, 일반 로그인과 2FA 관련 로직을 **login slice 내부**에 모아둘 수 있습니다. ## Authenticated Requests를 위한 token 저장[​](#how-to-store-the-token-for-authenticated-requests "해당 헤딩으로 이동") 로그인, 비밀번호 변경, OAuth, 2단계 인증 등 어떤 방법으로 인증을 하든,
인증 API 호출의 **응답(response)** 으로 보통 token이 함께 내려옵니다. 이 token을 어딘가에 저장해 두면,
이후 **모든 인증이 필요한 API 요청(request)** 에 token을 자동으로 포함시켜 백엔드 인증을 통과할 수 있습니다. 웹 애플리케이션에서 token을 저장하는 방법 중 **가장 권장되는 방식은 cookie**입니다. cookie를 사용하면, 브라우저가 요청마다 token을 자동으로 넣어 주기 때문에
프론트엔드에서 token을 직접 관리할 필요가 거의 없습니다.
따라서 프론트엔드 아키텍처 차원에서 신경 쓸 부분이 크게 줄어듭니다. 사용 중인 프레임워크가 서버 사이드 기능을 제공한다면(예: [Remix](https://remix.run)),
서버 측 cookie 관련 로직을 shared/api에 두는 것을 권장합니다. Remix에서의 구현 예시는 [튜토리얼의 Authentication 섹션](/kr/docs/get-started/tutorial.md#authentication)을 참고하면 됩니다. 하지만 cookie를 사용할 수 없는 환경도 있습니다.
이 경우에는 token을 클라이언트에서 직접 저장하고, token 만료를 감지하고,
refresh token을 사용해 새 token을 발급받고 기존 요청을 다시 실행하는 등의 로직을 함께 구현해야 합니다. FSD에서는 여기서 한 가지 추가 고민이 필요합니다. token을 **어느 layer 또는 어느 segment에** 저장할지,
그렇게 저장한 token을 앱 전역에서 **어떻게** 사용할 수 있게 할지에 따라 전체 구조가 달라지기 때문입니다. ### 3-1. Shared[​](#3-1-shared "해당 헤딩으로 이동") Shared layer에 token을 두는 방식은 shared/api에 정의된 **공용 API 클라이언트**와 자연스럽게 결합되는 패턴입니다. token을 module scope나 어떤 reactive store에 저장해 두면,
인증이 필요한 다른 API 함수에서 이 token을 **그대로 참조**해 사용할 수 있습니다. token 자동 재발급(refresh)은 API client의 **middleware**에서 담당합니다. 1. 로그인 시 **access token, refresh token**을 저장합니다. 2. 인증이 필요한 request를 보냅니다. 3. 응답에서 token 만료 코드를 받으면, refresh token으로 새 token을 발급해 저장한 뒤 실패한 request을 동일하게 다시 시도합니다. #### Token 관리 분리 전략[​](#token-관리-분리-전략 "해당 헤딩으로 이동") * **전담 segment 부재**
token 저장과 재발급 로직이 request 로직과 같은 파일에 뒤섞여 있으면, 코드가 많아질수록 유지보수가 점점 어려워집니다.
이런 경우에는 **request 함수와 client는 `shared/api`에 두고**,
**token 관리 로직은 `shared/auth` segment로 분리**하는 방식을 권장합니다. * **token과 사용자 정보를 함께 받는 경우**
백엔드가 token과 동시에 **현재 사용자 정보**를 반환하는 API를 제공하는 경우도 있습니다.
이때는 다음 두 가지 방식 중 하나로 처리할 수 있습니다. 1. 별도 store에 함께 저장하거나 2. `/me`·`/users/current` 같은 endpoint를 따로 호출해 user 정보를 가져올 수 있습니다. ### 3-2. Entities[​](#3-2-entities "해당 헤딩으로 이동") FSD 프로젝트에서는 보통 **User entity**(또는 **Current User entity**)를 두는 경우가 많습니다.
두 entity를 하나로 합쳐서 사용하는 것도 전혀 문제 없습니다. note **Current User**는 `viewer` 또는 `me`라고 부르기도 합니다.
이는 권한과 개인 정보가 있는 **현재 로그인한 단일 사용자**와,
공개적으로 표시되는 **여러 사용자 목록**을 구분하기 위해 쓰는 이름입니다. #### Token을 User Entities에 저장하기[​](#token을-user-entities에-저장하기 "해당 헤딩으로 이동") User entity의 model segment에 **reactive store**를 만들고,
이곳에 token과 user 객체를 함께 보관할 수 있습니다. 이렇게 하면: **현재 로그인한 사용자 정보** 와 **그 사용자가 가진 token**을 한 곳에서 관리할 수 있어서,
인증과 관련된 비즈니스 로직을 작성할 때 구조를 이해하기 쉬워집니다. 다만 API client는 보통 shared/api에 정의되거나,
여러 entity에 분산되어 있는 경우가 많습니다. 따라서 layer의 import 규칙([import rule on layers](/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 — 권장하지 않음[​](#3-3-pages--widgets--권장하지-않음 "해당 헤딩으로 이동") 다음과 같은 이유로 page layer나 widget layer에 token을 저장하는 것은 권장하지 않습니다. page, widget layer에 token을 두면 전역에서 이 token에 의존하게 되는데,
이렇게 되면 다른 slice에서 재사용하기 어렵고, 구조가 쉽게 얽힙니다. 따라서 token 저장 위치는 Shared 또는 Entities 중 하나로 결정하는 것을 권장합니다. ## 4. Logout & Token Invalidation[​](#4-logout--token-invalidation "해당 헤딩으로 이동") ### 로그아웃과 token 무효화[​](#로그아웃과-token-무효화 "해당 헤딩으로 이동") 대부분의 애플리케이션에는 **로그아웃 전용 페이지**는 따로 두지 않습니다.
대신, 어느 화면에서든 호출할 수 있는 로그아웃 기능을 두는 것이 일반적입니다. 로그아웃은 일반적으로 다음 두 단계로 이루어집니다. 1. 백엔드에 인증된 로그아웃 request 보내기 (예: `POST /logout`) 2. token store reset (access token / refresh token 모두 제거) > 모든 API request을 shared/api에 모아 관리하고 있다면,
로그아웃 API는 login() 근처, 예를 들어 shared/api/endpoints/logout.ts에 두는 것이 자연스럽습니다. > > 반대로 특정 UI(예: Header)에만 로그아웃 버튼이 있고,
그곳에서만 이 API를 호출한다면 widgets/header/api/logout.ts처럼
버튼이 위치한 widget 근처에 두는 것도 가능합니다. token store reset은 실제로 로그아웃 버튼을 가진 UI에서 트리거됩니다.
로그아웃 request와 store reset을 같은 widget의 model segment에 함께 두어도 됩니다. ### 자동 로그아웃[​](#자동-로그아웃 "해당 헤딩으로 이동") 다음과 같은 경우에는 반드시 token store를 초기화해야 합니다. * 로그아웃 request가 실패했을 때 * 로그인 token 갱신(`/refresh`)이 실패했을 때 이 상황에서 token이 그대로 남아 있으면,
화면 상으로는 **로그인된 것처럼** 보이지만 실제로는 대부분의 요청이 실패하는 애매한 상태가 될 수 있습니다. > token을 Entities(User)에 보관했다면,
해당 entity의 model segment에 token 초기화 코드를 두는 것이 좋습니다.
Shared layer에서 token을 관리한다면, 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** 예시를 살펴보겠습니다.
이 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 내용은 **외부에서 주입(의존성 주입)** 받을 수 있도록 합니다. 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; } ``` 위 예시에서 사이드바 UI 자체 구현 코드는 길어질 수 있으므로, 설명에서는 생략했습니다.
중요한 포인트는 layout이 **틀만 제공하고, 구체적인 내용은 props로 받아서 렌더링한다** 는 점입니다. ## layout에 widget 적용하기[​](#layout에-widget-적용하기 "해당 헤딩으로 이동") layout 컴포넌트에서 인증 처리나 데이터 로딩 같은 **비즈니스 로직**을 수행해야 할 때가 있습니다.
예를 들어, [React Router](https://reactrouter.com/)의 deeply nested routes 구조에서는 `/users`, `/users/:id`, `/users/:id/settings` 처럼
공통된 URL prefix를 가진 여러 child routes가 존재합니다. 이 경우, 인증 확인이나 공통 데이터 로딩 같은 로직을
각 페이지마다 작성하기보다는 **layout 레벨에서 한 번에 처리**하는 방식이 훨씬 효율적입니다. 다만 이런 layout을 shared나 widgets 폴더에 두면 [layer에 대한 import 규칙](/kr/docs/reference/layers.md#import-rule-on-layers)을 위반할 수 있습니다. > Slice의 module은 자신보다 **하위 layer**에 있는 Slice만 import할 수 있습니다. 즉, layout에서 entity/feature/page를 직접 불러오게 되면
**위에서 아래를 가져오는** 잘못된 의존성이 생길 수 있습니다. 그래서 먼저 아래와 같은 점을 고려하는 것이 좋습니다. * *이 layout이 정말 필요한가?* * *꼭 widget 형태로 만들 필요가 있는가?* layout이 적용되는 페이지 수가 2\~3곳 정도라면,
이 layout이 사실상 **특정 페이지만을 위한 wrapper**일 수도 있으며 굳이 widget으로 승격시킬 필요가 없을 수 있습니다. 이런 상황에서는 아래 두 가지 대안을 먼저 고려하세요. 1. **App layer에서 inline으로 작성하기**
URL 패턴이 공통된 여러 경로를 Router의 nesting 기능으로 묶어 하나의 route group으로 만들 수 있습니다.
이 route group에 layout을 한 번만 지정하면, 해당 그룹 아래 모든 페이지에 자동으로 동일한 layout이 적용됩니다. 2. **코드 복사 & 붙여넣기**
layout은 자주 변경되는 코드가 아니므로, 필요한 페이지만 layout 코드를 복사해 사용해도 큰 문제가 없습니다.
수정이 필요할 때만 해당 layout들을 개별적으로 업데이트하면 되고, 페이지 간 관계를 주석으로 남겨 두면 누락을 방지할 수 있습니다. *** 위 방법들이 프로젝트에 맞지 않다면, layout 안에서 widget을 사용하는 다음 두 가지 해결책을 고려할 수 있습니다. ### 1. Render Props 또는 Slots 사용하기[​](#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 틀을 제공하고**,
자식 컴포넌트가 전달한 UI를 layout 내부 특정 위치에 **주입(injection)** 하는 구조입니다.
Layout이 비즈니스 로직을 직접 수행하면서도, UI 구성은 외부에서 유연하게 가져올 수 있다는 장점이 있습니다. ### 2. layout을 App layer로 이동하기[​](#2-layout을-app-layer로-이동하기 "해당 헤딩으로 이동") layout을 app/layouts 같은 상위 layer로 옮기면,
App layer는 아래 layer(entities, features, shared)를 자유롭게 import할 수 있기 때문에
Layer 규칙을 위반하지 않고 layout 안에서 widget을 사용할 수 있습니다. ## 참고 자료[​](#참고-자료 "해당 헤딩으로 이동") React 및 Remix(React Router와 구조가 유사)의
인증 layout 구현 예시는 [튜토리얼](/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에 다음 내용을 명확히 적어 두세요. * 우리 팀에서 **유틸리티 타입**이라고 부르는 기준 * 어떤 타입을 추가/제외할지에 대한 규칙 > 유틸리티 타입의 **재사용 가능성**을 과대평가하지 마세요.
**재사용 가능하다**는 이유만으로 꼭 전역(`shared`)에 둘 필요는 없습니다. 유틸리티 타입은 아래처럼 **실제 사용되는 위치 근처**에 두는 것이 오히려 유지보수에 유리한 경우가 많습니다. ``` - 📂 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 layer에 두면**,
이러한 상호 참조 관계를 한곳에서 관리할 수 있어서 유지보수가 훨씬 쉬워집니다. 반대로 이 Request 함수를 `entities/song/api` 내부에 두면 다음과 같은 문제가 생깁니다. `entities/artist` slice에서 `Song` 타입을 **참조하고 싶어도**,
FSD의 [layer별 import 규칙](/kr/docs/reference/layers.md#import-rule-on-layers) 때문에 **동일 layer 간(import)** 의존은 금지됩니다. * 규칙 요약: > *“한 slice의 모듈은 자신보다 **아래 layer**에 있는 slice만 import할 수 있다.”* 즉, 같은 layer에 있는 entity끼리는 직접 cross-import 할 수 없기 때문에 **Artist → Song** 의존을 바로 연결하기가 어렵습니다.
이런 경우에는 제네릭 타입 매개변수를 사용하거나, `@x` Public API 같은 패턴을 사용해 우회하는 전략이 필요합니다. ### 2. 상호 참조 해결 전략[​](#2-상호-참조-해결-전략 "해당 헤딩으로 이동") entity끼리 서로를 참조해야 할 때 사용할 수 있는 대표적인 전략은 다음 두 가지입니다. #### 1. 제네릭 타입 매개변수화[​](#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) 활용)[​](#2-cross-import-public-apix-활용 "해당 헤딩으로 이동") 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`와 매우 비슷하지만,
**artist에서 사용할 수 있는 부분**만 노출하는 역할을 합니다. entities/song/@x/artist.ts ``` export type { Song } from "../model/song.ts"; ``` 이렇게 분리해 두면 `📄 entities/artist/model/artist.ts`에서 `Song`을 가져올 때,
다음과 같이 **의존 대상이 명확한 import**를 사용할 수 있습니다. 이 방식은 entity들의 의존 관계를 코드 구조 상에서 명확하게 보여 주고, 도메인 간 분리를 유지하는 데 도움이 됩니다. entities/artist/model/artist.ts ``` import type { Song } from "entities/song/@x/artist"; export interface Artist { name: string; songs: Array; } ``` ## 데이터 전송 객체와 mappers[​](#data-transfer-objects-and-mappers "해당 헤딩으로 이동") 데이터 전송 객체(Data Transfer Object, DTO)는 **백엔드에서 전달되는 데이터 구조 그대로를 표현한 타입**입니다. 간단한 경우에는 DTO를 프론트엔드에서 그대로 사용해도 되지만, 실제 UI나 도메인 로직에서는 다루기 불편한 경우도 많습니다.
이럴 때 `mapper`를 사용해 DTO를 **프론트엔드 친화적인 형태**로 변환합니다. ### 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/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와 DTO가 `shared/api`에 있다면 → mapper도 `shared/api`에 둡니다.
Request와 store가 `entity slice` 내부에 있다면 → mapper도 해당 slice 안에 두되,
layer 간 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가 함께 포함되는 경우도 있습니다.
예를 들어 곡 정보에 저자(Author) 객체 전체가 포함되는 식입니다. 이럴 때 entity들끼리는 **서로의 존재를 완전히 모른 채** DTO 안에서만 연결될 수도 있습니다. 이 경우 간접 연결(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의 관계를 코드 상에서 명확하게 드러낼 수 있다는 장점이 있습니다. 즉, 나중에 둘 중 하나를 수정할 때 **연결된 entity까지 함께 리팩토링해야 한다는 사실**을 더 쉽게 인지할 수 있습니다. ## Global 타입과 Redux[​](#global-타입과-redux "해당 헤딩으로 이동") Global 타입은 애플리케이션 전역에서 사용되는 타입을 말하며, 크게 두 가지 종류로 나눌 수 있습니다. 1. 애플리케이션에 특화되지 않은 **제너릭 타입** 2. 애플리케이션 전체가 알고 있어야 하는 **전역 도메인 타입** ### 1) 제너릭 타입[​](#1-제너릭-타입 "해당 헤딩으로 이동") 첫 번째 경우(특정 도메인에 묶이지 않은 제너릭 타입)는 `Shared` 폴더 안의 적절한 segment에 배치하면 됩니다.
예를 들어, **분석(analytics) 관련 전역 인터페이스**라면 `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 규칙](/kr/docs/reference/layers.md#import-rule-on-layers)에 의해 App layer에 있는 `RootState`, `AppDispatch` 타입을 바로 가져올 수 없습니다. > 한 slice의 module은 자신보다 하위 layer에 있는 slice만 import할 수 있습니다. #### 권장 해결책[​](#권장-해결책 "해당 헤딩으로 이동") 이 경우에는 **Shared ↔ App layer 간에 한정된 암묵적 의존성을 허용**하는 것이 현실적인 해결책입니다.
`RootState`, `AppDispatch` 타입은 자주 바뀌지 않고, 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 "해당 헤딩으로 이동") enum 타입은 다음 원칙에 따라 배치하는 것을 권장합니다. * 가능한 한 **가장 가까운 사용 위치**에 정의합니다. * 어떤 segment에 둘지는 **용도 기준**으로 결정합니다. * UI toast 상태를 표현하는 enum → `ui` segment * 백엔드 Response 상태를 표현하는 enum → `api` segment 프로젝트 전역에서 공통으로 쓰는 값(예: Response 상태, 디자인 토큰 등)은 `Shared` layer에 두고,
역할에 따라 `api`, `ui` 등 적절한 segment를 선택합니다. ## 타입 검증 Schema와 Zod[​](#타입-검증-schema와-zod "해당 헤딩으로 이동") 데이터의 형태와 제약 조건을 검증하려면 [Zod](https://zod.dev) 같은 라이브러리로 **validation schema**를 정의합니다. schema의 위치는 **어디에서 쓰이는 데이터인지**에 따라 결정합니다. * 백엔드 Response 검증 → `api` segment 근처 * 폼 입력 값 검증 → `ui` segment (또는 복잡한 경우 `model` segment) 검증 schema는 DTO를 받아 파싱하고, schema와 맞지 않으면 즉시 에러를 던집니다.
([Data transfer objects and mappers](#data-transfer-objects-and-mappers) 섹션도 참고하세요.) 특히 백엔드 Response가 예상한 schema와 일치하지 않을 때 request를 실패시키도록 구현하면,
버그를 비교적 이른 시점에 발견할 수 있습니다. 이 때문에 검증 schema는 보통 `api` segment에 두는 편이 일반적입니다. ## Component props, context 타입[​](#component-props-context-타입 "해당 헤딩으로 이동") 일반적으로 Component의 props 타입과 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.md`에 다음 내용을 함께 기록해 두는 것을 추천합니다. * 이 폴더에 있는 파일들의 용도 * 타입을 재생성하는 방법 (스크립트 명령어 등) --- # 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.tsx` 파일을 추가해 **진입점(entry point)** 를 노출합니다. note 이 단계에서는 **Page A에서 Page B의 코드를 import**해도 괜찮습니다.
나중 단계에서 이러한 의존성을 분리할 예정이니, 우선 **페이지 폴더를 만드는 것**에 집중하세요. **📁 Route File** 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 "해당 헤딩으로 이동") **📁 src/shared 폴더를 만들고,** 📁 pages 또는 📁 routes를 import하지 않는 모든(파일)은 이 폴더로 모읍니다.
**📁 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 "해당 헤딩으로 이동") 한 페이지가 다른 페이지의 코드를 직접 import하고 있다면, 아래 두 가지 방식 중 하나로 의존성을 정리합니다. | 방법 | 사용 시점 | | ----------------------------------- | -------------------------------------------------------------- | | **A. 코드 복사하여 독립시키기** | 페이지별로 로직이 달라질 가능성이 높거나, 재사용성이 낮은 경우 | | **B. Shared로 이동하여 공통화하기** | 여러 페이지에서 반복적으로 사용되는 경우 | * Shared 이동 위치 예시 * UI 구성 요소 → `📁 shared/ui` * 설정 상수   → `📁 shared/config` * 백엔드 호출  → `📁 shared/api` note 코드를 복사하는 것은 잘못이 아닙니다.
경우에 따라서는 **중복을 허용하더라도 페이지 간 의존성을 줄이는 것**이 더 중요합니다.
다만, 비즈니스 로직처럼 변경 가능성이 큰 핵심 부분은 중복을 피하고, 복사할 때에도 가능한 한 DRY 원칙을 고려합니다. ## 4단계: Shared Layer 정리하기[​](#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. **각 페이지 내부**에서, 필요한 `segment(ui, model, api 등)`를 구성합니다. 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**를 표현합니다.
이러한 slice는 **Entities layer**로 이동하며, **entity**마다 별도의 폴더를 구성합니다.
반대로, 댓글 작성처럼 **사용자의 특정 행동(action)** 을 중심으로 한 **slice**는 **Features layer**로 옮겨 독립적으로 관리합니다. **Entities**와 **Features**는 서로 의존하지 않고 사용할 수 있도록 설계해야 합니다.
Entity 간의 관계가 필요하다면 [Business-Entities Cross-Relations 가이드](/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)** 입니다.
v2는 기존 장점을 유지하는 동시에 이러한 문제들을 보완하도록 설계되었습니다.
또한 [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** 라는 세 가지 개념만 이해하면 구조적 결정을 쉽게 내릴 수 있습니다.
덕분에 신규 팀원이 **어디에 어디에 둘지** 고민할 필요가 줄어들어 온보딩 속도가 크게 향상됩니다. ### 유연한 모듈화[​](#유연한-모듈화 "해당 헤딩으로 이동") * **독립 영역**은 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 의존 규칙](https://t.me/atomicdesign/18708)을 명확히 설정할 수 있도록 돕습니다.
**상위 layer**는 더 넓은 **context**를 제공하며, **하위 layer**는 더 낮은 **변경 리스크와 높은 재사용성**을 갖습니다. ### 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 권장 네이밍과 이전 명칭 간의 대응 관계입니다.
아래에서는 v2 권장 layer·segment 명칭을 이전 명칭과 대응하여 정리했습니다.
추상화/네이밍 관련 상세 가이드는 [명확한 네이밍 권장사항](/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 원칙](/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이기 때문입니다. v2.1의 핵심 원칙은 다음과 같습니다: * Page 내부에 주요 UI와 비즈니스 로직을 배치합니다. * Shared layer에는 순수 재사용 요소만 유지합니다. * 여러 Page에서 실제로 공유되는 로직만 Feature/Entity로 분리합니다. 이 접근 방식의 장점 1. **Page가 명확한 책임 단위**가 되어, 코드의 역할이 분명해집니다. 2. **Shared** layer가 불필요하게 비대해지는 것을 방지해 의존성이 간결해집니다. 3. 공통 로직을 실제로 재사용할 때만 분리하므로 과도한 추상화가 줄어듭니다. 또한 v2.1에서는 **Entity 간 cross-import**를 위한 `@x` 표기법이 **표준화**되었습니다.
이를 통해 import 경로를 더 명확하고 일관되게 관리할 수 있습니다. ## 마이그레이션 프로세스[​](#how-to-migrate "해당 헤딩으로 이동") v2.1은 하위 호환성을 제공합니다.
즉, 기존 v2.0 프로젝트는 **수정 없이** 그대로 동작합니다.
다만 v2.1의 구조적 장점을 활용하려면 아래 단계를 차례로 적용하면 됩니다. ### 1. Slice 병합[​](#1-slice-병합 "해당 헤딩으로 이동") v2.1 Page-First 모델에서는 **여러 Page에서 재사용되지 않는** slice는 Page 내부로 병합하는 것을 권장합니다.
이렇게 하면 코드 탐색이 빨라지고 유지보수 비용도 줄어듭니다. #### Steiger로 자동 탐지하기[​](#steiger로-자동-탐지하기 "해당 헤딩으로 이동") 프로젝트 루트에서 [Steiger](https://github.com/feature-sliced/steiger)를 실행하면 v2.1 mental model에 맞추어 slice 사용 여부를 자동으로 분석해줍니다: ``` npx steiger src ``` * [`insignificant-slice`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice)
단일 Page에서만 사용되는 slice를 탐지합니다. → **Page 내부로 병합하는 것을 권장**합니다. * [`excessive-slicing`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/excessive-slicing)
지나치게 잘게 나뉜 slice를 찾아줍니다. → **유사한 slice를 통합하거나 그룹화하여 탐색성**을 높입니다. 이 명령으로 `한 번만 쓰이는 slice` 목록이 출력됩니다.
이제 각 slice의 재사용 여부를 검토하고, 과하다면 해당 page로 병합하거나 비슷한 역할끼리 묶어 보세요. Slice 관리 Slice는 해당 layer의 namespace를 구성하는 요소입니다.
전역 변수를 최소화하듯, slice도 실제로 재사용되는 경우에만 독립 단위로 유지하세요. 한 곳에서만 쓰이는 slice → Page 또는 Feature 내부로 이동
여러 Page에서 재사용되는 slice → 그대로 유지 ### 2. Cross Import 표준화[​](#2-cross-import-표준화 "해당 헤딩으로 이동") v2.1에서는 Entity 간 cross-import를 위해 `@x-` 표기법을 사용합니다. entities/B/some/file.ts ``` // v2.1 권장 방식 import type { EntityA } from "entities/A/@x/B"; ``` 자세한 내용은 [Public API for cross-imports](/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나 내부 문서에 반드시 기록해야 합니다.
이 내용을 [프로젝트 지식](/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](/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](/kr/llms.txt) * [llms-full.txt](/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는 **이 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.](/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.](/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를 반드시 사용해야 하는 것은 아닙니다.
**필요한 경우에만** Layer를 추가하세요.
대부분의 프론트엔드 프로젝트는 보통 최소한 `shared`, `page`, `app` 정도는 사용합니다. 실무에서는 폴더명을 보통 소문자로 작성합니다. (예: `📁 shared`, `📁 page`, `📁 app`)
또한, **새로운 Layer를 직접 정의해서 사용하는 것은 권장하지 않습니다.**
(각 Layer의 역할이 이미 표준으로 충분히 정리되어 있기 때문입니다.) ## Import 규칙[​](#import-규칙 "해당 헤딩으로 이동") 각 Layer는 여러 개의 **Slice(서로 밀접하게 연관된 모듈 묶음)** 로 구성됩니다.
Slice들 사이의 연결은 **Layer Import 규칙**을 통해 제한합니다. > **규칙:**
하나의 Slice 안에서 작성된 코드는
**자신이 속한 Layer보다 아래 Layer**에 있는 *다른 Slice*만 import할 수 있습니다. 예를 들어, `📁 ~/features/aaa/api/request.ts` 파일은 다음과 같습니다. * 같은 Layer의 `📁 ~/features/bbb` → **import 불가능** * 더 아래 Layer(`📁 ~/entities`, `📁 ~/shared`) → **import 가능** * 같은 Slice(`📁 ~/features/aaa/lib/cache.ts`) → **import 가능** `app`과 `shared`는 조금 특이한 Layer입니다.
두 Layer는 **Layer이면서 동시에 하나의 큰 Slice처럼 동작**하고 내부 구조는 **Segment**로 나뉩니다. 이 경우에는 Layer 내부에서 Segment끼리는 자유롭게 import할 수 있습니다.
(`shared`는 비즈니스 도메인이 없고, `app`은 모든 도메인을 묶는 상위 조정자 역할을 합니다.) ## Layer별 역할[​](#layer별-역할 "해당 헤딩으로 이동") 이제 각 Layer가 어떤 의미를 가지는지,
그리고 보통 어떤 종류의 코드가 해당 Layer에 들어오는지 정리해 보겠습니다. ### Shared[​](#shared "해당 헤딩으로 이동") Shared Layer는 앱의 **기본 구성 요소와 기반 도구들을 모아두는 곳**입니다.
백엔드, 서드파티 라이브러리, 실행 환경과의 연결,
그리고 여러 곳에서 사용하는 **응집도 높은 내부 라이브러리**가 여기에 위치합니다. `app`과 마찬가지로 **Slice 없이 Segment로만 구성**합니다.
비즈니스 도메인이 없기 때문에, **Shared 내부의 파일들은 서로 자유롭게 import**할 수 있습니다. Segment 예시: * `📁 api` — API 클라이언트와 공통 백엔드 요청 함수 * `📁 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 "해당 헤딩으로 이동") Entities Layer는 프로젝트에서 다루는 **핵심 비즈니스 개념**을 표현합니다.
대부분의 경우, 실제 도메인 용어(예: `User`, `Post`, `Product`)와 일치합니다. 각 Entity Slice에는 다음과 같은 것들을 포함할 수 있습니다. 구성: * `📁 model` — 데이터 상태, 도메인 로직, 검증 스키마 * `📁 api` — 해당 Entity와 관련된 API 요청 * `📁 ui` — Entity의 시각적 표현 * 완성된 큰 UI 블록이 아니어도 됩니다. * 여러 페이지에서 재사용 가능한 형태로 설계합니다. * 비즈니스 로직은 가능하면 props/slot으로 외부에서 주입하는 방식을 권장합니다. #### Entity 간 관계[​](#entity-간-관계 "해당 헤딩으로 이동") 원칙적으로는 Entity Slice끼리는 서로 **서로를 모르는 상태**가 이상적입니다.
하지만 실제 애플리케이션에서는 한 Entity가 다른 Entity를 **포함하거나**
여러 Entity가 서로 **상호작용**하는 일이 자주 발생합니다. 이런 경우, 두 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](/kr/docs/reference/public-api.md#public-api-for-cross-imports) 문서를 참고하세요. ### Feature[​](#feature "해당 헤딩으로 이동") Features Layer에는 **사용자가 애플리케이션에서 수행하는 주요 기능**이 들어갑니다.
보통 하나 이상의 Entity와 연관되어 동작합니다. * 모든 동작을 무조건 Feature로 만들 필요는 없습니다. * **여러 페이지에서 재사용되는 기능**일 때 Feature로 추출하는 것을 고려하세요. * 예: 여러 종류의 에디터에서 동일한 댓글 기능을 사용한다면, `comments`를 Feature로 만들 수 있습니다. * Feature가 너무 많아지면, 중요한 기능이 어디 있는지 찾기 어려워질 수 있습니다. 구성: * `📁 ui` — 상호작용 UI (예: 폼, 검색 바 등) * `📁 api` — 해당 기능과 직접 관련된 API 요청 * `📁 model` — 검증 로직, 내부 상태 관리 * `📁 config` — Feature Flag 등 기능별 설정 > 새로운 팀원이 프로젝트에 합류했을 때,
Page와 Feature만 훑어봐도 **이 앱이 어떤 기능을 제공하는지**를 대략 이해할 수 있도록 구성하는 것이 목표입니다. ### Widget[​](#widget "해당 헤딩으로 이동") Widgets Layer는 **독립적으로 동작하는 비교적 큰 UI 블록**을 두는 곳입니다.
여러 페이지에서 재사용되거나, 한 페이지에서 **큰 섹션 단위로 나누어지는 UI 블록**이 있을 때 유용합니다. tip 재사용되지 않고 특정 페이지의 핵심 콘텐츠에만 쓰인다면, 굳이 Widget으로 분리하지 말고 Page 내부에 두는 것이 좋습니다.
Nested Routing(예: [Remix](https://remix.run)) 환경에서는 Widget이 **Page와 비슷한 역할**을 할 수 있습니다.
예를 들어 데이터 로딩, 로딩 상태 표시, 에러 처리 등을 모두 포함하는 **하나의 라우터 단위 UI 블록**으로 동작할 수 있습니다. ### Page[​](#page "해당 헤딩으로 이동") Pages Layer는 웹/앱에서 보이는 **화면(screen) 또는 액티비티(activity)** 에 해당합니다.
일반적으로 “페이지 1개 = Slice 1개” 구조를 많이 사용하지만,
구조가 유사한 페이지들은 하나의 Slice로 묶는 것도 가능합니다. 코드를 찾기만 쉽다면, Page Slice의 크기에 특별한 제한은 없습니다.
재사용되지 않는 UI는 그대로 Page 내부에 두면 됩니다.
Page Layer에는 보통 전용 model이 없으며, 필요한 경우 간단한 상태만 컴포넌트 내부에서 관리합니다. 구성 예: * `📁 ui` — 페이지 UI, 로딩 상태, 에러 상태 처리 * `📁 api` — 페이지에서 사용하는 데이터 패칭/변경 요청 ### Process[​](#process "해당 헤딩으로 이동") caution **Deprecated** — 기존에 사용하던 코드는 가능하면 Feature나 App Layer로 이동하세요. 과거에는 여러 페이지를 넘나드는 복잡한 기능을 처리하기 위한 **탈출구 같은 Layer**로 사용되었습니다.
하지만 역할이 모호하고, 대부분의 애플리케이션에서는 굳이 사용하지 않아도 충분히 설계가 가능합니다. 라우터, 서버 연동 같은 전역적인 로직은 보통 App Layer에 둡니다.
App Layer가 너무 복잡해질 때 정말 필요한 경우에만 제한적으로 고려할 수 있습니다. ### App[​](#app "해당 헤딩으로 이동") App Layer는 앱 전역에서 동작하는 **환경 설정**과 **공용 로직**을 관리하는 곳입니다.
예를 들어 라우터 설정, 전역 상태 관리(Store 설정), 글로벌 스타일 앱 진입점(Entry Point) 설정 등과 같이
**앱 전체에 영향을 주는 코드**가 위치합니다.
`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` 파일에서 `LoginPage`, `RegisterPage` 등을 다시 내보내는 방식입니다. 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의 인터페이스를 흐리게 만들고, 나중에 큰 부담이 됩니다. 예를 들어 `features/comments/index.js`에서 `./ui/Comment` 전체를 그대로 export 하거나
`./model/comments` 내부 모델을 통째로 export 하는 식입니다. Bad practice, features/comments/index.js ``` // ❌ 이렇게 하지 마세요 export * from "./ui/Comment"; // 👎 무분별한 UI export export * from "./model/comments"; // 💩 내부 모델 노출 ``` 이 방식이 문제가 되는 이유: * **발견 가능성 저하** * Public API에서 어떤 기능을 제공하는지 한눈에 파악하기 어렵습니다. * **내부 구현 노출** * 원래 외부에서 알 필요가 없는 내부 코드를 외부에서 직접 사용하게 되고,
이 코드에 대한 의존성이 생기면 리팩터링이 매우 어려워집니다. ## Cross-Import를 위한 Public API[​](#public-api-for-cross-imports "해당 헤딩으로 이동") **Cross-import**는 같은 Layer 안에서 한 Slice가 다른 Slice를 import하는 것을 말합니다.
[Layer Import Rule](/kr/docs/reference/layers.md#import-rule-on-layers)에 따라 원칙적으로는 금지되지만,
**Entity 간 참조**처럼 현실적으로 불가피한 경우가 있습니다. 예를 들어, 도메인 모델에서 `Artist`와 `Song`이 서로 연관 관계를 가진다면
이를 억지로 숨기기보다는 코드에도 그 관계를 드러내는 편이 낫습니다. 이럴 때는 `@x` 표기를 사용해 **교차 참조 전용 Public API**를 명시적으로 만듭니다. 폴더 예시: ``` - 📂 entities - 📂 artist - 📂 @x - song.ts — entities/song 전용 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(`pages/home/index.js`)에서 `HomePage`와 `loadUserStatistics`를 export합니다. ![세 파일이 서로 원형으로 import하는 모습](/kr/img/circular-import-light.svg#light-mode-only)![세 파일이 서로를 원형으로 import하고 있는 예시입니다.](/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"; ``` `HomePage` 컴포넌트(`HomePage.jsx`)는 다시 Public API를 통해 `loadUserStatistics`를 import합니다. 이 경우 의존 관계는 다음과 같이 순환합니다. * `index.js` → `HomePage.jsx` → 다시 `index.js` 이렇게 되면, 빌드 시점이나 런타임 시점에 예측하기 어려운 문제가 생길 수 있습니다. #### 예방 원칙[​](#예방-원칙 "해당 헤딩으로 이동") * **같은 Slice 내부**에서 가져올 때는 * 상대 경로(`../api/loadUserStatistics`)를 사용해서 * 어느 파일을 참조하는지 명확히 작성합니다. * **다른 Slice**에서 가져올 때는 * 절대 경로(예: `@/features/...`)나 Alias를 사용합니다. * Index에서 export한 모듈이 다시 Index를 참조하지 않도록 주의합니다. ### Large Bundle & Tree-shaking 문제[​](#large-bundles "해당 헤딩으로 이동") 일부 Bundler는 Index 파일에서 여러 모듈을 한 번에 export할 경우,
실제로 사용하지 않는 코드(Dead code)를 제대로 제거(Tree-shaking)하지 못할 수 있습니다. 대부분의 Public API에서는 모듈 간 연관성이 높아 크게 문제되지 않지만,
`shared/ui`, `shared/lib`처럼 **서로 관련성이 낮은 모듈 묶음**에서는 문제가 커집니다. 예시 구조: ``` - 📂 shared/ui/ - 📂 button - 📂 text-field - 📂 carousel - 📂 accordion ``` 이 상황에서 단순히 `Button` 하나만 사용하고 싶어도,
만약 `shared/ui` 전체를 통째로 export하는 큰 Index가 있다면 * `carousel`, `accordion` 등 무거운 의존성까지 * 함께 번들에 포함될 수 있습니다. 특히 Syntax Highlighter, Drag-and-Drop 라이브러리처럼
용량이 큰 의존성은 최종 번들 크기에 큰 영향을 줍니다. #### 해결 방법[​](#해결-방법 "해당 헤딩으로 이동") 각 컴포넌트/라이브러리별로 **별도의 작은 Index 파일**을 만듭니다. 예시: ``` - 📂 shared/ui/ - 📂 button - index.ts - 📂 text-field - index.ts ``` 그리고 사용하는 쪽에서는 `@/shared/ui/button`, `@/shared/ui/text-field`와 같이 **컴포넌트 단위로 직접 import**합니다. pages/sign-in/ui/SignInPage.jsx ``` import { Button } from "@/shared/ui/button"; import { TextField } from "@/shared/ui/text-field"; ``` 이렇게 하면 Bundler가 사용되지 않는 컴포넌트 코드를 제거할 수 있어 Tree-shaking이 더 잘 동작하게 됩니다. ### Public API 우회 방지의 한계[​](#public-api-우회-방지의-한계 "해당 헤딩으로 이동") Slice에 Index 파일을 만들어도, 개발자가 직접 내부 경로를 입력해 import하는 것을 완전히 막을 수는 없습니다. 특히 IDE의 Auto Import 기능이 내부 파일 경로를 자동으로 선택해 버리면, Public API 규칙을 모르는 상태에서 내부 구현을 바로 import하게 될 수 있습니다. #### 해결 방법[​](#해결-방법-1 "해당 헤딩으로 이동") * [Steiger](https://github.com/feature-sliced/steiger) 같은 **FSD 전용 아키텍처 린터**를 사용해 프로젝트의 import 경로를 검사하고, 규칙을 강제합니다. ## 대규모 프로젝트에서의 Bundler 성능 문제[​](#대규모-프로젝트에서의-bundler-성능-문제 "해당 헤딩으로 이동") [TkDodo 글](https://tkdodo.eu/blog/please-stop-using-barrel-files)에서도 언급되듯,
Index 파일(일명 **barrel 파일**)이 너무 많아지면 개발 서버 실행 속도나 HMR(Hot Module Replacement) 성능이 저하될 수 있습니다. #### 최적화 방법[​](#최적화-방법 "해당 헤딩으로 이동") 1. [Large Bundle & Tree-shaking 문제](#large-bundles)에서 설명한 것처럼, `shared/ui`, `shared/lib`에 있는 큰 Index를 없애고 컴포넌트/모듈 단위로 쪼갠 작은 Index를 사용합니다. 2. Segment 단위로 불필요한 Index 파일을 만들지 않습니다. * 예: `features/comments/index.ts`가 이미 Slice의 Public API 역할을 하고 있다면, `features/comments/ui/index.ts` 같이 중첩된 Index는 굳이 만들 필요가 없습니다. 3. 큰 프로젝트는 **기능 단위 Chunk 또는 패키지**로 나눕니다. * 예: Google Docs처럼 Document Editor와 File Browser를 서로 다른 Chunk/패키지로 분리 * 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` Layer와 `App` Layer는 Slice를 가지지 않습니다. * `Shared` Layer는 비즈니스 로직이 전혀 없으므로, 제품 관점에서 Slice로 나눌 의미가 없습니다. * `App` Layer는 애플리케이션 전체를 다루기 때문에, 여기서 다시 Slice로 나눌 필요가 없습니다. ### Zero 결합도와 높은 응집도[​](#zero-coupling-high-cohesion "해당 헤딩으로 이동") Slice는 **다른 Slice와 최대한 독립적**이어야 하고,
또한 **자신의 핵심 목적과 직접 관련된 코드 대부분을 내부에 포함**해야 합니다. 아래 그림은 **응집도(cohesion)** 와 **결합도(coupling)** 개념을 시각적으로 보여 줍니다. ![](/kr/img/coupling-cohesion-light.svg#light-mode-only)![](/kr/img/coupling-cohesion-dark.svg#dark-mode-only) Image inspired by Slice 간 독립성은 [Layer Import Rule](/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](/kr/docs/reference/public-api.md)에서 자세히 설명합니다. ### Slice Group[​](#slice-group "해당 헤딩으로 이동") 서로 연관성이 높은 Slice들은 폴더로 묶어 **그룹처럼** 관리할 수 있습니다.
다만, 그룹으로 묶더라도 각 Slice에 대해 기존과 동일한 **격리 규칙**이 적용되며,
**그룹 내부라고 해서 코드 공유가 허용되는 것은 아닙니다.** ![Features \"compose\", \"like\" 그리고 \"delete\"가 \"post\" 폴더에 그룹화되어 있습니다. 해당 폴더에는 허용되지 않음을 나타내기 위해 취소선이 그어진 \"some-shared-code.ts\" 파일도 있습니다.](/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 페이지](/kr/docs/reference/layers.md#layer-definitions)에서 자세히 설명합니다. 또한 프로젝트에 맞게 **커스텀 Segment**를 정의할 수도 있습니다.
특히 `App` Layer와 `Shared` Layer는 Slice가 없기 때문에,
이 두 Layer에서는 커스텀 Segment를 자주 사용하게 됩니다. Segment 이름을 정할 때는,
폴더 안에 **무슨 파일이 들어 있는지**가 아니라 **무엇을 위해 존재하는지(목적)** 가 드러나도록 작성하는 것이 좋습니다. 예를 들어 `components`, `hooks`, `types` 같은 이름은 성격만 나타낼 뿐,
**역할이나 목적을 알기 어렵기 때문에** 가능한 한 피하는 편이 좋습니다. --- ### 명시적 비즈니스 로직 도메인별로 코드를 구분해 필요한 로직을 즉시 찾을 수 있습니다. ---