Lighthouse of FE beginner

FSD 아키텍처의 참조 규칙 본문

[WEB] 프론트엔드

FSD 아키텍처의 참조 규칙

[FE] Lighthouse 2025. 12. 16. 17:36

들어가며

프론트엔드 프로젝트가 커질수록 우리는 코드의 구조와 의존성 관리에 대해 고민하게 됩니다.

 

"이 컴포넌트가 저 레이어의 코드를 참조해도 될까?", "같은 기능 내 파일끼리는 어떻게 import 해야 할까?" 같은 질문들이 늘어나고, 일관성 없는 import 패턴은 프로젝트의 복잡도를 기하급수적으로 증가시킵니다.

 

이번 글에서는 FSD(Feature-Sliced Design) 아키텍처에서 다른 소스 코드를 참조할 때 절대 경로, 상대 경로를 혼재해 사용하는 전략에 대해 살펴보겠습니다.

 

FSD 아키텍처에 대한 간단한 이야기

FSD(Feature-Sliced Design) 아키텍처는 레이어, 슬라이스, 세그먼트 단위로 나눠지며 프론트엔드 애플리케이션을 도메인(기능 - feature) 단위로 작게 쪼개(slice)어 디자인(design)하는 아키텍처입니다.

 

이때 각 레이어는 명확한 책임(역할)을 가지며, 레이어는 아래의 구조로 나눠집니다.

app # 애플리케이션 전역 설정
pages # 페이지 레벨 컴포넌트
widgets # 독립된 UI 블록의 모음
features # 사용자 시나리오와 기능
entities # 비즈니스 엔티티
shared # 공용 유틸리티, 상수(혹은 설정), UI 컴포넌트, library 코드

 

이런 구조를 통해 각 레이어는 독립적으로 발전할 수 있으며, 의존성 방향이 명확해집니다.

FSD 아키텍처에 대한 자세한 내용은 생략하겠습니다! 문서가 잘 되어있으니 아래 문서를 참고해보세요!

 

개요 | Feature-Sliced Design

Feature-Sliced Design (FSD) 는 프론트엔드 애플리케이션 구조를 위한 아키텍처 방법론입니다.

feature-sliced.design

 

FSD 아키텍처 내 규칙

FSD 아키텍처는 몇 가지 핵심 규칙을 통해 코드의 품질을 유지합니다.

 

레이어에서 레이어로 흐르는 엄격한 의존성

FSD 아키텍처에서 의존성은 항상 위에서 아래로만 흐릅니다.

app → pages → widgets → features → entities → shared

상위 레이어는 하위 레이어를 참조할 수 있지만, 하위 레이어는 상위 레이어를 참조할 수 없습니다.

예를 들어 entities 레이어의 코드는 features 레이어를 절대 참조할 수 없으며, shared 레이어는 app 레이어를 참조할 수 없습니다.

이런 단방향 의존성 규칙은 순환 참조를 방지하고, 코드의 흐름을 예측 가능하게 만듭니다.

 

공개 API와 슬라이스 캡슐화

각 슬라이스는 index.ts 파일을 통해 외부에 공개할 API만을 내보냅니다.

features/
  # 슬라이스
  ㄴpost/
     # 이하 세그먼트
     ㄴui/
     ㄴmodel/
     ㄴconstants/
     ㄴindex.ts # 공개 API

 

배럴 파일(index.ts)을 통해 슬라이스 내 코드를 외부에 노출하고, 노출되지 않은 소스 코드의 구현은 다른 레이어에 노출하지 않음으로써 내부 소스 코드는 캡슐화가 됩니다.

이는 다른 레이어의 소스가 해당 레이어에서 관심사가 아님으로 결합을 최소화 하는 원칙과도 같습니다. 이를 통해 슬라이스 내부 구현을 자유롭게 리팩토링할 수 있으며, 외부 코드에 영향을 주지 않습니다.

 

같은 레이어 내 슬라이스 참조 금지

같은 레이어에 속한 슬라이스끼리는 서로를 참조할 수 없습니다.

features/
   ㄴpost/ # ❌ comment를 직접 참조 불가
   ㄴcomment/ # ❌ post를 직접 참조 불가

 

만약 두 슬라이스가 서로 의존해야 한다면, 이는 설계상의 문제일 가능성이 높습니다. 공통 로직은 하위 레이어(entities 또는 shared)에 위치하거나, 상위 레이어(pages)에서 조합하는 방식으로 재설계 해야합니다.

 

예외 레이어: shared, app

shared, app 레이어는 슬라이스가 곧 세그먼트 입니다. 슬라이스 내 소스코드는 세분화 되지 않으며 세그먼트의 역할을 할 수 있습니다.

 

또 shared 레이어 속 소스 코드는 서로를 참조할 수 있습니다. app 레이어 역시 전역 설정을 담당하므로 특별한 제약 없이 필요한 모든 레이어를 참조할 수 있습니다.

 

공개 API

슬라이스의 배럴 파일(index.ts) 파일은 해당 슬라이스의 공개 API 역할을 합니다.

// features/post/index.ts
export { PostList, PostDetail } from './ui'
export { usePostQuery } from './model'
export { POST_STATUS } from './constants'

 

외부에서는 반드시 이 공개 API를 통해서만 슬라이스 내부에 접근해야 합니다.

// pages/post-page/ui/post-page.tsx
import { PostList } from '@/features/post'  // ✅ 공개 API 사용

 

내부 구현에 직접 접근하는 것은 금지됩니다.

// ❌ 내부 경로 직접 접근
import { PostList } from '@/features/post/ui/post-list'

 

이런 캡슐화를 통해 슬라이스 내부 구조가 변경되어도 외부 코드는 영향을 받지 않습니다.

 

import 규칙

FSD 아키텍처에서는 절대 경로와 상대 경로를 혼재해서 사용하는 것을 권장합니다. 이는 단순한 스타일의 문제가 아니라, 코드의 의존성 범위를 시각적으로 명확하게 구분하기 위함입니다.

 

import 선언 순서

import는 다음의 3단계 순서를 따릅니다.

// 1. 외부 라이브러리 (node_modules)
import { useMemo } from 'react'
import { useNavigate } from 'react-router'

// 2. 타 레이어의 슬라이스 (절대 경로)
import { Comment } from '@/entities/comment'

// 3. 같은 슬라이스 내부 (상대 경로)
import { Post } from '../model/type'
import { PostButton } from './post-button'

 

절대 경로를 사용하는 경우

타 레이어의 슬라이스를 참조할 때는 반드시 절대 경로를 사용합니다.

// features/post/ui/post.tsx
import { Comment } from '@/entities/comment'
import { Button } from '@/shared/ui'

 

절대 경로는 항상 슬라이스의 공개 API(index.ts)를 통해 접근해야 하며, 내부 경로를 직접 명시해서는 안 됩니다.

 

상대 경로를 사용하는 경우

같은 슬라이스 내부의 코드를 참조할 때는 상대 경로를 사용합니다.

// features/post/ui/post.tsx
import { Post } from '../model/type'
import { usePostQuery } from '../api/query'
import { PostButton } from './post-button'

 

FSD 아키텍처에서 슬라이스는 2단계 깊이(슬라이스/세그먼트)로 평탄하게 유지되므로, 상대 경로는 항상 ../ 한 단계 또는 ./ 같은 경로로만 표현됩니다.

features/post/
  ├── ui/
  │   ├── post.tsx
  │   └── post-button.tsx
  ├── model/
  │   ├── type.ts
  │   └── query.ts
  └── index.ts

 

이렇게 평탄한 구조를 유지하면 ../../ 같은 복잡한 경로가 등장하지 않아 코드의 가독성이 높아집니다.

 

장점

절대 경로와 상대 경로를 혼재해서 사용하는 전략은 다음과 같은 이점을 제공합니다.

 

1. 의존성 범위를 한눈에 파악

import { useState } from 'react'
import { Comment } from '@/entities/comment'
import { Post } from '../model/type'

 

이 코드를 보는 순간, 우리는 즉시 알 수 있습니다.

  • react는 외부 라이브러리
  • @/entities/comment는 다른 레이어에 대한 의존성
  • ../model/type은 같은 슬라이스 내부 코드

2. 리팩토링 영향 범위 최소화

상대 경로로 참조되는 코드는 같은 슬라이스 내부이므로, 수정 시 외부에 영향을 주지 않습니다. 반대로 절대 경로로 참조되는 코드를 수정하면 다른 레이어에 영향을 줄 수 있다는 것을 즉시 인지할 수 있습니다.

 

3. 캡슐화 원칙 강화

절대 경로는 반드시 공개 API를 통해서만 사용하도록 강제할 수 있습니다.

// ✅ 공개 API 사용
import { PostList } from '@/features/post'

// ❌ 내부 구조 직접 접근 (린트로 차단 가능)
import { PostList } from '@/features/post/ui/post-list'

 

ESLint 규칙을 통해 내부 경로 직접 접근을 차단할 수 있습니다.

 

4. 예측 가능한 경로 패턴

FSD의 평탄한 구조에서는 항상 ../세그먼트/파일 또는 ./파일 형태로만 상대 경로가 나타납니다. 이는 개발자가 경로를 예측하기 쉽게 만들고, 인지 부하를 크게 줄여줍니다.

 

주의해야 할 점

절대 경로와 상대 경로를 혼재해서 사용할 때 주의해야 할 몇 가지 사항이 있습니다.

 

1. 팀 컨벤션의 명확한 공유

이런 규칙은 팀 전체가 이해하고 따라야 효과가 있습니다. 프로젝트 초기에 충분히 논의하고, 문서화하여 공유해야 합니다.

 

2. ESLint 규칙으로 강제

사람이 실수 없이 규칙을 지키기는 어렵습니다. eslint-plugin-boundaries 같은 도구를 사용해 import 규칙을 자동으로 검증하고 강제하는 것이 좋습니다.

// .eslintrc.js 예시
{
  rules: {
    'no-restricted-imports': ['error', {
      patterns: [
        {
          group: ['@/features/*/ui/*', '@/features/*/model/*'],
          message: '슬라이스 내부 경로는 상대 경로를 사용하세요'
        }
      ]
    }]
  }
}

 

3. 일관성 유지

절대 경로는 타 레이어 참조, 상대 경로는 같은 슬라이스 내부 참조라는 원칙을 반드시 지켜야 합니다. 예외를 두는 순간 규칙의 의미가 사라지고 혼란만 가중됩니다.

 

예시로 살펴보기

실제 코드 예시를 통해 import 규칙을 살펴보겠습니다.

// features/post/ui/post.tsx

import { useMemo } from 'react'
import { useNavigate } from 'react-router'

import { Comment } from '@/entities/comment'
import { Button } from '@/shared/ui'

import { Post } from '../model/type'
import { usePostQuery } from '../api/query'
import { POST_STATUS } from '../constants'
import { PostButton } from './post-button'

export const PostComponent = () => {
  const navigate = useNavigate()
  const { data: post } = usePostQuery()
  
  const isPublished = useMemo(
    () => post?.status === POST_STATUS.PUBLISHED,
    [post]
  )

  return (
    <div>
      <h1>{post?.title}</h1>
      <Button onClick={() => navigate('/posts')}>
        목록으로
      </Button>
      {isPublished && (
        <>
          <Comment commentId={post.commentId} />
          <PostButton post={post} />
        </>
      )}
    </div>
  )
}

 

이 코드를 보면 아래 내용을 즉시 파악할 수 있습니다.

  1. react, react-router는 외부 라이브러리 의존성
  2. @/entities/comment, @/shared/ui는 다른 레이어에 대한 의존성
  3. ../model/type, ../api/query 등은 같은 슬라이스 내부 코드

만약 Comment 컴포넌트를 수정해야 한다면, entities 레이어 전체를 확인해야 한다는 것을 알 수 있습니다. 반대로 PostButton을 수정한다면 같은 슬라이스 내부에서만 영향을 확인하면 됩니다.

 

마치며

프론트엔드 프로젝트의 복잡도가 증가할수록 명확한 아키텍처와 일관된 import 규칙의 중요성은 더욱 커집니다.

절대 경로와 상대 경로를 혼재해서 사용하는 전략은 단순히 "어떻게 import 할 것인가"를 넘어서, 코드의 의존성 범위를 시각적으로 명확하게 드러내는 강력한 도구입니다.

 

FSD 아키텍처의 레이어 구조와 결합된 import 규칙은 다음을 가능하게 합니다.

  • 코드 리뷰 시 의존성을 즉시 파악
  • 리팩토링 시 영향 범위를 명확하게 인지
  • 캡슐화 원칙을 코드 레벨에서 강제

물론 이런 규칙을 도입하기 위해서는 팀 전체의 이해와 합의, 그리고 린트 도구를 통한 자동화가 필요합니다. 하지만 일단 자리 잡고 나면, 프로젝트의 유지보수성과 확장성이 크게 향상되는 것을 경험할 수 있을 것입니다.

 

여러분의 프로젝트에도 명확한 import 규칙을 도입하여, 더 나은 코드 품질과 개발 경험을 만들어가시길 바랍니다.

읽어주셔서 감사합니다.