1177 lines
33 KiB
Markdown
1177 lines
33 KiB
Markdown
# Frontend 프로젝트 가이드
|
|
|
|
Vite + React + TypeScript 기반 프론트엔드 프로젝트 개발 가이드입니다.
|
|
|
|
---
|
|
|
|
## 목차
|
|
|
|
1. [개발 규칙](#1-개발-규칙)
|
|
2. [개발 환경 및 옵션 리스팅](#2-개발-환경-및-옵션-리스팅)
|
|
3. [설치 및 동작 방법](#3-설치-및-동작-방법)
|
|
4. [아키텍처 & 책임 분리 규칙](#4-아키텍처--책임-분리-규칙)
|
|
5. [네이밍 & 패키지 규칙](#5-네이밍--패키지-규칙)
|
|
6. [예외 처리 & 에러 응답 규칙](#6-예외-처리--에러-응답-규칙)
|
|
7. [로그 & 감사(Audit) 규칙](#7-로그--감사audit-규칙)
|
|
8. [설정 관리 규칙](#8-설정-관리-규칙)
|
|
9. [보안 관련 추가 규칙](#9-보안-관련-추가-규칙)
|
|
10. [테스트 & 검증 규칙](#10-테스트--검증-규칙)
|
|
11. [금지 패턴 / 안티 패턴 목록](#11-금지-패턴--안티-패턴-목록)
|
|
12. [성능 최적화 가이드](#12-성능-최적화-가이드)
|
|
13. [의존성 관리](#13-의존성-관리)
|
|
14. [트러블슈팅 가이드](#14-트러블슈팅-가이드)
|
|
|
|
---
|
|
|
|
## 1. 개발 규칙
|
|
|
|
### 1.1 시큐어 코딩 규칙 준수
|
|
|
|
- **OWASP Top 10 기반 보안 규칙 준수 필수**
|
|
- XSS 방지: `dangerouslySetInnerHTML` 금지, 필요 시 DOMPurify 사용
|
|
- 인증 정보 저장 금지: `localStorage`/`sessionStorage`에 토큰 저장 금지
|
|
- URL 인코딩: `encodeURIComponent` 사용 필수
|
|
- `eval` / `new Function` 사용 금지
|
|
- 환경 변수에 비밀키 저장 금지
|
|
|
|
**상세 규칙은 `base_arcitectures_md/SECURE_RULE.md` 참조**
|
|
|
|
### 1.2 공통 소스 기능 및 활용 방법
|
|
|
|
#### 공통 API (`common/api/`)
|
|
|
|
- **`http.ts`**: Axios 인스턴스
|
|
- `baseURL`, `timeout`, `withCredentials` 설정
|
|
- 전역 헤더 설정
|
|
|
|
- **`endpoints.ts`**: API 엔드포인트 상수
|
|
```typescript
|
|
import { API } from "@/common/api/endpoints";
|
|
const response = await http.get(API.AUTH.ME);
|
|
```
|
|
|
|
- **`interceptors.ts`**: 전역 인터셉터
|
|
- 요청: 전역 로딩 시작
|
|
- 응답: 전역 로딩 종료, 401/403 처리
|
|
|
|
#### 공통 인증 (`common/auth/`)
|
|
|
|
- **`AuthContext`**: 인증 상태 Context
|
|
- **`AuthProvider`**: 인증 상태 제공자
|
|
- **`useAuth`**: 인증 훅
|
|
```typescript
|
|
const { user, login, logout, refreshMe } = useAuth();
|
|
```
|
|
|
|
- **`ProtectedRoute`**: 인증 필요 라우트 보호
|
|
```typescript
|
|
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
|
```
|
|
|
|
- **`authService`**: 인증 API 호출
|
|
- `login()`, `logout()`, `me()`
|
|
|
|
#### 공통 컴포넌트 (`common/components/`)
|
|
|
|
**디렉터리 구조:**
|
|
```
|
|
common/components/
|
|
├── form/ # 폼 입력 컴포넌트 (Input, Select, Textarea, Checkbox, Radio)
|
|
├── action/ # 액션 컴포넌트 (Button)
|
|
├── display/ # 표시 컴포넌트 (Table)
|
|
├── system/ # 시스템 컴포넌트 (LoadingOverlay, LoadingProvider, Toast, CommonModal)
|
|
└── index.ts # 통합 export
|
|
```
|
|
|
|
**공통 원칙:**
|
|
- 모든 공통 컴포넌트는 `id`, `name`, `className` 전달 옵션 지원
|
|
- 스타일 관련 세부 구현은 CSS로 처리 가능한 영역은 최대한 CSS에 위임
|
|
- 컴포넌트는 범용성 위주로 설계하되, 과도한 추상화는 지양
|
|
- 동작이 필요한 부분은 props 옵션으로만 제어
|
|
|
|
**입력 / 액션 계열 컴포넌트:**
|
|
|
|
- **`Button.tsx`**: 버튼 컴포넌트
|
|
```typescript
|
|
import { Button } from "@/common/components";
|
|
|
|
<Button
|
|
disabled={false}
|
|
loading={false}
|
|
size="md" // "sm" | "md" | "lg"
|
|
variant="primary" // "primary" | "secondary" | "danger" | "outline" | "ghost"
|
|
onClick={() => {}}
|
|
>
|
|
버튼 텍스트
|
|
</Button>
|
|
```
|
|
- `disabled`: 비활성화
|
|
- `loading`: 로딩 상태 (중복 클릭 방지)
|
|
- `size`: 버튼 크기 (`sm`, `md`, `lg`)
|
|
- `variant`: 버튼 스타일 변형
|
|
|
|
- **`Input.tsx`**: 입력 컴포넌트 (검색 기능 포함)
|
|
```typescript
|
|
import { Input } from "@/common/components";
|
|
|
|
// 기본 텍스트 입력
|
|
<Input
|
|
type="text"
|
|
value={value}
|
|
onChange={setValue}
|
|
format={{ formatEmail: true }} // 이메일 포맷
|
|
format={{ formatPhone: true }} // 전화번호 포맷 (010-1234-5678)
|
|
format={{ validationRegex: /^[a-z]+$/ }} // 정규식 검증
|
|
search={{
|
|
enabled: true,
|
|
debounceMs: 300,
|
|
commitOnEnter: true,
|
|
clearOnEscape: true,
|
|
onCommit: (value) => {}
|
|
}}
|
|
/>
|
|
|
|
// 비밀번호 입력
|
|
<Input type="password" value={value} onChange={setValue} />
|
|
|
|
// 숫자 입력 (숫자 이외 문자 자동 제거)
|
|
<Input type="number" value={value} onChange={setValue} />
|
|
```
|
|
- 지원 타입: `text`, `password`, `number`
|
|
- `format`: 이메일/전화번호 자동 포맷, 정규식 검증
|
|
- `search`: debounce, commitOnEnter, clearOnEscape 옵션
|
|
|
|
- **`Select.tsx`**: 선택 컴포넌트
|
|
```typescript
|
|
import { Select } from "@/common/components";
|
|
|
|
<Select
|
|
options={[
|
|
{ value: "1", label: "옵션 1" },
|
|
{ value: "2", label: "옵션 2", disabled: true }
|
|
]}
|
|
value={selectedValue}
|
|
onChange={(value) => {}}
|
|
placeholder="선택하세요"
|
|
/>
|
|
```
|
|
|
|
- **`Textarea.tsx`**: 텍스트 영역 컴포넌트
|
|
```typescript
|
|
import { Textarea } from "@/common/components";
|
|
|
|
<Textarea
|
|
value={value}
|
|
onChange={setValue}
|
|
rows={5}
|
|
/>
|
|
```
|
|
- 기본 속성: `resize: none`, `width: 100%`
|
|
|
|
- **`Checkbox.tsx`**: 체크박스 컴포넌트
|
|
```typescript
|
|
import { Checkbox } from "@/common/components";
|
|
|
|
<Checkbox
|
|
checked={checked}
|
|
onChange={setChecked}
|
|
label="체크박스 레이블"
|
|
/>
|
|
```
|
|
|
|
- **`Radio.tsx`**: 라디오 버튼 컴포넌트
|
|
```typescript
|
|
import { Radio } from "@/common/components";
|
|
|
|
<Radio
|
|
name="radio-group"
|
|
checked={selected === "value"}
|
|
onChange={(checked) => {}}
|
|
label="라디오 레이블"
|
|
/>
|
|
```
|
|
|
|
**표시 계열 컴포넌트:**
|
|
|
|
- **`Table.tsx`**: 표준 테이블 컴포넌트
|
|
```typescript
|
|
import { Table } from "@/common/components";
|
|
|
|
<Table
|
|
columns={columns}
|
|
rows={rows}
|
|
getRowId={(row) => row.id}
|
|
loading={isLoading}
|
|
emptyText="데이터가 없습니다"
|
|
onRowClick={(row) => {}}
|
|
selection={selectionState}
|
|
sort={sortValue}
|
|
onSortChange={setSortValue}
|
|
/>
|
|
```
|
|
- 로딩, 빈 상태, 선택, 정렬, 페이징 지원
|
|
|
|
- **`CommonModal.tsx`**: 공통 모달 컴포넌트
|
|
```typescript
|
|
import { CommonModal } from "@/common/components";
|
|
|
|
<CommonModal
|
|
open={isOpen}
|
|
onClose={() => setIsOpen(false)}
|
|
title="모달 제목"
|
|
showFooter={true}
|
|
onConfirm={() => {}} // 전달 시 "확인", "닫기" 버튼 모두 표시
|
|
confirmText="확인"
|
|
cancelText="닫기"
|
|
width="50vw" // 기본값
|
|
height="50vh" // 기본값
|
|
>
|
|
모달 컨텐츠
|
|
</CommonModal>
|
|
```
|
|
- 구조: 헤더(타이틀, X 닫기) / 컨텐츠 / 푸터(확인/닫기 버튼)
|
|
- `onConfirm` 전달 시: "확인", "닫기" 버튼 모두 표시
|
|
- `onConfirm` 없을 시: "확인" 버튼만 표시 (단순 닫기)
|
|
- ESC 키로 닫기 지원
|
|
- 기본 사이즈: `width: 50vw`, `height: 50vh`
|
|
|
|
**시스템 컴포넌트:**
|
|
|
|
- **`LoadingOverlay.tsx`**: 전역 로딩 오버레이
|
|
```typescript
|
|
import { LoadingOverlay } from "@/common/components";
|
|
|
|
<LoadingOverlay visible={isLoading} />
|
|
```
|
|
|
|
- **`LoadingProvider.tsx`**: 전역 로딩 상태 관리
|
|
```typescript
|
|
import { LoadingProvider, useLoading } from "@/common/components";
|
|
|
|
const { visible } = useLoading(); // API 호출 시 자동으로 로딩 표시
|
|
```
|
|
|
|
- **`Toast.tsx`**: 토스트 알림 시스템
|
|
```typescript
|
|
import { useToast } from "@/common/components";
|
|
|
|
const toast = useToast();
|
|
toast.push("작업이 완료되었습니다.", "success"); // "success" | "error" | "warning" | "info"
|
|
```
|
|
|
|
**통합 Import:**
|
|
```typescript
|
|
// 통합 import (권장) - 모든 컴포넌트를 한 번에 import
|
|
import {
|
|
Button,
|
|
Input,
|
|
Select,
|
|
Textarea,
|
|
Checkbox,
|
|
Radio,
|
|
CommonModal,
|
|
Table,
|
|
LoadingOverlay,
|
|
LoadingProvider,
|
|
useLoading,
|
|
useToast
|
|
} from "@/common/components";
|
|
|
|
// 또는 디렉터리별 import
|
|
import { Button } from "@/common/components/action";
|
|
import { Input, Select, Textarea, Checkbox, Radio } from "@/common/components/form";
|
|
import { Table } from "@/common/components/display/table";
|
|
import { CommonModal, LoadingOverlay, LoadingProvider, useToast } from "@/common/components/system";
|
|
```
|
|
|
|
#### 공통 훅 (`common/hooks/`)
|
|
|
|
- **`useFilterQuery`**: URL 검색 파라미터 관리
|
|
- 뒤로가기/앞으로가기/새로고침 시 상태 유지
|
|
```typescript
|
|
const { state, update } = useFilterQuery();
|
|
// state: { q, page, size, sort, filters }
|
|
// update({ q: "검색어" }): URL 업데이트
|
|
```
|
|
|
|
- **`useAppError`**: 에러 처리 훅
|
|
```typescript
|
|
const { handle } = useAppError();
|
|
try {
|
|
await someApiCall();
|
|
} catch (error) {
|
|
handle(error); // 자동으로 Toast 표시
|
|
}
|
|
```
|
|
|
|
#### 공통 Query (`common/query/`)
|
|
|
|
- **`queryClient.ts`**: TanStack Query 클라이언트 설정
|
|
- **`keys.ts`**: Query Key 표준화
|
|
- **`QueryProvider`**: TanStack Query 제공자 (옵션)
|
|
|
|
### 1.3 공통소스 활용해서 서비스 구현하는 규칙
|
|
|
|
1. **Pages (`features/<domain>/pages/`)**: 화면 구성만 담당
|
|
- `useFilterQuery`로 URL 상태 관리
|
|
- `useAuth`로 인증 상태 확인
|
|
- `Table`, `Input` 등 공통 컴포넌트 사용
|
|
|
|
2. **Components (`features/<domain>/components/`)**: 도메인 특화 컴포넌트
|
|
- 공통 컴포넌트를 조합하여 도메인 특화 UI 구성
|
|
|
|
3. **API 호출**: `http` 인스턴스 사용
|
|
- `endpoints.ts`의 상수 활용
|
|
- 인터셉터가 자동으로 로딩/에러 처리
|
|
|
|
4. **상태 관리**:
|
|
- 전역 상태: Context API (`AuthContext`)
|
|
- 서버 상태: TanStack Query (옵션)
|
|
- URL 상태: `useFilterQuery`
|
|
|
|
### 1.4 공통 소스 수정 규칙
|
|
|
|
- **공통 소스는 불가피한 경우가 아니면 절대 수정 금지**
|
|
- **추가(Extension)는 허용**: 새로운 훅, 유틸 함수 추가 가능
|
|
- 수정이 필요한 경우 반드시 팀 리뷰 및 승인 필요
|
|
- 공통 소스 수정 시 모든 도메인에 미치는 영향 검토 필수
|
|
|
|
---
|
|
|
|
## 2. 개발 환경 및 옵션 리스팅
|
|
|
|
### 2.1 필수 환경
|
|
|
|
- **Node.js**: 18.x 이상
|
|
- **npm**: 9.x 이상 (또는 yarn, pnpm)
|
|
- **브라우저**: Chrome, Firefox, Safari, Edge 최신 버전
|
|
|
|
### 2.2 주요 의존성
|
|
|
|
- `react`: ^19.2.0
|
|
- `react-dom`: ^19.2.0
|
|
- `react-router-dom`: ^6.25.1
|
|
- `axios`: ^1.7.9
|
|
- `typescript`: ~5.9.3
|
|
- `vite`: ^7.2.4
|
|
|
|
### 2.3 옵션 기능
|
|
|
|
- **TanStack Query**: 서버 상태 관리 (옵션)
|
|
- 활성화: `.env`에 `VITE_USE_REACT_QUERY=true`
|
|
- 비활성화: `VITE_USE_REACT_QUERY=false` 또는 미설정
|
|
|
|
### 2.4 개발 도구
|
|
|
|
- **ESLint**: 코드 품질 검사
|
|
- **TypeScript**: 타입 안정성
|
|
- **Vite**: 빠른 개발 서버 및 빌드
|
|
|
|
---
|
|
|
|
## 3. 설치 및 동작 방법
|
|
|
|
### 3.1 사전 준비
|
|
|
|
1. **Node.js 설치 확인**
|
|
```bash
|
|
node -v
|
|
npm -v
|
|
```
|
|
|
|
2. **환경 변수 파일 생성**
|
|
- `.env.development`: 개발 환경
|
|
- `.env.production`: 운영 환경
|
|
- 자세한 내용은 `ENV_SETUP.md` 참조
|
|
|
|
### 3.2 프로젝트 설정
|
|
|
|
1. **의존성 설치**
|
|
```bash
|
|
cd frontend
|
|
npm install
|
|
```
|
|
또는
|
|
```bash
|
|
npm ci
|
|
```
|
|
|
|
2. **환경 변수 설정**
|
|
- `.env.development` 파일 생성 및 설정
|
|
```env
|
|
VITE_API_BASE_URL=http://localhost:8080
|
|
VITE_API_PREFIX=/api
|
|
VITE_USE_REACT_QUERY=false
|
|
```
|
|
|
|
### 3.3 실행
|
|
|
|
1. **개발 서버 실행**
|
|
```bash
|
|
npm run dev
|
|
```
|
|
- 기본 주소: `http://localhost:5173`
|
|
|
|
2. **빌드**
|
|
```bash
|
|
npm run build
|
|
```
|
|
- 빌드 결과: `dist/` 디렉터리
|
|
|
|
3. **프리뷰 (빌드 결과 확인)**
|
|
```bash
|
|
npm run preview
|
|
```
|
|
|
|
### 3.4 확인
|
|
|
|
- 개발 서버 실행 후 브라우저에서 `http://localhost:5173` 접속
|
|
- 콘솔에서 에러 없이 로드되는지 확인
|
|
|
|
---
|
|
|
|
## 4. 아키텍처 & 책임 분리 규칙
|
|
|
|
### 4.1 계층별 책임 명확화
|
|
|
|
#### Pages (`features/<domain>/pages/`)
|
|
- **책임**: 화면 구성 및 조합
|
|
- **역할**:
|
|
- 라우팅 대상 컴포넌트
|
|
- 공통 컴포넌트 조합
|
|
- URL 상태 관리 (`useFilterQuery`)
|
|
- 인증 상태 확인 (`useAuth`)
|
|
- **금지**:
|
|
- 직접 API 호출 (Service 계층 사용 권장)
|
|
- 복잡한 비즈니스 로직 포함 ❌
|
|
|
|
#### Components (`features/<domain>/components/`)
|
|
- **책임**: 도메인 특화 UI 컴포넌트
|
|
- **역할**:
|
|
- 공통 컴포넌트 조합
|
|
- 도메인 특화 로직 (프레젠테이션 로직만)
|
|
- **금지**:
|
|
- 직접 API 호출 ❌
|
|
- 전역 상태 직접 조작 ❌
|
|
|
|
#### Common (`common/`)
|
|
- **책임**: 공통 기능 제공
|
|
- **구성**:
|
|
- `api/`: API 통신 (http, endpoints, interceptors)
|
|
- `auth/`: 인증 관리 (Context, Provider, Service)
|
|
- `components/`: 공통 UI 컴포넌트
|
|
- `hooks/`: 공통 훅
|
|
- `query/`: TanStack Query 설정 (옵션)
|
|
- `utils/`: 유틸리티 함수
|
|
- `types/`: 공통 타입
|
|
|
|
### 4.2 비즈니스 로직 위치 규칙
|
|
|
|
| 위치 | 비즈니스 로직 허용 여부 |
|
|
|------|----------------------|
|
|
| Pages | ❌ 금지 (프레젠테이션 로직만) |
|
|
| Components | ❌ 금지 (프레젠테이션 로직만) |
|
|
| Common/Utils | ❌ 금지 (순수 함수만) |
|
|
| Service (선택) | ⭕ 허용 (API 호출 및 데이터 변환) |
|
|
|
|
**참고**: 프론트엔드는 주로 프레젠테이션 로직만 포함하며, 복잡한 비즈니스 로직은 백엔드에서 처리.
|
|
|
|
**예시**:
|
|
```typescript
|
|
// ❌ 잘못된 예: Page에 복잡한 로직
|
|
function UserListPage() {
|
|
const [users, setUsers] = useState([]);
|
|
useEffect(() => {
|
|
// 복잡한 필터링/정렬 로직
|
|
const filtered = users.filter(...).sort(...);
|
|
setUsers(filtered);
|
|
}, []);
|
|
}
|
|
|
|
// ⭕ 올바른 예: 백엔드에서 필터링/정렬 처리
|
|
function UserListPage() {
|
|
const { state, update } = useFilterQuery();
|
|
const { data } = useQuery({
|
|
queryKey: ["users", state],
|
|
queryFn: () => fetchUsers(state), // 백엔드에 필터링/정렬 파라미터 전달
|
|
});
|
|
}
|
|
```
|
|
|
|
### 4.3 공통 모듈과 도메인 모듈의 경계 규칙
|
|
|
|
- **공통 모듈 (`common/`)**: 모든 도메인에서 공통으로 사용하는 기능
|
|
- 도메인 특화 로직 포함 금지
|
|
- 재사용 가능한 순수 함수/컴포넌트만
|
|
|
|
- **도메인 모듈 (`features/<domain>/`)**: 특정 도메인 전용 기능
|
|
- 다른 도메인에서 직접 참조 금지
|
|
- 도메인 간 공통 기능은 `common/`으로 이동
|
|
|
|
### 4.4 "이 로직은 어디에 두어야 하는가" 판단 기준
|
|
|
|
1. **URL 상태 관리**: `useFilterQuery` (Pages에서 사용)
|
|
2. **인증 상태**: `useAuth` (Pages/Components에서 사용)
|
|
3. **API 호출**: `http` 인스턴스 또는 TanStack Query
|
|
4. **전역 로딩**: 자동 처리 (interceptors)
|
|
5. **에러 처리**: `useAppError` 또는 TanStack Query 에러 핸들링
|
|
6. **공통 UI**: `common/components/`의 컴포넌트 사용
|
|
|
|
---
|
|
|
|
## 5. 네이밍 & 패키지 규칙
|
|
|
|
### 5.1 디렉터리 구조 규칙
|
|
|
|
- **`app/`**: 앱 레벨 설정 (라우팅, 레이아웃)
|
|
- **`common/`**: 공통 기능
|
|
- **`features/`**: 도메인별 기능
|
|
- `features/<domain>/pages/`: 페이지 컴포넌트
|
|
- `features/<domain>/components/`: 도메인 특화 컴포넌트
|
|
|
|
### 5.2 파일 / 컴포넌트 / 함수 네이밍 기준
|
|
|
|
#### 파일
|
|
- **컴포넌트**: `PascalCase.tsx` (예: `UserList.tsx`, `LoginForm.tsx`)
|
|
- **훅**: `camelCase.ts` + `use` 접두사 (예: `useAuth.ts`, `useFilterQuery.ts`)
|
|
- **유틸**: `camelCase.ts` (예: `errorMapper.ts`)
|
|
- **타입**: `camelCase.ts` (예: `api.ts`, `error.ts`)
|
|
|
|
#### 컴포넌트
|
|
- **PascalCase** 사용
|
|
- 명확한 의미 전달 (예: `UserList`, `DocumentTable`, `LoginForm`)
|
|
|
|
#### 함수/변수
|
|
- **camelCase** 사용
|
|
- **boolean**: `is`, `has`, `can` 접두사 (예: `isLoading`, `hasPermission`)
|
|
- **이벤트 핸들러**: `handle` 접두사 (예: `handleSubmit`, `handleClick`)
|
|
|
|
#### 상수
|
|
- **UPPER_SNAKE_CASE** 사용 (예: `API_BASE_URL`, `MAX_FILE_SIZE`)
|
|
|
|
### 5.3 약어 사용 허용 / 금지 리스트
|
|
|
|
#### 허용 약어
|
|
- `api`, `auth`, `config`, `util`, `props`, `ctx`, `ref`
|
|
|
|
#### 금지 약어
|
|
- `usr` (→ `user`), `svc` (→ `service`), `cmp` (→ `component`)
|
|
- `mgr` (→ `manager`), `info` (→ `information`), `num` (→ `number`)
|
|
|
|
### 5.4 API 필드 ↔ 컴포넌트 Props ↔ 상태 변수 매핑 규칙
|
|
|
|
#### API 응답 → 컴포넌트
|
|
- **카멜 케이스 유지** (JSON 기본)
|
|
- API: `{"userId": 1, "email": "user@example.com"}`
|
|
- TypeScript: `{ userId: number; email: string }`
|
|
|
|
#### 컴포넌트 Props
|
|
- **카멜 케이스** 사용
|
|
```typescript
|
|
type UserCardProps = {
|
|
userId: number;
|
|
email: string;
|
|
displayName: string;
|
|
};
|
|
```
|
|
|
|
#### 상태 변수
|
|
- **카멜 케이스** 사용
|
|
```typescript
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 예외 처리 & 에러 응답 규칙
|
|
|
|
### 6.1 공통 Exception 구조
|
|
|
|
#### AppError 타입
|
|
```typescript
|
|
type AppError = {
|
|
kind: "network" | "timeout" | "unauthorized" | "forbidden" | "notfound" | "validation" | "server" | "unknown";
|
|
status?: number;
|
|
code?: string;
|
|
message: string;
|
|
details?: unknown;
|
|
};
|
|
```
|
|
|
|
#### errorMapper
|
|
- `mapAxiosError()`: `AxiosError` → `AppError` 변환
|
|
- 자동으로 에러 종류 분류 및 사용자 친화적 메시지 제공
|
|
|
|
### 6.2 비즈니스 예외 vs 시스템 예외 구분
|
|
|
|
| 예외 타입 | 발생 위치 | 처리 방법 | 사용자 메시지 |
|
|
|----------|---------|----------|-------------|
|
|
| **비즈니스 예외** | API 응답 (4xx) | `useAppError.handle()` | 서버 메시지 또는 기본 메시지 |
|
|
| **시스템 예외** | 네트워크/타임아웃 | `useAppError.handle()` | 일반화된 메시지 |
|
|
|
|
**예시**:
|
|
```typescript
|
|
// useAppError로 자동 처리
|
|
const { handle } = useAppError();
|
|
try {
|
|
await someApiCall();
|
|
} catch (error) {
|
|
handle(error); // 자동으로 Toast 표시 및 AppError 반환
|
|
}
|
|
```
|
|
|
|
### 6.3 API 응답 포맷 통일 규칙
|
|
|
|
모든 API 응답은 다음 형식:
|
|
|
|
```typescript
|
|
type ApiResponse<T> = {
|
|
success: boolean;
|
|
data: T | null;
|
|
error: {
|
|
code: string;
|
|
message: string;
|
|
} | null;
|
|
};
|
|
```
|
|
|
|
**사용 예시**:
|
|
```typescript
|
|
const response = await http.get<ApiResponse<User>>(API.AUTH.ME);
|
|
if (response.data.success) {
|
|
const user = response.data.data; // User 타입
|
|
} else {
|
|
const error = response.data.error; // { code, message }
|
|
}
|
|
```
|
|
|
|
### 6.4 로그 기록 기준과 사용자 노출 메시지 분리 규칙
|
|
|
|
- **콘솔 로그**: 개발 환경에서만 (`import.meta.env.DEV`)
|
|
- **사용자 메시지**: Toast 또는 에러 페이지에 표시
|
|
- **민감 정보**: 절대 로그/메시지에 포함하지 않음
|
|
|
|
**예시**:
|
|
```typescript
|
|
// ❌ 잘못된 예: 민감 정보 콘솔 출력
|
|
console.log("User login:", { email, password });
|
|
|
|
// ⭕ 올바른 예: 개발 환경에서만 제한적 로깅
|
|
if (import.meta.env.DEV && import.meta.env.VITE_HTTP_LOGGING === "true") {
|
|
console.log("User login:", { email: maskEmail(email) });
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 로그 & 감사(Audit) 규칙
|
|
|
|
### 7.1 로그 레벨 사용 기준
|
|
|
|
프론트엔드는 주로 `console.log`, `console.warn`, `console.error` 사용:
|
|
|
|
| 레벨 | 사용 시기 | 예시 |
|
|
|------|---------|------|
|
|
| **console.log** | 개발 중 디버깅 | 파라미터 값, 상태 변화 |
|
|
| **console.warn** | 예상 가능한 문제 | API 응답 경고, 사용자 입력 경고 |
|
|
| **console.error** | 예상치 못한 오류 | 예외 발생, 시스템 오류 |
|
|
|
|
**주의**: 운영 빌드에서는 `console.log` 제거 (Vite가 자동 처리)
|
|
|
|
### 7.2 개인정보 및 민감정보 마스킹 규칙
|
|
|
|
**마스킹 대상**:
|
|
- 비밀번호, 토큰
|
|
- 이메일 (일부 마스킹 가능)
|
|
- 전화번호 (일부 마스킹 가능)
|
|
|
|
**마스킹 방법**:
|
|
- 직접 구현 또는 라이브러리 사용
|
|
- 로그/콘솔 출력 전 마스킹 필수
|
|
|
|
**예시**:
|
|
```typescript
|
|
// ❌ 잘못된 예: 민감 정보 콘솔 출력
|
|
console.log("User data:", user);
|
|
|
|
// ⭕ 올바른 예: 마스킹 후 출력
|
|
console.log("User data:", {
|
|
...user,
|
|
email: maskEmail(user.email),
|
|
});
|
|
```
|
|
|
|
### 7.3 공통 로깅 유틸 사용 규칙
|
|
|
|
- **Axios Interceptor**: 자동 요청/응답 로깅 (개발 환경)
|
|
- **수동 로깅**: 필요한 경우에만 `console.log` 사용
|
|
- **운영 빌드**: Vite가 자동으로 `console.log` 제거
|
|
|
|
### 7.4 요청 추적용 식별자(Request ID 등) 사용 여부
|
|
|
|
현재는 기본 구조만 제공. 필요 시 다음 추가 가능:
|
|
- Axios Interceptor에서 Request ID 생성/추가
|
|
- 응답 헤더에서 Request ID 추출
|
|
- 로그에 Request ID 포함
|
|
|
|
---
|
|
|
|
## 8. 설정 관리 규칙
|
|
|
|
### 8.1 환경 변수 파일 분리 전략
|
|
|
|
- **`.env.development`**: 개발 환경
|
|
- **`.env.production`**: 운영 환경
|
|
- **`.env.local`**: 로컬 오버라이드 (Git 제외)
|
|
|
|
**환경 변수 접두사**: `VITE_` (Vite 요구사항)
|
|
|
|
### 8.2 환경별 설정 원칙
|
|
|
|
- **공통 설정**: 코드에 하드코딩 (예: 기본 타임아웃)
|
|
- **환경별 설정**: 환경 변수 사용
|
|
- **민감 정보**: 환경 변수 사용 (절대 코드에 포함 금지)
|
|
|
|
### 8.3 옵션 처리 기준 (enable / disable 방식)
|
|
|
|
옵션 기능은 환경 변수로 제어:
|
|
|
|
```env
|
|
# TanStack Query 사용 여부
|
|
VITE_USE_REACT_QUERY=true # 또는 false
|
|
```
|
|
|
|
**코드에서 사용**:
|
|
```typescript
|
|
const useReactQuery = import.meta.env.VITE_USE_REACT_QUERY === "true";
|
|
```
|
|
|
|
### 8.4 TanStack Query 사용 여부를 옵션으로 제어하는 규칙
|
|
|
|
- **활성화**: `VITE_USE_REACT_QUERY=true`
|
|
- `QueryProvider`가 `QueryClientProvider`로 래핑
|
|
- TanStack Query 훅 사용 가능
|
|
|
|
- **비활성화**: `VITE_USE_REACT_QUERY=false` 또는 미설정
|
|
- `QueryProvider`가 children만 반환
|
|
- 직접 `http` 인스턴스로 API 호출
|
|
|
|
**예시**:
|
|
```typescript
|
|
// TanStack Query 사용 (옵션 활성화 시)
|
|
const { data } = useQuery({
|
|
queryKey: ["users"],
|
|
queryFn: () => fetchUsers(),
|
|
});
|
|
|
|
// 직접 API 호출 (옵션 비활성화 시)
|
|
const [users, setUsers] = useState([]);
|
|
useEffect(() => {
|
|
http.get(API.USER.LIST).then(res => setUsers(res.data.data));
|
|
}, []);
|
|
```
|
|
|
|
---
|
|
|
|
## 9. 보안 관련 추가 규칙 (시큐어 코딩 보강)
|
|
|
|
### 9.1 인증 / 인가 흐름 준수 규칙
|
|
|
|
#### 인증 (Authentication)
|
|
- **세션 기반 인증** 사용
|
|
- `AuthProvider`에서 `/api/auth/me` 호출로 인증 상태 확인
|
|
- 로그인 성공 시 세션 쿠키 자동 설정 (백엔드)
|
|
|
|
#### 인가 (Authorization)
|
|
- **ProtectedRoute**: 인증 필요 라우트 보호
|
|
- 백엔드에서 권한 검증 (403 Forbidden)
|
|
|
|
**예시**:
|
|
```typescript
|
|
// ProtectedRoute 사용
|
|
<Route
|
|
path="/dashboard"
|
|
element={
|
|
<ProtectedRoute>
|
|
<Dashboard />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
```
|
|
|
|
### 9.2 세션 / 토큰 사용 시 주의 사항
|
|
|
|
- **세션 쿠키**: `withCredentials: true` 설정 (Axios)
|
|
- **토큰 저장 금지**: `localStorage`/`sessionStorage`에 토큰 저장 절대 금지
|
|
- **자동 갱신**: 백엔드에서 세션 관리, 프론트엔드는 `/api/auth/me`로 상태 확인
|
|
|
|
### 9.3 파일 업로드 / 다운로드 처리 규칙
|
|
|
|
#### 업로드
|
|
1. **파일 크기 제한**: 클라이언트에서 사전 검증
|
|
2. **파일 타입 검증**: 확장자 및 MIME 타입 확인
|
|
3. **FormData 사용**: `multipart/form-data` 전송
|
|
|
|
#### 다운로드
|
|
1. **안전한 다운로드**: 백엔드에서 권한 검증 후 다운로드 URL 제공
|
|
2. **Content-Disposition**: 안전한 파일명 설정
|
|
|
|
### 9.4 외부 API 연동 시 보안 체크리스트
|
|
|
|
- [ ] CORS 설정 확인 (백엔드)
|
|
- [ ] HTTPS만 사용 (HTTP 금지)
|
|
- [ ] 타임아웃 설정 (무한 대기 방지)
|
|
- [ ] 입력값 검증 (외부 API로 전송 전)
|
|
- [ ] 응답 검증 (예상 형식 확인)
|
|
|
|
### 9.5 XSS 방지 규칙
|
|
|
|
- **`dangerouslySetInnerHTML` 금지**: HTML 직접 삽입 금지
|
|
- **필요 시 DOMPurify**: HTML 삽입이 불가피한 경우 DOMPurify로 sanitize
|
|
- **URL 인코딩**: `encodeURIComponent` 사용
|
|
- **`eval` / `new Function` 금지**: 동적 코드 실행 금지
|
|
|
|
**예시**:
|
|
```typescript
|
|
// ❌ 잘못된 예: XSS 위험
|
|
<div dangerouslySetInnerHTML={{ __html: userInput }} />
|
|
|
|
// ⭕ 올바른 예: DOMPurify 사용
|
|
import DOMPurify from "dompurify";
|
|
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
|
|
|
|
// ⭕ 더 안전한 예: 텍스트로 렌더링
|
|
<div>{userInput}</div>
|
|
```
|
|
|
|
---
|
|
|
|
## 10. 테스트 & 검증 규칙
|
|
|
|
### 10.1 단위 테스트 작성 기준 (필수 / 선택 구분)
|
|
|
|
#### 선택 테스트
|
|
- **컴포넌트 테스트**: React Testing Library 사용 (선택)
|
|
- **훅 테스트**: 커스텀 훅 테스트 (선택)
|
|
- **유틸 테스트**: 순수 함수 테스트 (선택)
|
|
|
|
**예시**:
|
|
```typescript
|
|
import { render, screen } from "@testing-library/react";
|
|
import { UserList } from "./UserList";
|
|
|
|
test("renders user list", () => {
|
|
render(<UserList users={mockUsers} />);
|
|
expect(screen.getByText("User 1")).toBeInTheDocument();
|
|
});
|
|
```
|
|
|
|
### 10.2 테스트용 데이터 작성 규칙
|
|
|
|
- **Mock 데이터**: `__mocks__/` 디렉터리에 정의
|
|
- **격리**: 각 테스트는 독립적으로 실행 가능해야 함
|
|
|
|
### 10.3 로컬 테스트 → 통합 테스트 흐름
|
|
|
|
1. **로컬 컴포넌트 테스트**: 단위 테스트
|
|
2. **로컬 통합 테스트**: 여러 컴포넌트 조합 테스트
|
|
3. **E2E 테스트**: 실제 브라우저 테스트 (선택, Playwright/Cypress)
|
|
|
|
### 10.4 테스트 미수행 시 병합 제한 여부
|
|
|
|
- 현재는 권고 사항 (필수 아님)
|
|
- 향후 CI/CD 파이프라인에서 테스트 실패 시 병합 차단 가능
|
|
|
|
---
|
|
|
|
## 11. 금지 패턴 / 안티 패턴 목록
|
|
|
|
### 11.1 공통 Util에 비즈니스 로직 포함 ❌
|
|
|
|
```typescript
|
|
// ❌ 잘못된 예
|
|
export function isAdminUser(email: string): boolean {
|
|
return email.includes("@admin.com");
|
|
}
|
|
|
|
// ⭕ 올바른 예: 도메인 로직은 해당 도메인에 위치
|
|
// (또는 백엔드에서 처리)
|
|
```
|
|
|
|
### 11.2 직접 API 호출 (인터셉터 우회) ❌
|
|
|
|
```typescript
|
|
// ❌ 잘못된 예: axios 직접 import
|
|
import axios from "axios";
|
|
const response = await axios.get("http://localhost:8080/api/users");
|
|
|
|
// ⭕ 올바른 예: 공통 http 인스턴스 사용
|
|
import { http } from "@/common/api/http";
|
|
import { API } from "@/common/api/endpoints";
|
|
const response = await http.get(API.USER.LIST);
|
|
```
|
|
|
|
### 11.3 옵션 무시 후 하드코딩 ❌
|
|
|
|
```typescript
|
|
// ❌ 잘못된 예: 옵션 무시하고 하드코딩
|
|
import { useQuery } from "@tanstack/react-query";
|
|
const { data } = useQuery({ ... });
|
|
|
|
// ⭕ 올바른 예: 옵션 확인
|
|
const useReactQuery = import.meta.env.VITE_USE_REACT_QUERY === "true";
|
|
if (useReactQuery) {
|
|
const { data } = useQuery({ ... });
|
|
} else {
|
|
const [data, setData] = useState(null);
|
|
useEffect(() => {
|
|
http.get(API.USER.LIST).then(res => setData(res.data.data));
|
|
}, []);
|
|
}
|
|
```
|
|
|
|
### 11.4 공통 코드 복사 후 개별 컴포넌트에 포함 ❌
|
|
|
|
```typescript
|
|
// ❌ 잘못된 예: 공통 로직을 각 컴포넌트에 복사
|
|
function UserList() {
|
|
const [loading, setLoading] = useState(false);
|
|
// 로딩 처리 로직 복사
|
|
}
|
|
|
|
function DocumentList() {
|
|
const [loading, setLoading] = useState(false);
|
|
// 동일한 로딩 처리 로직 복사
|
|
}
|
|
|
|
// ⭕ 올바른 예: 공통 훅/컴포넌트 사용
|
|
function UserList() {
|
|
// 인터셉터가 자동으로 로딩 처리
|
|
const { data } = useQuery({ ... });
|
|
}
|
|
```
|
|
|
|
### 11.5 localStorage/sessionStorage에 인증 정보 저장 ❌
|
|
|
|
```typescript
|
|
// ❌ 잘못된 예: 토큰을 localStorage에 저장
|
|
localStorage.setItem("token", token);
|
|
|
|
// ⭕ 올바른 예: 세션 쿠키 사용 (백엔드에서 관리)
|
|
// 프론트엔드는 withCredentials만 설정
|
|
```
|
|
|
|
### 11.6 dangerouslySetInnerHTML 사용 ❌
|
|
|
|
```typescript
|
|
// ❌ 잘못된 예: XSS 위험
|
|
<div dangerouslySetInnerHTML={{ __html: userInput }} />
|
|
|
|
// ⭕ 올바른 예: 텍스트로 렌더링 또는 DOMPurify
|
|
<div>{userInput}</div>
|
|
// 또는
|
|
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
|
|
```
|
|
|
|
### 11.7 eval / new Function 사용 ❌
|
|
|
|
```typescript
|
|
// ❌ 잘못된 예: 동적 코드 실행
|
|
eval(userInput);
|
|
new Function(userInput)();
|
|
|
|
// ⭕ 올바른 예: 안전한 방법 사용
|
|
// (동적 코드 실행이 필요한 경우는 거의 없음)
|
|
```
|
|
|
|
### 11.8 URL 상태 관리 무시 (useFilterQuery 미사용) ❌
|
|
|
|
```typescript
|
|
// ❌ 잘못된 예: useState로만 상태 관리
|
|
const [q, setQ] = useState("");
|
|
const [page, setPage] = useState(1);
|
|
|
|
// ⭕ 올바른 예: useFilterQuery 사용 (뒤로가기/새로고침 대응)
|
|
const { state, update } = useFilterQuery();
|
|
// state: { q, page, size, sort, filters }
|
|
// update({ q: "검색어" }): URL 업데이트
|
|
```
|
|
|
|
### 11.9 전역 로딩 중복 처리 ❌
|
|
|
|
```typescript
|
|
// ❌ 잘못된 예: 수동 로딩 처리
|
|
const [loading, setLoading] = useState(false);
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
await http.get(API.USER.LIST);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// ⭕ 올바른 예: 인터셉터가 자동 처리
|
|
const fetchData = async () => {
|
|
await http.get(API.USER.LIST); // 인터셉터가 자동으로 로딩 처리
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 12. 성능 최적화 가이드
|
|
|
|
### 12.1 번들 크기 최적화
|
|
|
|
#### 코드 스플리팅
|
|
- **라우트 기반 스플리팅**: React.lazy 사용
|
|
```typescript
|
|
const UserListPage = lazy(() => import("@/features/user/pages/UserListPage"));
|
|
```
|
|
|
|
#### 불필요한 의존성 제거
|
|
- **트리 쉐이킹**: 사용하지 않는 import 제거
|
|
- **대안 라이브러리**: 가벼운 라이브러리 선택 (예: `date-fns` vs `moment`)
|
|
|
|
### 12.2 렌더링 최적화
|
|
|
|
#### React 최적화
|
|
- **useMemo / useCallback**: 불필요한 재계산 방지
|
|
- **React.memo**: 컴포넌트 메모이제이션
|
|
- **가상화**: 대량 리스트는 `react-window` 사용
|
|
|
|
**예시**:
|
|
```typescript
|
|
// ❌ 잘못된 예: 매번 새로운 객체 생성
|
|
function UserCard({ user }) {
|
|
const style = { color: "blue" }; // 매 렌더링마다 새 객체
|
|
return <div style={style}>{user.name}</div>;
|
|
}
|
|
|
|
// ⭕ 올바른 예: useMemo 사용
|
|
function UserCard({ user }) {
|
|
const style = useMemo(() => ({ color: "blue" }), []);
|
|
return <div style={style}>{user.name}</div>;
|
|
}
|
|
```
|
|
|
|
### 12.3 API 호출 최적화
|
|
|
|
#### 중복 호출 방지
|
|
- **TanStack Query**: 자동 캐싱 및 중복 제거
|
|
- **Debounce**: 검색 입력 시 debounce 적용
|
|
|
|
#### 데이터 페이징
|
|
- **서버 사이드 페이징**: 대량 데이터는 페이징 필수
|
|
- **무한 스크롤**: 필요 시 `react-infinite-scroll` 활용
|
|
|
|
### 12.4 이미지 최적화
|
|
|
|
- **이미지 포맷**: WebP 사용 권장
|
|
- **이미지 크기**: 적절한 해상도로 리사이즈
|
|
- **Lazy Loading**: `loading="lazy"` 속성 사용
|
|
|
|
---
|
|
|
|
## 13. 의존성 관리
|
|
|
|
### 13.1 버전 관리 원칙
|
|
|
|
- **명시적 버전 지정**: `package.json`에 버전 명시
|
|
- **보안 패치 우선**: 취약점 발견 시 즉시 업데이트
|
|
- **마이너 버전 업데이트**: 정기적으로 검토 및 업데이트
|
|
|
|
### 13.2 보안 취약점 점검
|
|
|
|
**정기 점검**:
|
|
```bash
|
|
npm audit
|
|
npm audit fix
|
|
```
|
|
|
|
**수동 점검**:
|
|
- [npm audit](https://docs.npmjs.com/cli/v8/commands/npm-audit) 활용
|
|
- GitHub Dependabot 설정 권장
|
|
|
|
### 13.3 업데이트 프로세스
|
|
|
|
1. **의존성 업데이트**: `package.json` 수정
|
|
2. **로컬 테스트**: 업데이트 후 빌드 및 테스트
|
|
3. **통합 테스트**: 개발 환경에서 검증
|
|
4. **운영 배포**: 검증 완료 후 배포
|
|
|
|
### 13.4 lock 파일 관리
|
|
|
|
- **package-lock.json**: Git에 커밋 필수
|
|
- **npm ci 사용**: CI/CD에서는 `npm ci` 사용 (lock 파일 기반 설치)
|
|
|
|
---
|
|
|
|
## 14. 트러블슈팅 가이드
|
|
|
|
### 14.1 자주 발생하는 문제
|
|
|
|
#### 문제 1: CORS 오류
|
|
**증상**: `Access to XMLHttpRequest has been blocked by CORS policy`
|
|
|
|
**해결**:
|
|
1. 백엔드 CORS 설정 확인 (`WebMvcConfig.addCorsMappings`)
|
|
2. `withCredentials: true` 설정 확인 (프론트엔드)
|
|
3. 백엔드에서 `allowCredentials: true` 확인
|
|
4. 프록시 설정 확인 (개발 환경)
|
|
|
|
#### 문제 2: 세션이 유지되지 않음
|
|
**증상**: 로그인 후 요청 시 401 Unauthorized
|
|
|
|
**해결**:
|
|
1. `withCredentials: true` 설정 확인 (Axios)
|
|
2. CORS 설정에서 `allowCredentials: true` 확인 (백엔드)
|
|
3. 쿠키 도메인/경로 설정 확인
|
|
4. 브라우저 쿠키 차단 여부 확인
|
|
|
|
#### 문제 3: 환경 변수가 적용되지 않음
|
|
**증상**: `import.meta.env.VITE_*`가 `undefined`
|
|
|
|
**해결**:
|
|
1. 환경 변수 파일 이름 확인 (`.env.development`, `.env.production`)
|
|
2. `VITE_` 접두사 확인
|
|
3. 개발 서버 재시작
|
|
4. 빌드 시 환경 변수 파일 위치 확인
|
|
|
|
#### 문제 4: 빌드 실패 (TypeScript 오류)
|
|
**증상**: `npm run build` 실패
|
|
|
|
**해결**:
|
|
1. `npm run lint`로 오류 확인
|
|
2. TypeScript 타입 오류 수정
|
|
3. `tsconfig.json` 설정 확인
|
|
|
|
### 14.2 디버깅 팁
|
|
|
|
#### React DevTools
|
|
- **컴포넌트 트리**: 컴포넌트 상태 확인
|
|
- **Profiler**: 성능 분석
|
|
|
|
#### 브라우저 DevTools
|
|
- **Network 탭**: API 호출 확인
|
|
- **Console 탭**: 에러 메시지 확인
|
|
- **Application 탭**: 쿠키/로컬 스토리지 확인
|
|
|
|
#### Vite DevTools
|
|
- **HMR (Hot Module Replacement)**: 빠른 개발 피드백
|
|
- **에러 오버레이**: 빌드 에러 즉시 확인
|
|
|
|
---
|
|
|
|
## 참고 문서
|
|
|
|
- **아키텍처 문서**: `base_arcitectures_md/FRONT_ARCHITECTURE_V1.md`
|
|
- **보안 규칙**: `base_arcitectures_md/FRONTEND_SECURE_RULE.md`
|
|
- **공통 보안 규칙**: `../SECURE_RULE.md`
|
|
- **환경 변수 설정**: `ENV_SETUP.md`
|
|
|
|
---
|
|
|
|
**마지막 업데이트**: 2024년
|