etc 이슈

CRA 환경에서 FSD 아키텍처 적용하기

Unlimited00 2025. 4. 15. 11:17

아래는 Create React App(CRA) 환경에서 Feature-Sliced Design(FSD) 아키텍처를 적용할 때의 예시 구조와 간략한 코드 샘플입니다. 실제로는 프로젝트의 규모나 팀 상황에 따라 세부 구조나 네이밍을 조정해야 하지만, 전반적인 디렉터리 레이아웃과 개념을 잡는 데 도움이 될 것입니다.


1. 폴더 구조 예시

FSD에서는 일반적으로 다음과 같은 레이어(layer) 가 사용됩니다:

  • app/: 루트 애플리케이션 레벨 설정 (Provider, 라우팅, 전역 스타일 등)
  • processes/ (선택): 사용자 인증, 빌링 처리 등 전역적·비즈니스 크로스컷 기능
  • pages/: 페이지 단위 (URL 단위)로 구성
  • widgets/: 화면에서 주요 UI 블록(헤더, 푸터, 사이드바 등)
  • features/: 특정 기능(검색, 로그인, 코멘트 작성 등)
  • entities/: 도메인 엔티티(사용자, 게시글, 상품 등) + 비즈니스 로직
  • shared/: 전역 유틸, 재사용 가능한 컴포넌트, 타입, 함수 등
  • assets/: 이미지, 폰트, 글로벌 스타일 등

예시는 다음과 같습니다:

src/
├── index.js                # [CRA 필수] 앱 진입점
├── app/
│   ├── providers/
│   │   └── store.js          # Redux Store, 혹은 React Query 설정 등
│   ├── App.js                # 전역 라우팅, 레이아웃 등
├── processes/
│   └── <...>                 # (선택) 인증, 빌링 등의 전역적 비즈니스 로직
├── pages/
│   └── HomePage/
│       ├── ui/
│       │   └── HomePage.jsx  # 페이지 UI
│       └── index.js          # HomePage 관련 설정/내보내기
├── widgets/
│   └── Header/
│       ├── ui/
│       │   └── Header.jsx    # 헤더 UI
│       └── index.js
├── features/
│   └── userAuth/
│       ├── ui/
│       │   └── LoginForm.jsx # 로그인 UI 컴포넌트
│       ├── model/
│       │   └── useLogin.js   # 로그인 상태, 훅, 액션 로직 등
│       └── index.js
├── entities/
│   └── user/
│       ├── model/
│       │   └── userSlice.js  # Redux slice, Recoil atom, Zustand store 등
│       ├── lib/
│       │   └── userApi.js    # API 호출 로직, DTO 변환 등
│       ├── types/
│       │   └── index.js      # 사용자 타입 정의(TypeScript 시)
│       └── index.js
├── shared/
│   ├── config/
│   │   └── index.js          # 환경변수, 전역 설정 등
│   ├── api/
│   │   └── httpClient.js     # axios, fetch 래퍼 등
│   ├── ui/
│   │   └── Button.jsx        # 재사용 가능한 디자인 컴포넌트
│   ├── lib/
│   │   └── helpers.js        # 공통 헬퍼함수
│   └── index.js
└── assets/                # 이미지, 폰트, 글로벌 스타일 등
폴더 이름과 구조는 FSD의 핵심 개념
을 지키되, 팀의 합의에 따라 조정해도 됩니다.

예:

widgets 대신 components 를 쓰는 경우도 있고,

processes 레이어가 불필요하면 생략할 수도 있습니다.

2. 예시 코드 스니펫

2.1 app/index.js

CRA에서 기본적으로 제공되는 index.js(또는 main.js) 파일입니다. 전역 Store, Router, Error Boundary 등을 제공할 수 있습니다.

import React from "react";
import ReactDOM from "react-dom/client";
import App from './app/App';
import { Provider } from "react-redux";
import { store } from "./providers/store";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

2.2 app/providers/store.js

Redux를 사용한다고 가정한 예시입니다. (React Query, Zustand 등 다른 상태 관리도 유사하게 구성)

import { configureStore } from "@reduxjs/toolkit";
import { userReducer } from "entities/user";

export const store = configureStore({
  reducer: {
    user: userReducer,
  },
});

2.3 app/App.js

전역 라우팅을 담당하는 컴포넌트 예시입니다.

import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { HomePage } from "pages/HomePage";
import { Header } from "widgets/Header";

function App() {
  return (
    <Router>
      <Header />
      <Routes>
        <Route path="/" element={<HomePage />} />
        {/* 라우트 추가 가능 */}
      </Routes>
    </Router>
  );
}

export default App;

2.4 pages/HomePage/ui/HomePage.js

HomePage에서 필요한 상태나 로직은 features나 entities에서 가져다가 사용합니다.

import React from "react";
import { LoginForm } from "features/userAuth";
import "./HomePage.css";

export const HomePage = () => {
  return (
    <div className="home-page">
      <h1>Welcome to the Home Page</h1>
      <LoginForm />
    </div>
  );
};

2.5 features/userAuth/ui/LoginForm.js

실제 로그인 폼 UI와 관련된 로직입니다. 로그인 액션은 model/useLogin.js에서 가져옵니다.

import React, { useState } from "react";
import { useLogin } from "../model/useLogin";

export const LoginForm = () => {
  const { login, isLoading, error } = useLogin();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    login({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email
        <input 
          type="email" 
          value={email}
          onChange={(e) => setEmail(e.target.value)} 
        />
      </label>
      <label>
        Password
        <input 
          type="password" 
          value={password}
          onChange={(e) => setPassword(e.target.value)} 
        />
      </label>
      {error && <div>{error}</div>}
      <button type="submit" disabled={isLoading}>
        {isLoading ? "Logging in..." : "Login"}
      </button>
    </form>
  );
};

2.6 features/userAuth/model/useLogin.js

useLogin 훅에서는 API 요청을 처리하거나, Redux와 연결하는 등 비즈니스 로직을 구현합니다.

import { useCallback, useState } from "react";
import { loginUser } from "entities/user"; // 가령 userSlice 혹은 user API

export function useLogin() {
  const [isLoading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const login = useCallback(async ({ email, password }) => {
    setLoading(true);
    setError(null);
    try {
      // 실제 API 호출 혹은 Redux 액션 디스패치
      await loginUser({ email, password });
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []);

  return { login, isLoading, error };
}

2.7 entities/user/model/userSlice.js

Redux Toolkit을 예시로 사용자 데이터를 관리합니다.

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { authApi } from "shared/api/authApi";

// Thunk: 비동기 로그인 로직
export const loginUser = createAsyncThunk(
  "user/loginUser",
  async ({ email, password }) => {
    const response = await authApi.login({ email, password });
    return response.data;
  }
);

const userSlice = createSlice({
  name: "user",
  initialState: {
    userData: null,
    status: "idle",
    error: null,
  },
  reducers: {
    logout: (state) => {
      state.userData = null;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(loginUser.pending, (state) => {
        state.status = "loading";
      })
      .addCase(loginUser.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.userData = action.payload;
      })
      .addCase(loginUser.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      });
  },
});

export const { logout } = userSlice.actions;
export const userReducer = userSlice.reducer;

3. 경로 설정(옵션)

CRA에서는 절대 경로(alias)를 쓰려면 jsconfig.json 또는 tsconfig.json에서 설정해야 합니다. 예:

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "app/*": ["app/*"],
      "pages/*": ["pages/*"],
      "widgets/*": ["widgets/*"],
      "features/*": ["features/*"],
      "entities/*": ["entities/*"],
      "shared/*": ["shared/*"]
    }
  }
}

이렇게 설정하면 import { LoginForm } from "features/userAuth"; 와 같은 절대 경로를 사용하여 가독성을 높일 수 있습니다.


4. 마무리

위 예시는 CRA 환경에서 FSD 아키텍처를 간단히 적용한 것이며, 실제 프로젝트에서는:

  1. 각 레이어(폴더)의 역할과 책임을 팀원들이 명확히 이해해야 합니다.
  2. pages, widgets, features, entities, shared 순으로 의존이 흐르도록(상하 관계 준수) 구조를 지켜야 유지 보수가 수월해집니다.
  3. 프로젝트 규모가 작으면 processes 폴더를 생략하거나, 반대로 대형 프로젝트라면 app, pages, widgets 아래 하위 레이어를 좀 더 세분화할 수도 있습니다.

이렇게 FSD를 적용하면 기능과 엔티티별로 코드가 모듈화되어 코드 가독성, 확장성, 유지보수성 모두 크게 개선될 수 있습니다.