Files
sam-react-prod/claudedocs/guides/[PLAN-2025-12-19] project-health-improvement.md
byeongcheolryu d7f491fa84 refactor: 로딩 스피너 표준화 및 프로젝트 헬스 개선
- LoadingSpinner 컴포넌트 5가지 변형 구현
  - LoadingSpinner (인라인/버튼용)
  - ContentLoadingSpinner (상세/수정 페이지)
  - PageLoadingSpinner (페이지 전환)
  - TableLoadingSpinner (테이블/리스트)
  - ButtonSpinner (버튼 내부)
- 18개+ 페이지 로딩 UI 표준화
  - HR 페이지 (사원, 휴가, 부서, 급여, 근태)
  - 영업 페이지 (견적, 거래처)
  - 게시판, 팝업관리, 품목기준정보
- API 키 보안 개선 (NEXT_PUBLIC_API_KEY → API_KEY)
- Textarea 다크모드 스타일 개선
- DropdownField Radix UI Select 버그 수정 (key prop)
- 프로젝트 헬스 개선 계획서 문서화

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 14:33:11 +09:00

11 KiB

프로젝트 헬스 개선 계획서

작성일: 2025-12-19 최종 업데이트: 2025-12-20 목적: 프로젝트 구조, 성능, 안정성 개선


현황 요약

영역 상태 핵심 이슈
빌드 설정 해결됨 98개 타입 에러 무시 → 0개, API 키 노출 → 서버 사이드 이동
상태관리 🟡 개선 필요 ItemMasterContext 과부하 (13개 상태)
Next.js 활용 🔴 심각 259개 'use client', Server Component 미활용
디자인 일관성 완료 다크모드 일부 미완성 → 다크모드 스타일 완성 (2025-12-20)

Phase 1: 긴급 (이번 주)

1.1 TypeScript 에러 해결 + ignoreBuildErrors 제거

현재 상태:

// next.config.ts
typescript: { ignoreBuildErrors: true }  // 98개 에러 숨김
eslint: { ignoreDuringBuilds: true }

작업 내용:

Step 1: 타입 에러 카테고리 분류

카테고리 개수 예시
모델 타입 불일치 ~26개 Employee에 concurrentPosition 없음
Props 미스매치 ~35개 IntegratedListTemplateV2 props 변경
배열 타입 불일치 ~9개 PricingListItem 타입 정의
기타 ~28개 -

Step 2: 수정 순서

  1. src/types/ 폴더의 모델 타입 정의 업데이트
  2. IntegratedListTemplateV2 Props 인터페이스 정리
  3. 페이지별 타입 에러 수정
  4. ignoreBuildErrors: false 변경
  5. npm run build 성공 확인

예상 소요: 2-3시간

위험도: 🔴 높음 (빌드 실패 가능)


1.2 API 키 서버 사이드 이동

현재 상태:

# .env.local
NEXT_PUBLIC_API_KEY=42Jfwc6EaR...      # 브라우저에서 노출됨!
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=...   # 브라우저에서 노출됨!

문제점:

  • NEXT_PUBLIC_ 접두사 → 클라이언트 번들에 포함
  • 브라우저 개발자도구에서 확인 가능
  • API 남용/해킹 위험

작업 내용:

Step 1: 환경변수 이름 변경

# .env.local (수정 후)
API_KEY=42Jfwc6EaR...                    # 서버만 접근
GOOGLE_MAPS_API_KEY=AIzaSyAS3bA...       # 서버만 접근

# 클라이언트에서 필요한 공개 정보만
NEXT_PUBLIC_API_BASE_URL=https://api.example.com

Step 2: 서버 사이드 프록시 확인

// src/app/api/proxy/[...path]/route.ts
// 이미 구현됨 - API_KEY를 서버에서 주입
const response = await fetch(url, {
  headers: {
    'Authorization': `Bearer ${process.env.API_KEY}`,  // 서버에서만 접근
  },
});

Step 3: Google Maps 처리

// 옵션 A: 서버 사이드 렌더링
// 옵션 B: API 라우트로 프록시
// 옵션 C: Maps Embed API 사용 (키 제한 설정)

예상 소요: 30분-1시간

위험도: 🟡 중간 (dev 서버 재시작 필요)


1.3 ThemeContext SSR 수정

현재 상태:

// src/contexts/ThemeContext.tsx
const [theme, setThemeState] = useState<Theme>("light");

useEffect(() => {
  const savedTheme = localStorage.getItem("theme");  // SSR에서 에러 가능
  if (savedTheme) {
    setThemeState(savedTheme);
  }
}, []);

문제점:

  • 서버에서 localStorage 접근 시 에러
  • Hydration mismatch 발생 가능

작업 내용:

수정 코드

// src/contexts/ThemeContext.tsx (수정 후)
const [theme, setThemeState] = useState<Theme>(() => {
  // SSR 안전 체크
  if (typeof window === 'undefined') return 'light';

  const savedTheme = localStorage.getItem('theme');
  return (savedTheme as Theme) || 'light';
});

// 또는 useEffect 패턴 유지 (더 안전)
const [theme, setThemeState] = useState<Theme>('light');
const [isHydrated, setIsHydrated] = useState(false);

useEffect(() => {
  setIsHydrated(true);
  const savedTheme = localStorage.getItem('theme');
  if (savedTheme) {
    setThemeState(savedTheme as Theme);
  }
}, []);

예상 소요: 15분

위험도: 🟢 낮음 (HMR 즉시 반영)


Phase 2: 단기 (2주)

2.1 ItemMasterContext 분할

현재 상태:

ItemMasterContext
├── 품목 데이터 (3개 상태)
├── 기준정보 (7개 상태)
├── 폼 구조 (4개 상태)
└── 50개+ 메서드

→ ANY 상태 변경 시 전체 리렌더링

개선 방향:

ItemMasterDataContext     → 품목 기본 데이터
ItemFormContext           → 페이지/섹션/필드 구조
ItemLookupContext         → 단위/재질/처리방식 등 기준정보

작업 내용:

  1. Context 분할 설계
  2. 각 Context별 Provider 구현
  3. 기존 useItemMaster → 새 Context hooks로 마이그레이션
  4. 테스트

예상 소요: 1-2일

위험도: 🟡 중간 (기존 코드 변경 필요)


2.2 IntegratedListTemplate → Zustand Store

현재 상태:

// 20개+ props 전달
<IntegratedListTemplateV2
  searchValue={searchValue}
  onSearchChange={setSearchValue}
  currentPage={currentPage}
  onPageChange={setCurrentPage}
  selectedItems={selectedItems}
  onSelectionChange={setSelectedItems}
  // ... 더 많은 props
/>

개선 방향:

// Zustand store
const useListStore = create((set) => ({
  // 페이지네이션
  currentPage: 1,
  pageSize: 20,
  setPage: (page) => set({ currentPage: page }),

  // 필터/검색
  searchValue: '',
  filters: {},
  setSearch: (value) => set({ searchValue: value }),

  // 선택
  selectedIds: new Set(),
  toggleSelection: (id) => set((state) => { /* ... */ }),
}));

// 컴포넌트에서 사용
function MyListPage() {
  const { currentPage, setPage } = useListStore();
  return <IntegratedListTemplateV2 />; // props 최소화
}

작업 내용:

  1. src/store/listStore.ts 생성
  2. 공통 리스트 상태 추출
  3. 페이지별 점진적 마이그레이션
  4. IntegratedListTemplateV2 리팩토링

예상 소요: 2-3일

위험도: 🟡 중간


2.3 다크모드 스타일 완성

현재 상태:

// Button - 일부 variant만 dark: 정의
ghost: "hover:bg-accent hover:text-accent-foreground"  // dark: 없음
outline: "border-input bg-background"                   // dark: 없음

작업 내용:

  1. 모든 UI 컴포넌트 다크모드 스타일 점검
  2. Button, Select, Input 등 주요 컴포넌트 수정
  3. 색상 대비 검증 (WCAG AA 기준)
  4. 다크모드 테스트

예상 소요: 1일

위험도: 🟢 낮음


Phase 3: 중기 (1개월)

3.1 주요 페이지 Server Component 전환

현재 상태:

  • 259개 'use client' 컴포넌트
  • 모든 데이터 페칭: useEffect 내 클라이언트 fetch
  • 초기 로딩 지연

개선 방향:

// Before (Client Component)
'use client';
export default function ItemPage() {
  const [items, setItems] = useState([]);
  useEffect(() => {
    fetch('/api/items').then(r => r.json()).then(setItems);
  }, []);
  return <ItemList items={items} />;
}

// After (Server Component)
export default async function ItemPage() {
  const items = await fetch('/api/items').then(r => r.json());
  return <ItemList items={items} />;  // 클라이언트로 props 전달
}

마이그레이션 우선순위:

  1. 정적 페이지 (설정, 정보 페이지)
  2. 리스트 페이지 (items, employees)
  3. 상세 페이지

예상 소요: 1-2주

위험도: 🟡 중간


3.2 캐싱 전략 수립

현재 상태:

cache: 'no-store'  // 모든 fetch에 적용 → 성능 저하

개선 방향:

데이터 유형 캐싱 전략 TTL
정적 데이터 (카테고리, 단위) force-cache 1시간
사용자 데이터 no-store -
리스트 데이터 revalidate: 60 1분
상세 데이터 revalidate: 300 5분

작업 내용:

  1. 데이터 유형별 분류
  2. fetch 옵션 표준화
  3. revalidateTag/revalidatePath 활용
  4. 성능 측정

예상 소요: 3-5일

위험도: 🟢 낮음


3.3 TanStack Query 도입 검토

도입 이점:

  • API 상태 자동 관리 (loading, error, data)
  • 캐싱 및 백그라운드 리페치
  • 낙관적 업데이트
  • DevTools 지원

도입 시 구조:

// hooks/useItems.ts
export function useItems() {
  return useQuery({
    queryKey: ['items'],
    queryFn: () => itemApi.getAll(),
    staleTime: 5 * 60 * 1000,  // 5분
  });
}

// 컴포넌트에서 사용
function ItemList() {
  const { data, isLoading, error } = useItems();
  // 자동으로 loading/error 처리
}

검토 포인트:

  • 현재 API 호출 패턴 분석
  • 도입 시 마이그레이션 범위
  • 번들 사이즈 영향 (~20KB)
  • 팀 학습 비용

예상 소요: 1-2주 (검토 + 파일럿)

위험도: 🟡 중간 (큰 변화)


체크리스트 요약

Phase 1 (긴급) 완료 (2025-12-20)

  • 1.1 타입 에러 해결 + ignoreBuildErrors 제거
    • 98개 → 0개 에러 수정
    • npx tsc --noEmit 성공
    • npm run build 성공
  • 1.2 API 키 서버 사이드 이동
    • NEXT_PUBLIC_API_KEYAPI_KEY 변경
    • 프록시 라우트에서 서버 사이드 주입
  • 1.3 ThemeContext SSR 수정
    • typeof window 체크 추가

Phase 2 (단기) 📅

  • 2.1 ItemMasterContext 3개로 분할
  • 2.2 IntegratedListTemplate → Zustand store
  • 2.3 다크모드 스타일 완성 (2025-12-20)
    • Textarea: Input과 스타일 통일 (dark:bg-input/30 추가)
    • 모든 UI 컴포넌트 다크모드 지원 확인:
      • Button, Select, Input (dark: 스타일 적용됨)
      • Card, Dialog, Sheet, Popover (CSS 변수로 처리)
      • Table, DropdownMenu (CSS 변수 + dark: 스타일)
      • Badge, Checkbox, RadioGroup, Switch (dark: 스타일 적용됨)
      • Alert, Tabs (CSS 변수 + dark: 스타일)
  • 2.4 로딩 스피너 표준화 (2025-12-20)
    • loading-spinner.tsx 5가지 변형 컴포넌트 구현:
      • LoadingSpinner: 인라인/버튼용 (xs, sm, md, lg 사이즈)
      • ContentLoadingSpinner: 상세/수정 페이지용 (min-h-[400px])
      • PageLoadingSpinner: 페이지 전환용 (min-h-[calc(100vh-200px)])
      • TableLoadingSpinner: 테이블/리스트용 (py-16)
      • ButtonSpinner: 버튼 내부 스피너
    • 18개+ 페이지 표준화 적용:
      • HR 페이지 (사원, 휴가, 부서, 급여, 근태관리)
      • 품목기준정보관리, 게시판, 팝업관리
      • 견적관리 상세/수정
    • 빌드 테스트 성공 (231 pages)

Phase 3 (중기) 📆

  • 3.1 주요 페이지 Server Component 전환
  • 3.2 캐싱 전략 수립
  • 3.3 TanStack Query 도입 검토

참고 자료

  • 분석 리포트: 2025-12-19 프로젝트 헬스체크
  • 관련 문서:
    • claudedocs/guides/[GUIDE-2025-12-16] options-vs-flattened-data.md
    • claudedocs/guides/[PLAN-2025-12-19] page-layout-standardization.md