FSD μν€ν μ²μ ν¨κ» μκ°ν΄λ³΄κΈ°
Vue3μμ React.jsλ‘ νλ‘μ νΈλ₯Ό λ§μ΄κ·Έλ μ΄μ νλ©΄μ Feature-Sliced Design(FSD) μν€ν μ²λ₯Ό λμ νκΈ°λ‘ κ²°μ νμ΅λλ€. κ·Έ κ³Όμ μμ λ°°λ΄ νμΌ(barrel files)μ λν κ³ λ―Όμ΄ μκ²Όκ³ , μ΅κ·Ό λͺ λ μ¬μ΄μ μ΄μ λν κ΄μ μ΄ λ§μ΄ λ°λμλ€λ κ²μ λ°κ²¬νμ΅λλ€.
λ°°λ΄ νμΌμ΄λ?
λ°°λ΄ νμΌμ μ¬λ¬ λͺ¨λμ νλμ νμΌ(μ£Όλ‘ index.js λλ index.ts)μμ λ€μ λ΄λ³΄λ΄λ ν¨ν΄μ λλ€. μ΄ ν¨ν΄μ μ½λ μ 리μ import λ¬Έμ λ¨μννλ λ° λμμ΄ λ©λλ€.
λ°°λ΄ νμΌ μμ΄:
// κ° νμΌμμ κ°λ³μ μΌλ‘ κ°μ Έμ€κΈ°
import { Button } from "../components/Button";
import { TextField } from "../components/TextField";
import { Checkbox } from "../components/Checkbox";
λ°°λ΄ νμΌ μ¬μ© μ:
// νλμ κ²½λ‘μμ λͺ¨λ μ»΄ν¬λνΈ κ°μ Έμ€κΈ°
import { Button, TextField, Checkbox } from "../components";
λ°°λ΄ νμΌμ μ€μ ꡬν
μΌλ°μ μΈ λ°°λ΄ νμΌ (components/index.ts):
// μμΌλμΉ΄λ λ΄λ³΄λ΄κΈ° (κΆμ₯νμ§ μμ)
export * from './Button';
export * from './TextField';
export * from './Checkbox';
// λλ λͺ
μμ λ΄λ³΄λ΄κΈ° (κΆμ₯)
export { Button } from './Button';
export { TextField } from './TextField';
export { Checkbox } from './Checkbox';
λ°°λ΄ νμΌμ μ±λ₯ λ¬Έμ
Marvinμ JavaScript μνκ³ μλ κ°μ : λ°°λ΄ νμΌ λ¬Έμ κΈμμ μ§μ ν λ°μ κ°μ΄, λ°°λ΄ νμΌμ λ€μκ³Ό κ°μ μ±λ₯ λ¬Έμ λ₯Ό μΌμΌν¬ μ μμ΅λλ€:
- λͺ¨λ κ·Έλν 볡μ‘μ± μ¦κ°: λ°°λ΄ νμΌμ΄ μ¦κ°ν μλ‘ λͺ¨λ κ° μμ‘΄μ± κ·Έλνκ° λ³΅μ‘ν΄μ§λλ€.
- λΆνμν λͺ¨λ λ‘λ©: νΉν μμΌλμΉ΄λ λ΄λ³΄λ΄κΈ°(export * from)λ₯Ό μ¬μ©ν κ²½μ°, μ€μ λ‘ νμνμ§ μμ λͺ¨λκΉμ§ λ‘λλ μ μμ΅λλ€.
λͺ¨λ κ·Έλν λΉκ΅
λ°°λ΄ νμΌ μμ΄:
App.js
βββ Button.js
βββ TextField.js
βββ Checkbox.js
λ°°λ΄ νμΌ μ¬μ© μ:
App.js
βββ components/index.js
βββ Button.js
βββ TextField.js
βββ Checkbox.js
μ±λ₯ ν μ€νΈ κ²°κ³Ό μμ
λͺ¨λ μλ°°λ΄ νμΌ μμλ°°λ΄ νμΌ μ¬μ©μ°¨μ΄
λͺ¨λ μ | λ°°λ΄ νμΌ μμ | λ°°λ΄ νμΌ μ¬μ© | μ°¨μ΄ |
10κ° | 150ms | 180ms | +20% |
50κ° | 450ms | 630ms | +40% |
100κ° | 820ms | 1250ms | +52% |
μ°Έκ³ : μ΄ μμΉλ μμμ©μ΄λ©°, μ€μ νλ‘μ νΈμμλ λ€μν μμΈμ λ°λΌ λ¬λΌμ§ μ μμ΅λλ€.
FSD μν€ν μ²μμμ λ°°λ΄ νμΌ
Feature-Sliced Design(FSD)μ λ°°λ΄ νμΌκ³Ό μ μ¬ν ν¨ν΄μ μλμ μΌλ‘ νμ©ν©λλ€. μ΄λ λ¨μν μ½λ μμ±μ νΈμμ±μ μν κ²μ΄ μλλΌ, μν€ν
μ²μ ꡬ쑰μ λͺ©μ μ λ°μν κ²°μ μ
λλ€.
FSDλ μ ν리μΌμ΄μ μ κΈ°λ₯ λ¨μλ‘ λΆν νκ³ κ³μΈ΅νλ ꡬ쑰λ₯Ό λ§λλ κ²μ λͺ©νλ‘ ν©λλ€. μ΄ κ΅¬μ‘°μμ κ° λ μ΄μ΄(layer)μ μ¬λΌμ΄μ€(slice)λ λͺ νν μ± μκ³Ό μν μ κ°μ§λλ€. λ°°λ΄ νμΌκ³Ό μ μ¬ν ν¨ν΄μ μ¬μ©ν¨μΌλ‘μ¨, FSDλ λ€μκ³Ό κ°μ μ΄μ μ μ»μ΅λλ€:
- μΊ‘μν: κ° λͺ¨λμ λ΄λΆ ꡬνμ μ¨κΈ°κ³ κ³΅κ° APIλ§μ λ ΈμΆν©λλ€.
- μμ‘΄μ± κ΄λ¦¬: μμ λ μ΄μ΄κ° νμ λ μ΄μ΄μλ§ μ κ·Όν μ μλλ‘ μ νν©λλ€.
- μ¬μ¬μ©μ±: κ³΅ν΅ κΈ°λ₯μ μ½κ² 곡μ νκ³ μ¬μ¬μ©ν μ μκ² ν©λλ€.
- μ μ§λ³΄μμ±: μ½λ ꡬ쑰λ₯Ό μΌκ΄λκ² μ μ§νμ¬ κ°λ°μκ° μ½κ² μ΄ν΄νκ³ μμ ν μ μκ² ν©λλ€.
μ΄λ¬ν ꡬ쑰λ λκ·λͺ¨ νλ‘μ νΈμμ μ½λμ μ‘°μ§νμ κ΄λ¦¬λ₯Ό μ©μ΄νκ² λ§λ€μ΄, μ₯κΈ°μ μΌλ‘ νλ‘μ νΈμ νμ₯μ±κ³Ό μ μ§λ³΄μμ±μ ν₯μμν΅λλ€. λ°λΌμ FSDμμ λ°°λ΄ νμΌκ³Ό μ μ¬ν ν¨ν΄μ μ¬μ©μ λ¨μν νΈμμ±μ λμ΄μ, μν€ν μ²μ ν΅μ¬ μμΉμ μ€ννκΈ° μν μ λ΅μ μ νμ΄λΌκ³ λ³Ό μ μμ΅λλ€.
FSDμμμ λ°°λ΄ νμΌ νμ© - μ€μ κ²½νκ³Ό ν
FSD μν€ν μ²λ₯Ό μ€μ νλ‘μ νΈμ μ μ©ν΄λ³Έ κ²½νμ λ°λ₯΄λ©΄, λ°°λ΄ νμΌ(Public API)μ μ£Όμ μ΄μ μ€ νλλ μ μ§λ³΄μμ± ν₯μμ λλ€. μ΄μ λν΄ μ΄λμν΄ μ¬μ§μ κ³Όμ μμ λ§λ λλ£ κ°λ°μλΆκ»μ λ€μκ³Ό κ°μ μΈμ¬μ΄νΈλ₯Ό 곡μ ν΄μ£Όμ ¨μ΄μ:
"FSDλ₯Ό μ¬μ©νλ©΄μ λλ λ°λ‘λ, Public APIλ₯Ό νμ©νλ μ£Όλ μ΄μ μ€ νλκ° μ μ§λ³΄μμ μμ΅λλ€. μλ₯Ό λ€μ΄, entities/userμ μ€ν€λ§κ° μΌλΆ λ³κ²½λλλΌλ features/loginUser, features/registerUser λ±μμ import ꡬ문μ λ³κ²½ν νμκ° μμ΄μ§λλ€."
μ΄λ FSDμ ν΅μ¬ μμΉ μ€ νλμΈ λͺ¨λ κ° μμ‘΄μ± κ΄λ¦¬μ μΌμΉνλ κ΄μ μ
λλ€.
λ°°λ΄ νμΌ μμ± μλν
λκ·λͺ¨ νλ‘μ νΈμμ λ§€λ² μλμΌλ‘ λ°°λ΄ νμΌ(Public API)μ κ΄λ¦¬νλ κ²μ λ²κ±°λ‘μΈ μ μμ΅λλ€. μ΄λ₯Ό ν΄κ²°νκΈ° μν μ€μ©μ μΈ μ κ·Ό λ°©λ²μΌλ‘, λ°°λ΄ νμΌμ μλμΌλ‘ μμ±νλ μ€ν¬λ¦½νΈλ₯Ό νμ©ν μ μμ΅λλ€.
μλ₯Ό λ€μ΄:
pnpm api features/loginUser
μ΄λ¬ν CLI λͺ
λ Ήμ΄λ₯Ό ν΅ν΄ features/loginUser λ΄λΆμ λͺ¨λμ μλμΌλ‘ λΆμνκ³ , λͺ
μμ μΌλ‘ λ΄λ³΄λ΄λ λ°°λ΄ νμΌμ μμ±ν μ μμ΅λλ€. μ΄λ _λ‘ μμνλ νμΌμ΄λ ν΄λλ λ΄λΆμ©μΌλ‘ κ°μ£Όνμ¬ μλμΌλ‘ μ μΈν μ μμ΅λλ€.
λͺ μμ λ΄λ³΄λ΄κΈ°μ μ€μμ±
μμ μΈκΈνλ―μ΄, μμΌλμΉ΄λ λ΄λ³΄λ΄κΈ°(export * from './module')λ κΆμ₯λμ§ μμ΅λλ€. λμ , λͺ μμ λ΄λ³΄λ΄κΈ°λ₯Ό μ¬μ©νλ©΄ μ±λ₯ μ΄μλ₯Ό μ΅μννλ©΄μλ FSDμ μ΄μ μ μΆ©λΆν νμ©ν μ μμ΅λλ€.
// Good: λͺ
μμ λ΄λ³΄λ΄κΈ°
export { LoginForm } from './LoginForm';
export { useAuth } from './useAuth';
export type { User } from './types';
μ΄λ¬ν μ κ·Ό λ°©μμ λͺ¨λμ κ³΅κ° APIλ₯Ό λͺ
νν μ μνκ³ , λΆνμν μμ‘΄μ±μ λ°©μ§νλ©°, μ½λμ κ°λ
μ±κ³Ό μ μ§λ³΄μμ±μ ν₯μμν΅λλ€. μ’ λ μμΈν μμλ₯Ό λ€μ΄λ³Όκ²μ.
μ€ν¬λ¦½νΈμ λν ν΄λ ꡬ쑰 μμ:
project-root/
βββ src/
β βββ features/
β β βββ loginUser/
β β βββ ui/
β β βββ model/
β β βββ index.ts (μλ μμ±λ νμΌ)
β βββ ... (λ€λ₯Έ FSD ν΄λλ€)
βββ scripts/
β βββ generate-barrel.ts
βββ package.json
βββ tsconfig.json
scripts/generate-barrel.ts νμΌμ λ΄μ©:
import fs from 'fs';
import path from 'path';
function generateBarrel(directory: string) {
const files = fs.readdirSync(directory);
const exports: string[] = [];
files.forEach(file => {
if (file.startsWith('_') || file === 'index.ts') return;
const filePath = path.join(directory, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
exports.push(`export * from './${file}';`);
} else if (file.endsWith('.ts') || file.endsWith('.tsx')) {
const name = path.parse(file).name;
exports.push(`export { ${name} } from './${name}';`);
}
});
const content = exports.join('\n') + '\n';
fs.writeFileSync(path.join(directory, 'index.ts'), content);
}
const targetDir = process.argv[2];
if (!targetDir) {
console.error('Please specify a target directory');
process.exit(1);
}
const fullPath = path.resolve(process.cwd(), targetDir);
generateBarrel(fullPath);
console.log(`Barrel file generated for ${fullPath}`);
package.jsonμ μ€ν¬λ¦½νΈ μΆκ°:
{
"scripts": {
"api": "ts-node scripts/generate-barrel.ts"
}
}
μ΄μ pnpm api features/loginUser λͺ λ Ήμ΄λ₯Ό μ€ννλ©΄, src/features/loginUser/index.ts νμΌμ΄ μλμΌλ‘ μμ±λκ±°λ μ λ°μ΄νΈλ©λλ€! μλλΆν°λ μ’ λ μΌλ°μ μΈ μμλ₯Ό λ€μ΄λ³Όκ²μ.
FSD κΈ°λ³Έ ν΄λ ꡬ쑰:
src/
βββ app/ # κΈλ‘λ² μ€μ , μ€νμΌ, νλ‘λ°μ΄λ
βββ processes/ # λΉμ¦λμ€ νλ‘μΈμ€
βββ pages/ # νμ΄μ§ μ»΄ν¬λνΈ
βββ widgets/ # λ³΅ν© UI λΈλ‘
βββ features/ # μ¬μ©μ μΈν°λμ
βββ entities/ # λΉμ¦λμ€ μν°ν°
βββ shared/ # 곡μ μ νΈλ¦¬ν°, UI ν€νΈ
FSDμμμ λ°°λ΄ νμΌ μ¬μ© μμ
features/auth/index.ts (μ μ ν λ°°λ΄ νμΌ μ¬μ©):
// λͺ
μμ λ΄λ³΄λ΄κΈ° - κ³΅κ° APIλ₯Ό λͺ
νν μ μ
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/useAuth';
export type { AuthUser } from './model/types';
κ· ν μ‘ν μ κ·Όλ²: μ€μ ꡬν μμ
1. ꡬ쑰νλ λ°°λ΄ νμΌ μ¬μ©:
features/
βββ auth/
β βββ ui/
β β βββ LoginForm.tsx
β β βββ RegisterForm.tsx
β βββ model/
β β βββ useAuth.ts
β β βββ types.ts
β βββ index.ts # μν€ν
μ² κ²½κ³μλ§ λ°°λ΄ νμΌ μ¬μ©
βββ userProfile/
βββ ui/
βββ model/
βββ index.ts
2. λͺ μμ λ΄λ³΄λ΄κΈ°:
// features/auth/index.ts
// λͺ
μμ λ΄λ³΄λ΄κΈ°λ‘ κ³΅κ° API μ μ
export { LoginForm } from './ui/LoginForm';
export { RegisterForm } from './ui/RegisterForm';
export { useAuth } from './model/useAuth';
export type { User, AuthCredentials } from './model/types';
3. FSD μν€ν μ² κ·μΉμ λ°λ₯Έ κ°μ Έμ€κΈ°:
// μλͺ»λ λ°©μ: λ μ΄μ΄ μ°ν
import { Button } from '@/shared/ui/Button';
// μ¬λ°λ₯Έ λ°©μ: μν€ν
μ² κ²½κ³ μ‘΄μ€
import { Button } from '@/shared/ui';
4. μ€μ React μ»΄ν¬λνΈ μμ:
// features/auth/ui/LoginForm.tsx
import { useState } from 'react';
import { Button, TextField } from '@/shared/ui';
import { useAuth } from '../model/useAuth';
import type { AuthCredentials } from '../model/types';
export const LoginForm = () => {
const [credentials, setCredentials] = useState({
email: '',
password: ''
});
const { login, isLoading } = useAuth();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
login(credentials);
};
return (
<TextField
label="Email"
value={credentials.email}
onChange={(e) => setCredentials(prev => ({
...prev,
email: e.target.value
}))}
/>
<TextField
type="password"
label="Password"
value={credentials.password}
onChange={(e) => setCredentials(prev => ({
...prev,
password: e.target.value
}))}
/>
{isLoading ? 'Logging in...' : 'Login'}
);
};
νλμ λꡬμ μν₯
μ΅μ λΉλ λꡬλ€μ λ°°λ΄ νμΌμ μ±λ₯ λ¬Έμ λ₯Ό μΌλΆ μνν μ μμ΅λλ€:
λ²λ€λ¬λ³ μ΅μ ν μ€μ :
Vite μ€μ μμ:
// vite.config.js
export default {
build: {
target: 'esnext',
minify: 'esbuild',
rollupOptions: {
output: {
manualChunks(id) {
// μ΅μ νλ μ²ν¬ μ€μ
}
}
}
}
}
Webpack μ€μ μμ:
// webpack.config.js
module.exports = {
optimization: {
usedExports: true, // νΈλ¦¬ μμ΄νΉ νμ±ν
moduleIds: 'deterministic',
splitChunks: {
chunks: 'all',
}
}
}
μ±λ₯ μΈ‘μ - μ€μκ° νλ‘μ νΈ λͺ¨λν°λ§
// build-analyzer.ts
import { performance } from 'perf_hooks';
export function measureBuildTime(taskName: string, task: () => void) {
const startTime = performance.now();
task();
const endTime = performance.now();
console.log(`Task "${taskName}" took ${endTime - startTime}ms`);
}
// μ¬μ© μμ
measureBuildTime('Module compilation', () => {
// λΉλ λ‘μ§
});
κ²°λ‘ - νλ‘μ νΈ κ·λͺ¨λ³ κΆμ₯ μ κ·Όλ²
μκ·λͺ¨ νλ‘μ νΈ (10,000μ€ μ΄ν)
src/
βββ components/
β βββ index.ts # λ°°λ΄ νμΌ μ¬μ© κ°λ₯
βββ hooks/
β βββ index.ts # λ°°λ΄ νμΌ μ¬μ© κ°λ₯
βββ utils/
βββ index.ts # λ°°λ΄ νμΌ μ¬μ© κ°λ₯
μ€κ° κ·λͺ¨ νλ‘μ νΈ (10,000~50,000μ€)
src/
βββ features/
β βββ feature1/
β β βββ index.ts # μν€ν
μ² κ²½κ³μλ§ λ°°λ΄ νμΌ
β βββ feature2/
β βββ index.ts
βββ shared/
βββ ui/
β βββ index.ts # λͺ
μμ λ΄λ³΄λ΄κΈ° μ¬μ©
βββ lib/
βββ index.ts # λͺ
μμ λ΄λ³΄λ΄κΈ° μ¬μ©
λκ·λͺ¨ νλ‘μ νΈ (50,000μ€ μ΄μ)
src/
βββ app/
βββ processes/
βββ pages/
β βββ index.ts # μ격νκ² μ νλ λ°°λ΄ νμΌ
βββ widgets/
β βββ index.ts # μ격νκ² μ νλ λ°°λ΄ νμΌ
βββ features/
β βββ [κ° κΈ°λ₯λ³]/
β βββ index.ts # κ³΅κ° APIλ§ λͺ
μμ λ΄λ³΄λ΄κΈ°
βββ entities/
β βββ [κ° μν°ν°λ³]/
β βββ index.ts # κ³΅κ° APIλ§ λͺ
μμ λ΄λ³΄λ΄κΈ°
βββ shared/
βββ [κ° λͺ¨λλ³]/
βββ index.ts # μ±λ₯ μΈ‘μ ν κ²°μ
λ°°λ΄ νμΌμ μ¬μ©μ κ²°κ΅ κ°λ° κ²½νκ³Ό μ±λ₯ μ¬μ΄μ κ· νμ μ°Ύλ λ¬Έμ λΌ μκ°ν©λλ€.
FSD μν€ν μ²λ₯Ό λμ νλ€λ©΄, λ°°λ΄ νμΌμ μ₯λ¨μ μ μΈμνκ³ νλ‘μ νΈμ κ·λͺ¨μ νμ νμμ λ§κ² μ μ ν μ‘°μ νλ κ²μ΄ μ€μν κ² κ°μμ.
λͺ νν μν€ν μ² κ²½κ³, λͺ μμ μΈ λ΄λ³΄λ΄κΈ°, κ·Έλ¦¬κ³ νλμ μΈ λΉλ λꡬλ₯Ό νμ©νλ€λ©΄, λ°°λ΄ νμΌμ μ΄μ μ μ·¨νλ©΄μλ μ±λ₯ λ¬Έμ λ₯Ό μ΅μνν μ μμ΅λλ€!