카테고리 없음

FSD 아키텍처 정독하기 3편

harubanbo 2026. 4. 25. 21:58

슬라이스와 세그먼트

슬라이스는 레이어 내부를 비즈니스 도메인 별로 나눈다. 슬라이스의 역할은 제품, 비즈니스, 애플리케이션 관점에서 서로 관련 있는 코드를 하나로 묶는 것이다. 그러므로 슬라이스 이름은 고정된 규칙이 없고 수에 제한도 없으며 애플리케이션의 비즈니스 도메인에 맞춰 정한다. 이 규칙이 응집도는 높이고 결합도는 낮추는 장점이 있다.

  • 사진 갤러리: photo, effetcts, gallery-page
  • 소셜 네트워크: post, comments, news-feed

참고로 shared 레이어는 비즈니스 로직이 전혀 없으므로, 제품 관점에서 슬라이스로 나눌 의미가 없다. app 레이어 또한 애플리케이션 전체를 다루기 때문에, 여기서 다시 슬라이스로 나눌 필요가 없다.

슬라이스는 다른 슬라이스와 최대한 독립적(cross-import를 쓰지 않는 이상 같은 레이어 내에서 다른 슬라이스 간섭 불가)이어야 하고 또한 자신의 핵심 목적과 직접적으로 관련된 코드 대부분을 내부에 포함해야 한다. 슬라이스 내부 모듈(파일)은 자신보다 하위 레이어의 슬라이스의 모듈(파일)만 import 가능하다. (features 레이어의 슬라이스 안에서 entities 레이어의 요소 import)


슬라이스 그룹

서로 연관성이 높은 슬라이스들은 폴더로 묶어 그룹처럼 관리할 수 있다. 다만 그룹으로 묶어도 각 슬라이스에 대해 기존과 동일한 격리 규칙은 적용되며 그룹 내부라고 해서 코드 공유가 허용되지 않는다.




세그먼트

세그먼트는 FSD 구조에서 마지막 계층으로 코드를 기술적인 역할과 성격에 따라 나누는 기준인데 슬라이스 하위나 app, shared 레이어 하위에서 세분화되는 구조로 코드의 역할(UI, 데이터처리, 상태관리 등)에 따라 코드를 그룹화한다. 일반적으로는 다음과 같은 표준 세그먼트로도 충분하며 세그먼트 또한 아래 5가지를 무조건 만들 필요 없이 필요한 것만 만들어서 도입하면 된다.

  • ui - UI 관련 코드: Component, Date Formatter, Style 등 UI 표현과 직접 관련된 코드(렌더링과 외관을 다루는 코드)
  • api - Backend 통신: Request Function, Data Type, Mapper 등
  • model - Data Model: Schema, Interface, Store, Business Logic 등 애플리케이션 도메인 모델 등
  • lib - Slice 내부에서 사용하는 공통 Library 코드
  • config - Configuration, Feature Flag 등 설정 관련 코드

프로젝트에 맞게 커스텀 세그먼트를 정의할 수도 있다. 특히 app, shared 레이어는 슬라이스가 없어서 이 두 레이어에서는 커스텀 세그먼트를 자주 사용하게 된다. (필요하다면 App 또는 Shared 레이어에서만 추가적인 segment를 정의해라) 세그먼트 이름을 정할 때는 폴더 안에 무슨 파일이 들어있는지가 아니라 무엇을 위해 존재하는지(목적)이 드러나도록 작성하는 게 좋다.

public API - 엄격한 공개 API 정의

public API는 슬라이스의 기능을 외부에서 사용할 수 있는 공식 경로다. 상위 레이어는 하위 레이어의 요소를 참조할 수 있다고 했는데 무작정 참고할 수 있는 게 아니다. 하위 레이어의 요소에서 공개 API 정의를 해줘야 상위 레이어가 참조할 수 있다. 외부 코드는 반드시 이 경로를 통해서만 슬라이스 내부의 특정 코드에 접근할 수 있다. 즉, 슬라이스와 외부 코드간에 계약이자 접근 게이트의 역할이다.

일반적으로 public API는 re-export를 모아둔 index 파일로 구현한다. 예를 들어서 pages/auth/index.js 파일에서 LoginPage, RegisterPage등을 다시 내보내는 방식이다.

export { LoginPage } from "./ui/LoginPage";
export { RegisterPage } from "./ui/RegisterPage";
📂 shared/
  📂 ui/
    📄 Button.tsx
    📄 Avatar.tsx
    📄 Modal.tsx
    📄 index.ts # 공개 API 필수

📂 entities/
  📂 user/
    📂 ui/
      📄 UserAvatar.tsx
      📄 UserCard.tsx
    📂 model/
      📄 userStore.ts
    📄 index.ts # 공개 API 필수

📂 features/
  📂 auth/
    📂 ui/
      📄 LoginButton.tsx
    📄 index.ts # 공개 API 필수
// shared/ui/index.ts
export { Button } from './Button';
export { Avatar } from './Avatar';
export { Modal } from './Modal';

// entities/user/index.ts
export { UserAvatar } from './ui/UserAvatar';
export { UserCard } from './ui/UserCard';
export { useCurrentUser } from './model/userStore';
export type { User } from './model/types';

// features/auth/index.ts
export { LoginButton } from './ui/LoginButton';
// widgets/header/ui/Header.tsx
import { Button, Avatar } from 'shared/ui'; // 공개 API 통해
import { UserAvatar } from 'entities/user'; // 공개 API 통해
import { LoginButton } from 'features/auth';// 공개 API 통해

// ❌ 이렇게 직접 접근 금지
import { Button } from 'shared/ui/Button';
import { UserAvatar } from 'entities/user/ui/UserAvatar';

이렇게 하는 이유는 내부 구조 변경에 자유롭다는 장점이 있다. 예를 들어서 아래와 같다.

// entities/user/ui/UserAvatar.tsx → entities/user/ui/avatars/UserAvatar.tsx 로 내부 구조를 바꿔도

// entities/user/index.ts만 경로 수정하면 됨
export { UserAvatar } from './ui/avatars/UserAvatar'; // 경로만 변경

// 상위 레이어 코드는 변경 불필요
import { UserAvatar } from 'entities/user'; // 그대로 동작

공개 API가 외부와의 계약 역할을 하므로 내부를 마음대로 리팩토링해도 상위 레이어 참조 코드에는 영향이 없다.

슬라이스의 public API 규칙

슬라이스 내부 구조는 팀이 원하는 방식으로 자유롭게 설계할 수 있다. 하지만 다른 슬라이스에서 사용할 수 있도록 명확한 public API를 반드시 제공해야 한다. 이 규칙을 slice public API rule이라고 한다. (자세한 내용은 아래 public API에서 설명)

  • 모든 슬라이스(또는 슬라이스가 없는 레이어의 세그먼트 등)는 public API를 정의해야 한다.
  • 외부 모듈은 슬라이스, 세그먼트의 내부 구조에 직접 접근하지 않고 제공해주는 public API를 통해서만 접근해야 한다.

좋은 public API의 최소 세 가지 조건

좋은 public API는 슬라이스를 다른 코드와 통합하기 쉽고 안정적으로 유지보수할 수 있게 해준다.

  1. 내부 구조 변경에 영향이 없어야 한다. - 슬라이스 내부 폴더 구조를 바꾸더라도 외부 코드는 그대로 동작해야 한다.
  2. 주요 동작 변경 = API 변경 - 슬라이스 동작이 크게 바뀌어 기존 기대가 깨진다면 public API도 함께 변경되어야 한다.
  3. 필요한 부분만 노출 - 슬라이스 전체 구현을 공개하지 않고 외부에서 꼭 필요한 기능만 선별해서 노출한다.

안 좋은 예시로는 무분별한 wildcard re-export가 있다.

개발 초기에는 편의상 한 줄로 모든 것을 export하려고 와일드카드 re-export를 할 수도 있으나 이런 방식은 슬라이스의 인터페이스를 흐리게 만들고 나중에 큰 부담이 된다.

// ❌ 금지
export * from "./ui/Comment"; // 무분별한 UI export
export * from "./model/comments"; // 내부 모델 노출

이런 방식은 아래와 같은 문제가 있다.

  • 발견 가능성 저하 - public API에서 어떤 기능을 제공하는지 한눈에 파악하기 어렵다.
  • 내부구현 노출 - 외부에서 알 필요가 없는 내부 코드를 외부에서 직접 사용하게 되고 이 코드에 대한 의존성이 생기면 리팩토링이 어려워진다.

cross-import를 위한 public API

이전에 정리한 내용이지만 크로스 임포트는 같은 레이어 안에서 한 슬라이스가 다른 슬라이스를 임포트하는 것을 말한다. 레이어 import rule에 따라 원칙적으로는 금지되지만 엔티티 간 참조처럼 현실적으로 불가피한 경우가 있다. 예를 들어서 도메인 모델에서 artist 엔티티와 song 엔티티가 서로 연관성을 가진다면 억지로 숨기기보다 코드적으로 그 관계를 드러내는 편이 낫다.

이런 경우에는 @x 표기를 사용해서 교차 참조 전용 public API를 명시적으로 구현한다.

- 📂 entities
    - 📂 artist
        - 📂 @x
            - song.ts — entities/song 전용 Public API
        - index.ts — 일반 Public API
import type { Artist } from "entities/artist/@x/song";

크로스 임포트는 반드시 최소화 해야하며 허용하더라도 엔티티 레이어에서만 사용할 것을 권장한다. 다른 레이어에서는 가능한 의존관계를 제거하고 설계를 다시 검토하는 것이 좋다.

여기까지 정리된 내용만 보자면 FSD를 도입하면 다음과 같은 장점이 있다.

  1. 일관성 - 구조가 표준화되어 팀 간 협업과 신규 멤버 온보딩이 쉬워진다.
  2. 격리성 - Layer와 Slice간 의존성을 제한해서 특정 모듈만 안전하게 수정 가능 -> 무슨 소리?
  3. 재사용 범위 제어 - 재사용 가능한 코드를 필요한 범위에서만 활용할 수 있어서 DRY 원칙과 실용성을 균형있게 유지한다.
  4. 도메인 중심 구조 - 비즈니스 용어 기반의 구조로 되어있어서 전체 코드를 몰라도 특정 기능을 독립적으로 구현 가능

적용하다보면 규칙에 맞추기 어려운 지점이 있을 수 있으나 현재 시점에서는 가장 체계적인 아키텍처로 보인다.

다음에는 FSD 활용 시 주의할 점에 대해서 포스팅할 예정이다.