확장 가능한 React 애플리케이션 구축: 실제 프로젝트에서 얻은 교훈
Source: Dev.to
작성자: Borifan Dabasa | 풀 스택 개발자
“Just Start Coding”의 문제점
제가 첫 번째 React 앱을 만들었을 때, 구조를 고민하지 않고 모든 것을 컴포넌트에 집어넣었습니다. 3개월이 지나자 저는 prop drilling, 중복 로직, 그리고 너무 많은 역할을 하는 컴포넌트에 빠져 허우적거리고 있었습니다.
익숙한 이야기인가요?
1. 확장 가능한 폴더 구조
src/
├── components/
│ ├── common/ # Reusable UI components
│ ├── layout/ # Layout components
│ └── features/ # Feature‑specific components
├── hooks/ # Custom hooks
├── context/ # Context providers
├── services/ # API calls
├── utils/ # Helper functions
├── constants/ # Constants and configs
└── pages/ # Page components
왜 이렇게 작동하나요: 각 폴더는 단일 책임을 가집니다. 무언가를 찾아야 할 때 정확히 어디를 보면 되는지 알 수 있습니다.
2. 복잡성보다 구성 요소 조합
이것을 힘들게 배웠습니다. 초기 시절에 만든 컴포넌트는 다음과 같습니다:
// ❌ Bad: God component doing everything
function UserDashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
// 200 lines of logic...
return (
{/* 300 lines of JSX */}
);
}
이제 이렇게 나눕니다:
// ✅ Good: Composed components
function UserDashboard() {
return (
{/* composed sub‑components go here */}
);
}
경험 법칙: 컴포넌트가 150줄을 넘는다면, 아마도 너무 많은 일을 하고 있는 것입니다.
3. 로직 재사용을 위한 커스텀 훅
커스텀 훅은 내 삶을 바꾸었습니다. 컴포넌트 간에 로직을 복사하는 대신, 이를 추출합니다:
// hooks/useAuth.js
export function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(setUser);
setLoading(false);
return unsubscribe;
}, []);
return { user, loading };
}
// Now use it anywhere
function Profile() {
const { user, loading } = useAuth();
if (loading) return ;
return {user.name};
}
나는 커스텀 훅을 다음과 같이 사용합니다:
- API 호출 (
useFetch,useApi) - 폼 처리 (
useForm) - 로컬 스토리지 (
useLocalStorage) - 디바운싱 (
useDebounce)
4. State Management: Keep It Simple
I see developers reaching for Redux immediately. Here’s my approach:
- Local state for component‑specific data
- Context for app‑wide data (theme, auth)
- Redux/Zustand only when Context becomes messy
For my e‑commerce project, I used Context for cart and auth. That’s it. No Redux needed.
// context/CartContext.js
export function CartProvider({ children }) {
const [cart, setCart] = useState([]);
const addToCart = (item) => {
setCart([...cart, item]);
};
return (
{/* provide cart context to children */}
{children}
);
}
5. 성능 최적화
코드 분할
// Lazy load heavy components
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
{/* render routes with Dashboard */}
);
}
메모이제이션
// Prevent unnecessary re‑renders
const ExpensiveComponent = memo(({ data }) => {
return {/* Heavy computation */};
});
가상 리스트
내 암호화폐 트래커는 1,000개 이상의 코인을 다루므로 react-window를 사용합니다:
import { FixedSizeList } from 'react-window';
{/* render CoinRow inside FixedSizeList */}
6. 오류 경계
프로덕션 버그가 발생했을 때 이것이 큰 도움이 되었습니다:
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return ;
}
return this.props.children;
}
}
// 앱을 감싸세요
<ErrorBoundary>
<App />
</ErrorBoundary>
7. 환경 변수
API URL을 절대로 하드코딩하지 마세요:
# .env
REACT_APP_API_URL=https://api.example.com
REACT_APP_API_KEY=your_key_here
// Use in code
const API_URL = process.env.REACT_APP_API_URL;
실제 예시: 내 블로그 프로젝트
내 Next.js 블로그 (live at )에서 다음을 구현했습니다:
- 파일 기반 라우팅 자동 코드 분할을 위해
- ISR (증분 정적 재생성) 빠른 로드를 위해
- TypeScript 타입 안전성을 위해
# Tailwind CSS for consistent styling
**Result?** Lighthouse score of **95+** and sub‑second load times.
핵심 요점
- 구조가 중요합니다 – 파일 유형이 아니라 기능별로 조직하세요
- 구성하되 복잡하게 하지 마세요 – 작고 집중된 컴포넌트
- 로직을 추출하세요 – 커스텀 훅은 당신의 친구입니다
- 현명하게 최적화하세요 – 최적화하기 전에 측정하세요
- 오류에 대비하세요 – 오류 경계는 프로덕션을 구합니다
다음은?
현재 탐색 중인 내용:
- Next.js 14의 서버 컴포넌트
- 더 나은 데이터 페칭을 위한 React Query
- 대규모 앱을 위한 Micro‑frontends
이 패턴들을 실제로 보고 싶으신가요? 제 프로젝트를 GitHub에서 확인해 보세요.
질문이 있나요?
댓글을 남기거나 연락 주세요.
저와 연결하기
- GitHub: @Borifan02
- LinkedIn: Borifan Dabasa
태그: #WebDevelopment #JavaScript #Frontend #MERN
