CRA 환경에서 FSD 아키텍처 적용하기
아래는 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 아키텍처를 간단히 적용한 것이며, 실제 프로젝트에서는:
- 각 레이어(폴더)의 역할과 책임을 팀원들이 명확히 이해해야 합니다.
- pages, widgets, features, entities, shared 순으로 의존이 흐르도록(상하 관계 준수) 구조를 지켜야 유지 보수가 수월해집니다.
- 프로젝트 규모가 작으면 processes 폴더를 생략하거나, 반대로 대형 프로젝트라면 app, pages, widgets 아래 하위 레이어를 좀 더 세분화할 수도 있습니다.
이렇게 FSD를 적용하면 기능과 엔티티별로 코드가 모듈화되어 코드 가독성, 확장성, 유지보수성 모두 크게 개선될 수 있습니다.