📋 코드 감사 보고서 v1.0

measurement-system
프론트엔드 코드 감사

전체 소스코드 7개 파일 · 630+ 라인 분석 · 33개 개선 항목 도출

감사 일시 2026-03-04
총 발견 건수 33건
긴급 수정 필요 16건
기술 스택 React 19 · Vite · OpenLayers · Axios
📊
33
총 발견 항목
7개 파일 전수 분석
🔴
16
HIGH — 즉시 수정
보안·타입·접근성·버그
🟡
12
MEDIUM — 품질 개선
중복·아키텍처·에러처리
🟢
5
LOW — 선택적 개선
데드코드·미사용 prop

🔴 HIGH — 즉시 수정 필요

16건
🗺 src/components/Map/MapView.tsx
🔴 HIGH 보안 · XSS
innerHTML에 서버 데이터 직접 삽입 → XSS 취약점
Line 131–137

팝업 렌더링 시 popupRef.current.innerHTML = `...${feature.get('systemId')}...` 형태로 서버에서 받은 systemId, address 값을 직접 HTML에 삽입합니다. DB에 <img src=x onerror="alert('XSS')"> 같은 값이 저장된 경우 실행됩니다. 이는 OWASP Top 10 A03:2021 인젝션에 해당하는 취약점입니다.

// ❌ 현재 코드 (취약)
popupRef.current.innerHTML = `
  <strong>${feature.get('systemId')}</strong>
  <p>${feature.get('address')}</p>
`;

// ✅ 수정 방법 1: textContent 분리 설정
const idEl = popup.querySelector('#systemId');
if (idEl) idEl.textContent = feature.get('systemId');

// ✅ 수정 방법 2: React Portal로 팝업을 React DOM에서 렌더링
✅ 수정 방법
DOM 엘리먼트별 textContent 분리 설정 또는 React Portal을 사용하여 안전하게 렌더링
🔴 HIGH 보안 · ToS 위반
Google/Kakao 타일을 비공식 URL로 직접 호출
Line 31–64

mt0.google.com, map.daumcdn.net 등의 타일 서버 URL을 API 키 없이 직접 호출합니다. Google Maps Platform ToS 및 Kakao Maps ToS 위반으로 IP 차단 또는 서비스 중단 위험이 있으며, 특히 운영 환경에서 갑작스러운 지도 미표시가 발생할 수 있습니다.

✅ 수정 방법
내부 전용 도구라면 위험을 문서화. 외부 서비스라면 Google Maps JavaScript API 공식 키 또는 Kakao Maps REST API를 통한 공식 방식으로 교체
🔴 HIGH 성능 · GC 부담
VectorLayer 스타일 함수에서 매 렌더마다 새 Style 객체 생성
Line 88–101

style={(feature) => new Style({ image: new CircleStyle({...}) })} 형태로 VectorLayer에 인라인 스타일 함수가 전달됩니다. 지도에 피처가 렌더링될 때마다 Style, CircleStyle, Fill, Stroke, Text 객체가 새로 생성되어 피처 수가 많을수록 GC(Garbage Collection) 압박이 증가합니다.

// ❌ 현재 (매 렌더마다 객체 생성)
style={(feature) => new Style({ image: new CircleStyle(...) })}

// ✅ 개선 (컴포넌트 외부에서 캐시 함수 정의)
const styleCache = new Map<number, Style>();
const getFeatureStyle = (feature: Feature) => {
  const count = feature.get('lightSourceCount') ?? 0;
  if (!styleCache.has(count)) {
    styleCache.set(count, new Style({...}));
  }
  return styleCache.get(count)!;
};
✅ 수정 방법
lightSourceCount 값 기반 스타일 캐시 맵을 컴포넌트 외부 상수로 추출
🔴 HIGH 타입 안전성
feature.getGeometry()를 any로 캐스팅
Line 128

(feature.getGeometry() as any).getCoordinates() 형태로 OpenLayers의 Geometry 타입을 any로 캐스팅합니다. 타입 정보가 소실되어 자동완성이 동작하지 않고, 런타임에 다른 타입의 Geometry가 들어올 경우 에러를 감지할 수 없습니다.

// ❌ 현재
const coords = (feature.getGeometry() as any).getCoordinates();

// ✅ 개선
import Point from 'ol/geom/Point';
const geom = feature.getGeometry();
if (!(geom instanceof Point)) return;
const coords = geom.getCoordinates();
✅ 수정 방법
instanceof Point 가드로 타입을 좁힌 후 접근
⚛️ src/App.tsx
🔴 HIGH 타입 안전성
임포트 응답을 any 타입으로 캐스팅
Line 28

(res.data as any).imported 형태로 API 응답을 any로 캐스팅합니다. 오타 또는 백엔드 응답 변경 시 컴파일 타임에 감지되지 않고 런타임에서만 에러가 발생합니다.

// types/index.ts에 추가
export interface ImportResponse {
  imported: number;
  skipped?: number;
  errors?: string[];
}

// api/index.ts 수정
importLocations: (file: File) =>
  api.post<ImportResponse>('/import/locations', form).then(r => r.data),

// App.tsx 수정
const data = await importApi.importLocations(file); // data: ImportResponse
setImportStatus(`${data.imported}개 임포트 완료`);
✅ 수정 방법
ImportResponse 인터페이스 정의 후 API 제네릭 타입으로 지정
🔴 HIGH UX 버그
위치 등록 후 지도가 갱신되지 않음
Line 73

<LocationForm onCreated={() => {}} />onCreated 콜백이 빈 함수입니다. 위치를 성공적으로 등록해도 지도에 새 핀이 추가되지 않아 사용자가 등록 성공 여부를 알 수 없습니다.

✅ 수정 방법
onCreated에서 locationApi.getAll()을 재호출하거나, RadiusSearch 결과를 refresh하는 콜백으로 교체
🔴 HIGH 접근성 · WCAG 2.1
탭 버튼에 WAI-ARIA 속성 없음
Line 59

탭 UI에 role="tablist", role="tab", aria-selected, aria-controls 속성이 없습니다. 스크린 리더 사용자는 현재 어떤 탭이 선택되었는지 알 수 없으며, 키보드 화살표 키 내비게이션도 지원되지 않습니다. WCAG 2.1 SC 4.1.2 위반입니다.

// ✅ 접근성 있는 탭 구조
<div role="tablist">
  {tabs.map(tab => (
    <button
      role="tab"
      aria-selected={activeTab === tab.id}
      aria-controls={`panel-${tab.id}`}
      tabIndex={activeTab === tab.id ? 0 : -1}
    >{tab.label}</button>
  ))}
</div>
✅ 수정 방법
WAI-ARIA Tabs 패턴 적용 (role, aria-selected, aria-controls, tabIndex 관리)
🔴 HIGH UX
파일 업로드 중 input 비활성화 없음
Line 75–100

업로드가 진행 중일 때 파일 <input>이 비활성화되지 않습니다. 업로드 중 다른 파일을 선택하면 중복 요청이 발생할 수 있으며, 로딩 중 상태가 시각적으로 표시되지 않아 사용자가 완료 여부를 알 수 없습니다.

✅ 수정 방법
const [importing, setImporting] = useState(false) 상태 추가 후 <input disabled={importing}> 및 로딩 스피너 표시
📍 src/components/LocationForm/LocationForm.tsx
🔴 HIGH 입력 검증
빈 좌표 입력 시 Number('') = 0 으로 잘못 변환
Line 25–26

사용자가 좌표 필드를 비워두면 Number('') = 0이 되어 위도/경도 0,0 (아프리카 기니만)으로 등록됩니다. 의도하지 않은 위치에 핀이 생성되고 사용자는 에러 메시지를 받지 못합니다.

// ✅ 유효성 검사 추가
const xNum = Number(x);
const yNum = Number(y);
if (isNaN(xNum) || isNaN(yNum) || x === '' || y === '') {
  setError('유효한 좌표를 입력해주세요.');
  return;
}
if (xNum < -180 || xNum > 180 || yNum < -90 || yNum > 90) {
  setError('경도는 -180~180, 위도는 -90~90 범위여야 합니다.');
  return;
}
✅ 수정 방법
src/utils/validation.ts 생성 후 좌표 범위 유효성 검사 함수 공유
🔴 HIGH 접근성 · WCAG 2.1
모든 input에 label 없음, 좌표 입력이 type="text"
Line 42–68

스크린 리더는 placeholder를 레이블로 인식하지 않습니다. WCAG 2.1 SC 1.3.1 위반입니다. 또한 좌표 입력 필드가 type="text"로 설정되어 있어 모바일에서 숫자/소수점 키패드가 표시되지 않습니다.

// ✅ label 연결 + 숫자 입력
<label htmlFor="coord-x">경도 (X)</label>
<input
  id="coord-x"
  type="number"
  inputMode="decimal"
  step="any"
  min="-180"
  max="180"
  placeholder="126.978"
/>
✅ 수정 방법
htmlFor/id 연결 + type="number" + inputMode="decimal"
🔴 HIGH 에러 처리
catch 블록에서 에러 무시
Line 31

catch { setError('오류가 발생했습니다.') } 형태로 에러 객체를 무시합니다. 서버가 반환하는 중복 ID 에러, 주소 변환 실패 등의 구체적인 메시지를 사용자에게 전달하지 못합니다.

// ✅ 에러 메시지 추출
} catch (err) {
  const msg = err instanceof AxiosError
    ? err.response?.data?.message ?? err.message
    : '알 수 없는 오류가 발생했습니다.';
  setError(msg);
}
✅ 수정 방법
AxiosError 타입 가드로 서버 응답 메시지를 추출하여 표시
🔍 src/components/RadiusSearch/RadiusSearch.tsx
🔴 HIGH 입력 검증
빈값·음수 반경 검증 없음, 좌표 0 변환 동일 문제
Line 22–24, 77–78

반경 입력이 비어있거나 음수인 경우에도 API를 호출합니다. 좌표 모드에서는 LocationForm과 동일하게 Number('') = 0 변환 문제가 발생합니다. 백엔드에서 radius=0으로 요청되면 의미 없는 빈 결과가 반환됩니다.

✅ 수정 방법
src/utils/validation.ts의 공유 유효성 함수로 좌표·반경 모두 검증
📏 src/components/MeasurementForm/MeasurementForm.tsx
🔴 HIGH 비즈니스 로직
API 파라미터 구조 혼용 (URL 파라미터 vs 바디 systemId)
Line 18

locationApi.addMeasurement(locationId, { systemId, ... }) 호출 시 URL 경로에 위치 ID(locationId)를 사용하면서 바디에도 별도의 systemId를 전달합니다. 함수 시그니처의 첫 번째 파라미터가 systemId로 명명되어 위치 ID와 측정값 ID가 혼동됩니다.

✅ 수정 방법
api/index.ts에서 addMeasurement(systemId, ...)addMeasurement(locationId, ...) 로 파라미터명 일관성 통일

🟡 MEDIUM — 품질 개선

12건
🔁 코드 중복 (3개 파일 공통)
🟡 MEDIUM 코드 중복
inputStyle 상수가 3개 파일에 각각 중복 선언
LocationForm:86 / MeasurementForm:62 / RadiusSearch:88

동일한 inputStyle, buttonStyle 객체가 3개 컴포넌트에 각각 복사되어 있습니다. 스타일 변경 시 3곳을 모두 수정해야 하며, 실수로 불일치가 발생할 수 있습니다.

// ✅ src/styles/common.ts 생성
export const inputStyle = {
  width: '100%',
  padding: '8px',
  marginBottom: '8px',
  border: '1px solid #ccc',
  borderRadius: '4px',
  fontSize: '14px',
} as const satisfies React.CSSProperties;
✅ 수정 방법
src/styles/common.ts에 공유 스타일 상수 추출, 3개 컴포넌트에서 import
🔌 src/api/index.ts
🟡 MEDIUM 에러 처리 · 아키텍처
Axios 에러 인터셉터 없음 — 에러 처리 분산
Line 4–6

HTTP 401, 403, 500 등 서버 에러가 각 컴포넌트의 catch 블록으로 버블링됩니다. 모든 컴포넌트에서 에러를 개별 처리하므로 공통 에러(네트워크 끊김, 서버 다운 등)에 대한 일관된 UX를 제공하기 어렵습니다.

// ✅ Axios 인터셉터 추가
api.interceptors.response.use(
  response => response,
  error => {
    const status = error.response?.status;
    if (status === 401) { /* 인증 처리 */ }
    if (status >= 500) {
      console.error('서버 오류:', error.response?.data);
    }
    return Promise.reject(error);
  }
);
✅ 수정 방법
api.interceptors.response.use()로 HTTP 상태 코드별 공통 에러 처리 중앙화
🟡 MEDIUM 보안 · 설정
VITE_API_URL 누락 시 localhost로 폴백 + .env.example 없음
Line 5

import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1' — 환경변수 미설정 시 로컬호스트로 폴백됩니다. 배포 환경에서 이 상태로 배포하면 브라우저가 localhost:8080에 요청을 보내 모든 API가 실패합니다. .env.example 파일도 없어 신규 팀원이 설정을 모를 수 있습니다.

✅ 수정 방법
.env.example 생성 + 환경변수 누락 시 앱 시작을 막는 early validation 추가
🎨 App.css — 데드 코드
🟡 MEDIUM 데드 코드
Vite 기본 템플릿 CSS 전체가 잔존
Line 1–43 (App.css 전체)

.logo, .card, .read-the-docs, @keyframes logo-spin 등 Vite 초기 템플릿 CSS가 전부 잔존합니다. 이 클래스들은 실제 앱에서 사용되지 않으며, 번들에 불필요한 CSS가 포함됩니다.

✅ 수정 방법
App.css 전체 삭제 후 실제 앱에 필요한 스타일만 재작성
🗂 src/App.tsx — 아키텍처
🟡 MEDIUM React 성능
tabs 배열이 렌더링마다 새로 생성됨
Line 34–38

const tabs = [{ id: 'map', label: '지도' }, ...]가 컴포넌트 본문 안에 선언되어 렌더링마다 새 배열이 생성됩니다. 탭 항목은 정적 데이터이므로 컴포넌트 외부 상수로 이동해야 합니다.

// ✅ 컴포넌트 외부 상수
const TABS = [
  { id: 'map', label: '🗺 지도' },
  { id: 'add', label: '📍 위치 추가' },
  { id: 'measure', label: '📏 측정값' },
  { id: 'import', label: '📂 데이터 가져오기' },
] as const;
✅ 수정 방법
컴포넌트 외부 const TABS = [...]로 이동

🟢 LOW — 선택적 개선

5건
🟢 LOW HTML 메타데이터
index.html — lang="en", title="frontend"
index.html:2, 6

브라우저 탭 제목이 "frontend"로 표시되고, 스크린 리더가 문서 언어를 영어로 인식합니다.

✅ 수정
lang="ko", <title>측정 시스템</title>
🟢 LOW 번들 크기
axios → 네이티브 fetch 교체 검토
api/index.ts:1

현재 axios의 고급 기능(취소 토큰 등)을 사용하지 않습니다. 네이티브 fetch로 교체 시 ~2.5MB 의존성 제거 가능합니다.

✅ 수정
에러 인터셉터 도입 후 함께 검토
🟢 LOW 미사용 prop
MapView의 centerCoords prop이 미전달
MapView.tsx:184, App.tsx

App.tsx에서 MapViewcenterCoords를 전달하지 않아 기능이 동작하지 않습니다.

✅ 수정
prop 활성화 또는 미사용 prop 제거
🟢 LOW 성능
setTimeout으로 map.updateSize() — ResizeObserver로 교체
MapView.tsx:150

100ms 타임아웃은 레이아웃 완료를 보장하지 않습니다. 느린 디바이스에서 지도가 잘릴 수 있습니다.

✅ 수정
ResizeObserver + map.updateSize()
🟢 LOW 타입 표준화
@types/geojson 패키지로 GeoJSON 타입 표준화
types/index.ts:17–33

자체 정의한 GeoJsonFeature, GeoJsonFeatureCollection을 npm의 @types/geojson 표준 타입으로 교체하면 GeoJSON 스펙 전체를 커버하고 유지보수 부담이 줄어듭니다.

✅ 수정
npm i -D @types/geojsonimport type { Feature, FeatureCollection } from 'geojson'
🚨 우선 수정 대상 파일
파일 경로
분류
핵심 이슈
src/components/Map/MapView.tsx
보안
innerHTML XSS 취약점 (즉시 수정), 스타일 함수 최적화, any 캐스팅
src/api/index.ts
타입
ImportResponse 타입 미정의, 에러 인터셉터 부재, 파라미터명 혼용
src/App.tsx
아키텍처
any 캐스팅, onCreated 빈 콜백, WAI-ARIA 탭 구조, tabs 배열 최적화
src/components/LocationForm/LocationForm.tsx
검증
좌표 입력 검증 부재, label 없음, catch 에러 무시
vite.config.ts
성능
manualChunks 미설정 — ol 번들 분리로 초기 로드 개선 필요

🗺 개선 계획 로드맵

Phase 1
보안 & 버그 수정
1~2일 · 즉시 시작
  • MapView innerHTML XSS 수정
  • ImportResponse 타입 정의
  • Axios 에러 인터셉터 추가
  • onCreated 콜백 실제 동작 연결
  • 파일 업로드 중 input 비활성화
Phase 2
코드 품질 & 중복 제거
2~3일
  • src/styles/common.ts 추출
  • src/utils/validation.ts 생성
  • API 파라미터명 정리
  • App.css 데드 코드 전체 삭제
  • .env.example 파일 생성
Phase 3
성능 & 번들 최적화
2~3일
  • vite manualChunks (ol 분리)
  • MapView 스타일 캐시 함수
  • tabs 배열 모듈 상수 이동
  • ResizeObserver 교체
  • axios → fetch 교체 검토
Phase 4
UX & 접근성
3~4일
  • 모든 input에 label 연결
  • WAI-ARIA 탭 구조 적용
  • 좌표 input type="number"
  • index.html lang/title 수정
  • centerCoords prop 활성화
Phase 5
테스트 커버리지 확대
병행 가능
  • @testing-library/user-event 추가
  • 컴포넌트 인터랙션 테스트
  • MSW 기반 API 모킹 테스트
  • MapView 스모크 테스트
  • validation 유틸 단위 테스트