프론트엔드 애플리케이션 개발
React와 TypeScript를 사용한 대규모 프론트엔드 애플리케이션 개발은 타입 안전성과 개발 생산성을 크게 향상시킵니다.
이 절에서는 프론트엔드 애플리케이션 개발의 다양한 측면을 살펴봅니다.
프로젝트 설정 및 컴포넌트 구조화
- Create React App을 사용한 프로젝트 생성
npx create-react-app my-app --template typescript
- 컴포넌트 구조화 예시
// src/components/Button/Button.tsx
import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
export const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false }) => (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
- 재사용 가능한 컴포넌트 라이브러리 구축
// src/components/index.ts
export { Button } from './Button/Button';
export { Input } from './Input/Input';
export { Card } from './Card/Card';
상태 관리
Zustand를 사용한 타입 안전 상태 관리
// src/store/useStore.ts
import create from 'zustand';
interface BearState {
bears: number;
increase: (by: number) => void;
}
export const useStore = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}));
// 사용 예
import React from 'react';
import { useStore } from '../store/useStore';
export const BearCounter: React.FC = () => {
const bears = useStore((state) => state.bears);
const increase = useStore((state) => state.increase);
return (
<div>
<h1>{bears} around here...</h1>
<button onClick={() => increase(1)}>Add a bear</button>
</div>
);
};
라우팅 및 코드 스플리팅
React Router와 React.lazy를 사용한 라우팅 및 코드 스플리팅
// src/App.tsx
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));
const Contact = React.lazy(() => import('./pages/Contact'));
const App: React.FC = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</Switch>
</Suspense>
</Router>
);
폼 처리 및 유효성 검사
React Hook Form을 사용한 폼 처리
// src/components/LoginForm.tsx
import React from 'react';
import { useForm } from 'react-hook-form';
import { yup } from '@hookform/resolvers';
interface LoginFormData {
email: string;
password: string;
}
const schema = yup.object().shape({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
});
export const LoginForm: React.FC = () => {
const { register, handleSubmit, errors } = useForm<LoginFormData>({
validationSchema: schema,
});
const onSubmit = (data: LoginFormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input name="email" ref={register} />
{errors.email && <span>{errors.email.message}</span>}
<input name="password" type="password" ref={register} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Login</button>
</form>
);
};
API 통신
Axios와 React Query를 사용한 API 통신
// src/api/users.ts
import axios from 'axios';
import { useQuery } from 'react-query';
interface User {
id: number;
name: string;
email: string;
}
const fetchUsers = async (): Promise<User[]> => {
const { data } = await axios.get<User[]>('https://api.example.com/users');
return data;
};
export const useUsers = () => {
return useQuery<User[], Error>('users', fetchUsers);
};
// 사용 예
import React from 'react';
import { useUsers } from '../api/users';
export const UserList: React.FC = () => {
const { data: users, isLoading, error } = useUsers();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>An error occurred: {error.message}</div>;
return (
<ul>
{users?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
테스팅
Jest와 React Testing Library를 사용한 테스팅
// src/components/Button.test.tsx
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button component', () => {
it('renders with correct label', () => {
const { getByText } = render(<Button label="Click me" onClick={() => {}} />);
expect(getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
const { getByText } = render(<Button label="Click me" onClick={handleClick} />);
fireEvent.click(getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
성능 최적화
React.memo와 useMemo를 사용한 성능 최적화
// src/components/ExpensiveComponent.tsx
import React, { useMemo } from 'react';
interface ExpensiveComponentProps {
data: number[];
}
export const ExpensiveComponent: React.FC<ExpensiveComponentProps> = React.memo(({ data }) => {
const processedData = useMemo(() => {
return data.map((item) => item * 2);
}, [data]);
return (
<ul>
{processedData.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
});
접근성(a11y) 구현
접근성을 고려한 컴포넌트 예시
// src/components/AccessibleButton.tsx
import React from 'react';
interface AccessibleButtonProps {
label: string;
onClick: () => void;
ariaLabel?: string;
}
export const AccessibleButton: React.FC<AccessibleButtonProps> = ({
label,
onClick,
ariaLabel
}) => (
<button
onClick={onClick}
aria-label={ariaLabel || label}
>
{label}
</button>
);
프론트엔드 아키텍처 가이드라인
1. 컴포넌트 설계
- 단일 책임 원칙 준수
- 프레젠테이션과 컨테이너 컴포넌트 분리
- 재사용 가능한 컴포넌트 라이브러리 구축
2. 상태 관리
- 전역 상태와 지역 상태 구분
- 불변성 유지
- 상태 정규화
3. 타입 안전성
- 엄격한 타입 체크 설정 (strict : true)
- any 타입 사용 최소화
- 유니온 타입과 제네릭 활용
4. 성능 최적화
- 컴포넌트 메모이제이션 (React.memo, useMemo, useCallback)
- 코드 스플리팅 및 레이지 로딩
- 가상화 기법 활용 (대량의 데이터 렌더링 시)
5. 테스팅
- 단위 테스트, 통합 테스트, E2E 테스트 구현
- 테스트 커버리지 목표 설정 및 유지
- 스냅샷 테스트 활용
6. 코드 품질
- ESLint와 Prettier를 사용한 일관된 코드 스타일 유지
- 주석과 문서화
- 코드 리뷰 프로세스 확립
7. 접근성
- WCAG 가이드라인 준수
- 키보드 네비게이션 지원
- 스크린 리더 호환성 확보
8. 빌드 및 배포
-
환경별 빌드 설정 (개발, 스테이징, 프로덕션)
-
CI/CD 파이프라인 구축
-
번들 크기 최적화 9. 에러 처리
-
전역 에러 바운더리 구현
-
사용자 친화적인 에러 메시지 제공
-
로깅 및 모니터링 시스템 연동
10. 보안
- XSS 방지
- CSRF 대응
- 안전한 데이터 저장 (LocalStorage, SessionStorage 사용 시 주의)
React와 TypeScript를 함께 사용하여 프론트엔드 애플리케이션을 개발할 때 타입 안전성과 개발 생산성을 동시에 높일 수 있습니다.
컴포넌트 기반 아키텍처를 통해 재사용성과 유지보수성을 향상시키고 상태 관리 솔루션을 활용하여 복잡한 애플리케이션 상태를 효과적으로 관리할 수 있습니다.