| 항목 | 체크 | 답변 기록 | 추가 질문 |
|---|---|---|---|
| JWT 토큰 형태 (access/refresh) | □ | ||
| 토큰 만료 시간 | □ | ||
| 토큰 갱신 방식 | □ | ||
| CORS 허용 도메인 | □ | ||
| 인증 헤더 형식 | □ |
| 항목 | 체크 | 답변 기록 | 추가 질문 |
|---|---|---|---|
| 서버 개발 스택 | □ | ||
| API 문서 제공 방식 | □ | ||
| API 기본 URL 구조 | □ | ||
| API 버전 관리 방식 | □ | ||
| REST API vs GraphQL | □ |
| 항목 | 체크 | 답변 기록 | 추가 질문 |
|---|---|---|---|
| 실시간 데이터 처리 방식 | □ | ||
| 웨이퍼 데이터 구조 | □ | ||
| 페이지네이션 방식 | □ | ||
| 에러 응답 구조 | □ | ||
| 대용량 데이터 처리 방식 | □ |
| 항목 | 체크 | 답변 기록 | 추가 질문 |
|---|---|---|---|
| 개발 서버 정보 | □ | ||
| 스테이징 서버 정보 | □ | ||
| 테스트 계정/데이터 | □ | ||
| CI/CD 파이프라인 | □ |
| 항목 | 체크 | 답변 기록 | 추가 질문 |
|---|---|---|---|
| API 응답시간 기준 | □ | ||
| 데이터 캐싱 전략 | □ | ||
| 동시접속 처리 방안 | □ | ||
| 대용량 처리 최적화 | □ |
| 항목 | 체크 | 답변 기록 | 추가 질문 |
|---|---|---|---|
| 권한 체계 | □ | ||
| 민감 데이터 처리 | □ | ||
| API 보안 헤더 | □ | ||
| 데이터 암호화 방식 | □ |
| 항목 | 체크 | 답변 기록 | 추가 질문 |
|---|---|---|---|
| 코드 저장소 정보 | □ | ||
| API 변경 공유 방식 | □ | ||
| 이슈 트래킹 도구 | □ | ||
| 커뮤니케이션 채널 | □ | ||
| 정기 회의 일정 | □ |
| 항목 | 체크 | 답변 기록 | 추가 질문 |
|---|---|---|---|
| API 테스트 환경 | □ | ||
| 테스트 데이터 제공 | □ | ||
| 통합 테스트 방식 | □ | ||
| QA 프로세스 | □ |
| 항목 | 체크 | 답변 기록 | 추가 질문 |
|---|---|---|---|
| 에러 모니터링 도구 | □ | ||
| 로그 수집 방식 | □ | ||
| 알림 설정 | □ | ||
| 성능 모니터링 | □ |
회의 중 추가로 논의된 중요 사항들을 기록하세요:
회의 후 필요한 후속 조치들을 기록하세요:
질문: "로그인 성공시 어떤 형태의 토큰을 받게 되나요?"
예상 답변:
// 패턴 1: accessToken + refreshToken
{
"accessToken": "eyJhbGciOiJIUzI1...",
"refreshToken": "eyJhbGciOiJIUzI1..."
}
// 패턴 2: accessToken만 사용
{
"accessToken": "eyJhbGciOiJIUzI1..."
}
프론트엔드 적용:
// src/services/auth.ts
interface LoginResponse {
accessToken: string;
refreshToken?: string;
}
const login = async (username: string, password: string): Promise<LoginResponse> => {
const response = await axios.post('/api/auth/login', { username, password });
// 토큰 저장
localStorage.setItem('accessToken', response.data.accessToken);
if (response.data.refreshToken) {
localStorage.setItem('refreshToken', response.data.refreshToken);
}
return response.data;
};
질문: "토큰이 만료되면 어떻게 갱신하나요?"
예상 답변:
// 패턴 1: refresh token 사용
"POST /api/auth/refresh로 refreshToken을 보내주시면 새로운 accessToken을 발급해드립니다."
// 패턴 2: 자동 갱신
"토큰 만료 10분 전에 자동으로 새로운 토큰을 발급합니다."
// 패턴 3: 재로그인
"토큰 만료시 다시 로그인해야 합니다."
프론트엔드 적용:
// src/services/axios.ts
import axios from 'axios';
// axios 인터셉터 설정
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/auth/refresh', { refreshToken });
localStorage.setItem('accessToken', response.data.accessToken);
// 실패했던 요청 재시도
return axios(error.config);
} catch {
// 갱신 실패시 로그인 페이지로
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
질문: "API 문서는 어떤 형태로 제공되나요?"
예상 답변:
// 패턴 1: Swagger
"Swagger UI를 통해 제공됩니다. 주소는 http://api-server/swagger-ui.html 입니다."
// 패턴 2: Postman
"Postman 컬렉션 파일을 공유드리겠습니다."
// 패턴 3: 정적 문서
"Git 저장소의 /docs 폴더에 마크다운 형식으로 제공됩니다."
프론트엔드 적용:
// src/api/types.ts - Swagger에서 생성된 타입 정의
interface WaferData {
id: string;
timestamp: string;
measurements: {
temperature: number;
pressure: number;
};
}
// src/api/wafers.ts
const getWaferData = async (id: string): Promise<WaferData> => {
const response = await axios.get(`/api/wafers/${id}`);
return response.data;
};
질문: "API 엔드포인트 구조는 어떻게 되나요?"
예상 답변:
// 패턴 1: REST 기반
"/api/v1/wafers - 웨이퍼 관련 엔드포인트
/api/v1/measurements - 계측 데이터 관련 엔드포인트"
// 패턴 2: 기능 기반
"/api/wafer-management/measurements
/api/user-management/permissions"
프론트엔드 적용:
// src/api/constants.ts
export const API_ENDPOINTS = {
WAFERS: '/api/v1/wafers',
MEASUREMENTS: '/api/v1/measurements',
AUTH: '/api/v1/auth'
} as const;
// src/api/wafers.ts
import { API_ENDPOINTS } from './constants';
export const getWaferList = () =>
axios.get(API_ENDPOINTS.WAFERS);
질문: "실시간 데이터는 어떤 방식으로 받게 되나요?"
예상 답변:
// 패턴 1: WebSocket
"ws://api-server/wafers/{waferId}/measurements 로 연결하시면 됩니다."
// 패턴 2: Server-Sent Events
"GET /api/wafers/stream 엔드포인트로 SSE 연결을 하시면 됩니다."
프론트엔드 적용:
// src/services/websocket.ts
class WaferWebSocket {
private ws: WebSocket;
constructor(waferId: string) {
this.ws = new WebSocket(`ws://api-server/wafers/${waferId}/measurements`);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 데이터 처리
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
disconnect() {
this.ws.close();
}
}
질문: "API 에러는 어떤 형식으로 반환되나요?"
예상 답변:
// 패턴 1: 표준 에러 객체
{
"code": "WAFER_NOT_FOUND",
"message": "웨이퍼를 찾을 수 없습니다.",
"status": 404
}
// 패턴 2: 상세 에러 정보
{
"error": {
"code": "VALIDATION_ERROR",
"message": "유효하지 않은 입력입니다.",
"details": [
{
"field": "temperature",
"message": "온도는 0~100 사이여야 합니다."
}
]
}
}
프론트엔드 적용:
// src/types/api.ts
interface ApiError {
code: string;
message: string;
status: number;
details?: Array<{
field: string;
message: string;
}>;
}
// src/services/error-handler.ts
const handleApiError = (error: unknown) => {
if (axios.isAxiosError(error) && error.response) {
const apiError = error.response.data as ApiError;
switch (apiError.code) {
case 'VALIDATION_ERROR':
toast.error('입력값을 확인해주세요');
break;
case 'UNAUTHORIZED':
window.location.href = '/login';
break;
default:
toast.error(apiError.message);
}
} else {
toast.error('알 수 없는 에러가 발생했습니다.');
}
};
이러한 내용들은 프로젝트 초기 구조를 잡는데 매우 중요합니다. 특히 인증 시스템과 실시간 데이터 처리 방식은 전체 애플리케이션 아키텍처에 큰 영향을 미치므로, 초기에 명확히 해두는 것이 좋습니다.
// 패턴 1: 일반적인 기준
"일반 API: 1초 이내
대용량 데이터: 3초 이내
실시간 데이터: 100ms 이내"
// 패턴 2: 상황별 기준
"- 웨이퍼 목록 조회: 500ms 이내
- 상세 데이터 조회: 1초 이내
- 대시보드 데이터: 2초 이내
- 보고서 생성: 5초 이내"
// src/services/api-client.ts
import axios from 'axios';
const API_TIMEOUT = {
DEFAULT: 5000, // 5초
REPORT: 10000, // 10초
REALTIME: 1000 // 1초
};
// API 클라이언트 설정
const createApiClient = (type: 'DEFAULT' | 'REPORT' | 'REALTIME') => {
return axios.create({
timeout: API_TIMEOUT[type],
headers: {
'Content-Type': 'application/json',
}
});
};
// 타임아웃 처리
const handleTimeout = async (promise: Promise<any>) => {
try {
const result = await promise;
return result;
} catch (error) {
if (axios.isAxiosError(error) && error.code === 'ECONNABORTED') {
showToast('요청 시간이 초과되었습니다. 다시 시도해주세요.');
}
throw error;
}
};
// 패턴 1: 페이지네이션
"한 페이지당 100건씩 페이지네이션으로 제공
정렬/필터링은 서버에서 처리"
// 패턴 2: 청크 단위 전송
"최대 1MB 단위로 청크 분할 전송
/api/data?chunk=1 형식으로 요청"
// 패턴 3: 스트리밍
"대용량 데이터는 WebSocket을 통해 스트리밍 방식으로 전송"
// src/hooks/usePagination.ts
interface PaginationParams {
page: number;
pageSize: number;
filters?: Record<string, any>;
}
const usePagination = <T>(
fetchFn: (params: PaginationParams) => Promise<T[]>
) => {
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchPage = async (params: PaginationParams) => {
setLoading(true);
try {
const result = await fetchFn(params);
setData(prev => [...prev, ...result]);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
return { data, loading, error, fetchPage };
};
// 사용 예시
const WaferList = () => {
const { data, loading, fetchPage } = usePagination(fetchWafers);
// 무한 스크롤 구현
const handleScroll = useCallback((entries: IntersectionObserverEntry[]) => {
const target = entries[0];
if (target.isIntersecting && !loading) {
fetchPage({ page: currentPage, pageSize: 100 });
}
}, [loading, currentPage]);
return (
<VirtualizedList
data={data}
renderItem={(item) => <WaferItem data={item} />}
onEndReached={handleScroll}
/>
);
};
// 패턴 1: 시간 기반 캐싱
"- 마스터 데이터: 1시간
- 사용자 정보: 30분
- 실시간 데이터: 캐싱하지 않음"
// 패턴 2: 이벤트 기반 캐싱
"데이터 업데이트 이벤트 발생 시 캐시 무효화"
// src/services/cache.ts
import { QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 기본 캐시 설정
staleTime: 5 * 60 * 1000, // 5분
cacheTime: 30 * 60 * 1000, // 30분
},
},
});
// 캐시 설정
const CACHE_CONFIG = {
masterData: {
staleTime: 60 * 60 * 1000, // 1시간
cacheTime: 2 * 60 * 60 * 1000, // 2시간
},
userInfo: {
staleTime: 30 * 60 * 1000, // 30분
cacheTime: 60 * 60 * 1000, // 1시간
},
realtime: {
staleTime: 0, // 항상 최신 데이터 fetch
cacheTime: 0,
},
};
// 캐시 무효화 함수
const invalidateCache = async (key: string) => {
await queryClient.invalidateQueries({ queryKey: [key] });
};
// WebSocket 이벤트로 캐시 무효화
socket.on('dataUpdated', (event) => {
invalidateCache(event.dataType);
});
// 패턴 1: 데이터 최적화
"- 필요한 필드만 선택적으로 전송
- 응답 데이터 압축 사용
- 이미지/파일은 CDN 사용"
// 패턴 2: 처리 최적화
"- 무거운 연산은 서버에서 처리
- 클라이언트는 표시용 데이터만 처리
- 실시간 데이터는 변경된 부분만 전송"
// src/hooks/useOptimizedData.ts
interface OptimizationOptions {
debounceMs?: number;
batchSize?: number;
virtualizeThreshold?: number;
}
const useOptimizedData = <T>(
data: T[],
options: OptimizationOptions = {}
) => {
// 데이터가 많은 경우 가상화 적용
const shouldVirtualize = data.length > (options.virtualizeThreshold ?? 1000);
// 실시간 업데이트 최적화
const debouncedUpdate = useMemo(
() => debounce((newData: T[]) => {
setProcessedData(newData);
}, options.debounceMs ?? 300),
[options.debounceMs]
);
// 대용량 데이터 처리 최적화
const processInBatches = useCallback((items: T[]) => {
const batchSize = options.batchSize ?? 100;
let processed = 0;
const processBatch = () => {
const batch = items.slice(processed, processed + batchSize);
// 배치 처리 로직
processed += batchSize;
if (processed < items.length) {
requestAnimationFrame(processBatch);
}
};
processBatch();
}, [options.batchSize]);
return {
processedData,
shouldVirtualize,
updateData: debouncedUpdate,
processInBatches
};
};
이러한 성능 최적화 전략들은 프로젝트 초기에 백엔드 팀과 합의하고, 프론트엔드 아키텍처에 반영하는 것이 중요합니다. 특히 실시간 데이터를 다루는 시스템에서는 데이터 처리 방식과 캐싱 전략이 매우 중요한 요소가 됩니다.
// 패턴 1: 역할 기반 권한
"ADMIN, MANAGER, OPERATOR 등 역할별로 권한 구분
각 API 문서에 필요한 역할이 명시되어 있음"
// 패턴 2: 리소스 기반 권한
"웨이퍼 조회, 수정, 삭제 등 작업별 권한 부여
특정 설비나 공정에 대한 접근 권한 별도 관리"
// src/types/auth.ts
type UserRole = 'ADMIN' | 'MANAGER' | 'OPERATOR';
interface Permission {
resource: string;
actions: Array<'read' | 'write' | 'delete'>;
}
// src/hooks/usePermission.ts
const usePermission = () => {
const user = useSelector(selectUser);
const checkPermission = useCallback((
requiredRole: UserRole | UserRole[]
) => {
const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
return roles.includes(user.role);
}, [user.role]);
return { checkPermission };
};
// 사용 예시
const WaferControl = () => {
const { checkPermission } = usePermission();
return (
<div>
{checkPermission('OPERATOR') && (
<button onClick={handleProcess}>공정 시작</button>
)}
{checkPermission(['ADMIN', 'MANAGER']) && (
<button onClick={handleDelete}>데이터 삭제</button>
)}
</div>
);
};
// 패턴 1: 데이터 분류
"- 사용자 개인정보: 이름, 전화번호 등
- 설비 정보: 일련번호, 보정값 등
- 공정 데이터: 특정 파라미터값"
// 패턴 2: 마스킹 규칙
"- 이름: 홍*동 형식으로 표시
- 전화번호: 010-****-5678 형식
- 민감 수치: 권한에 따라 표시 여부 결정"
// src/utils/security.ts
const maskingRules = {
name: (value: string) => {
if (!value) return '';
return value.replace(/(?<=.)./g, '*');
},
phone: (value: string) => {
if (!value) return '';
return value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-****-$3');
},
number: (value: number, userRole: UserRole) => {
if (userRole === 'ADMIN') return value;
return '**.**';
}
};
// src/components/SecureData.tsx
interface SecureDataProps {
type: keyof typeof maskingRules;
value: string | number;
}
const SecureData = ({ type, value }: SecureDataProps) => {
const { role } = useSelector(selectUser);
const maskedValue = maskingRules[type](value, role);
return <span>{maskedValue}</span>;
};
// 사용 예시
const UserInfo = ({ user }) => (
<div>
<SecureData type="name" value={user.name} />
<SecureData type="phone" value={user.phone} />
</div>
);
// 패턴 1: 입력값 처리
"모든 사용자 입력은 HTML 인코딩 필요
특수문자는 이스케이프 처리"
// 패턴 2: 출력값 처리
"API 응답의 텍스트 데이터는 항상 sanitize 처리
스크립트 태그 등은 제거 또는 이스케이프"
// src/utils/security.ts
import DOMPurify from 'dompurify';
const sanitizeInput = (value: string): string => {
return DOMPurify.sanitize(value, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: []
});
};
// src/hooks/useSafeForm.ts
const useSafeForm = <T extends Record<string, any>>(
onSubmit: (data: T) => void
) => {
const handleSubmit = useCallback((data: T) => {
const sanitizedData = Object.entries(data).reduce(
(acc, [key, value]) => ({
...acc,
[key]: typeof value === 'string' ? sanitizeInput(value) : value
}),
{} as T
);
onSubmit(sanitizedData);
}, [onSubmit]);
return { handleSubmit };
};
// 사용 예시
const CommentForm = () => {
const { handleSubmit } = useSafeForm<{ content: string }>(
async (data) => {
await submitComment(data);
}
);
return (
<form onSubmit={handleSubmit}>
<textarea name="content" />
<button type="submit">작성</button>
</form>
);
};
// 패턴 1: 기본 보안 헤더
"- Authorization: Bearer JWT
- X-CSRF-Token: 필수
- Content-Security-Policy 준수"
// 패턴 2: 추가 인증
"민감 작업은 2차 인증(OTP) 필요
특정 IP에서만 접근 가능한 API 존재"
// src/services/api-security.ts
import axios from 'axios';
const secureApiClient = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
}
});
// 보안 헤더 인터셉터
secureApiClient.interceptors.request.use((config) => {
// JWT 토큰
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// CSRF 토큰
const csrfToken = getCsrfToken();
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
// 민감 API 호출시 2차 인증
const callSecureApi = async (
endpoint: string,
data: any,
requireOtp: boolean = false
) => {
if (requireOtp) {
const otpToken = await requestOtpVerification();
return secureApiClient.post(endpoint, data, {
headers: {
'X-OTP-Token': otpToken
}
});
}
return secureApiClient.post(endpoint, data);
};
보안과 관련된 설정은 프로젝트 초기에 명확히 해두어야 하며, 특히 민감한 데이터를 다루는 시스템에서는 더욱 중요합니다. 백엔드 팀과의 긴밀한 협의를 통해 보안 정책을 수립하고, 프론트엔드에서 이를 철저히 구현해야 합니다.
// 패턴 1: 문서 기반 관리
"Swagger 문서가 자동으로 업데이트됩니다.
주요 변경사항은 Slack 채널에 공지됩니다."
// 패턴 2: 버전 관리
"- API 버전이 변경됨을 사전에 공지
- 이전 버전은 1개월간 유지
- release notes를 통해 변경사항 공유"
// src/config/api-version.ts
const API_VERSION = {
CURRENT: 'v1',
DEPRECATED: [], // 곧 제거될 버전들
SUPPORTED: ['v1', 'v2-beta'] // 지원되는 버전들
};
// src/services/api-client.ts
const createApiClient = (version = API_VERSION.CURRENT) => {
return axios.create({
baseURL: `/api/${version}`,
headers: {
'Api-Version': version
}
});
};
// 변경된 API 마이그레이션 예시
const migrateToNewApi = async () => {
try {
// 신규 API 호출
const response = await createApiClient('v2-beta')
.get('/wafers');
// 새로운 응답 구조 처리
return transformResponse(response.data);
} catch (error) {
// 실패시 이전 버전 API 사용
const fallbackResponse = await createApiClient('v1')
.get('/wafers');
return fallbackResponse.data;
}
};
// 패턴 1: Jira 사용
"Jira에서 이슈 관리
- Bug: 버그 리포트
- Task: 일반 작업
- Story: 기능 개발"
// 패턴 2: GitHub Issues
"GitHub Issues로 관리
PR과 이슈 연동하여 사용"
// src/utils/error-reporting.ts
interface BugReport {
title: string;
steps: string[];
expected: string;
actual: string;
environment: {
browser: string;
os: string;
screenSize: string;
};
logs?: string;
}
const createBugReport = async (error: Error, context: any): Promise<BugReport> => {
const report = {
title: error.message,
steps: context.userActions || [],
expected: context.expectedBehavior,
actual: error.stack,
environment: {
browser: navigator.userAgent,
os: navigator.platform,
screenSize: `${window.innerWidth}x${window.innerHeight}`
},
logs: await collectLogs()
};
// Jira API로 이슈 생성
await createJiraIssue({
project: 'FRONTEND',
issueType: 'Bug',
...report
});
return report;
};
// 패턴 1: PR 기반 리뷰
"- PR 생성 시 최소 1명의 승인 필요
- PR 템플릿 준수
- CI 통과 필수"
// 패턴 2: 페어 리뷰
"- 페어 프로그래밍 권장
- 주간 코드 리뷰 미팅
- 코드 품질 메트릭 체크"
// .github/pull_request_template.md
/**
* ## 변경사항
* -
*
* ## 테스트 방법
* 1.
* 2.
*
* ## 스크린샷
*
* ## 체크리스트
* - [ ] 테스트 코드 작성
* - [ ] 문서 업데이트
* - [ ] 브라우저 호환성 체크
*/
// src/utils/git-hooks/commit-msg
const commitRules = {
pattern: /^(feat|fix|docs|style|refactor|test|chore)(\([a-z]+\))?: .+/,
examples: [
'feat: 로그인 기능 추가',
'fix(auth): 토큰 갱신 버그 수정',
'docs: API 문서 업데이트'
]
};
// 패턴 1: 도구별 용도
"- Slack: 일상 소통
- Email: 공식 문서/결정사항
- Teams: 화상 회의
- Jira: 이슈 관리"
// 패턴 2: 상황별 채널
"- 일반 문의: 팀 채널
- 긴급 장애: 비상 연락망
- 코드 리뷰: PR 댓글"
// src/config/contact.ts
const CONTACT_CHANNELS = {
GENERAL: {
tool: 'Slack',
channel: '#team-frontend',
responseTime: '1시간 이내'
},
URGENT: {
tool: 'Mobile',
contacts: [
{ role: 'FE Lead', number: '010-****-****' },
{ role: 'BE Lead', number: '010-****-****' }
],
responseTime: '15분 이내'
},
CODE_REVIEW: {
tool: 'GitHub',
process: 'PR Comments',
responseTime: '24시간 이내'
}
};
// src/utils/error-handling.ts
const handleCriticalError = async (error: Error) => {
// 에러 로깅
await logError(error);
// Slack 알림
await sendSlackAlert({
channel: CONTACT_CHANNELS.URGENT.channel,
text: `🚨 긴급 에러 발생: ${error.message}`,
attachments: [{
title: '에러 상세',
text: error.stack
}]
});
};
효과적인 협업을 위해서는 명확한 커뮤니케이션 채널과 프로세스가 필수적입니다. 특히 프론트엔드와 백엔드 팀 간의 원활한 소통을 위해 API 변경 관리와 이슈 트래킹 프로세스를 잘 정립해두어야 합니다.