들어가며
안녕하세요, 여러분! 오늘은 제가 최근에 참가 준비 중인 「산림과학기술과 인공지능(AI)이 함께 찾는 해답」 아이디어 공모전을 위해 만든 프로토타입을 공유하려고 합니다.
솔직히 처음엔 막막했어요. "산림 재해를 AI로 어떻게 해결한다는 거지?" 라는 생각부터 시작했죠. 하지만 자료를 찾아보니 우리나라가 정말 심각한 상황이더라고요. 2025년 3월에만 강원도와 경북 일대에서 48,000헥타르가 불에 탔고, 소나무재선충병으로 연간 840억원의 피해가 발생하고 있다는 걸 알게 됐습니다.
그래서 "데이터 분석가로서 뭘 할 수 있을까?" 고민하다가, 실제로 작동하는 프로토타입을 만들어서 아이디어를 시각적으로 보여주기로 결정했습니다. 그 결과물이 바로 이 K-Forest AI 시스템입니다.
🏆 공모전 분석: 왜 이 시스템을 만들었나?
공모전 개요
- 주최: 산림청 & 한국임업진흥원
- 주제: 산림과학기술과 AI의 융합
- 목적: 산림 분야 현안 해결을 위한 혁신적 아이디어 발굴
제가 주목한 핵심 문제들:
공모전 자료를 분석하면서 발견한 한국 산림의 3대 위기:
1. 산불 조기 감지의 한계
현재 상황: 위성 탐지 2-4시간 지연
우리의 목표: 10분 이내 실시간 감지
2. 소나무재선충병 확산
감염 현황: 142개 시군구, 9,000만 그루
문제점: 육안 조사로는 전체 산림의 0.01%만 검사 가능
3. 산사태 예측 실패
2024년 산청 산사태: 11명 사망
원인: 5년 주기 갱신 시스템의 한계
이런 문제들을 보면서 "아, 이건 단순히 아이디어만 제출하는 게 아니라, 실제로 어떻게 작동할지 보여줘야겠다"는 생각이 들었어요.
개발 과정: 아이디어에서 코드로
Step 1: 기술 스택 선정 과정
처음엔 Vue나 Angular도 고민했는데, 결국 React를 선택한 이유:
// 왜 React를 선택했나?
const whyReact = {
1: "공모전 심사위원들이 바로 브라우저에서 확인 가능",
2: "복잡한 상태 관리를 Hooks로 심플하게 해결",
3: "Mapbox, Chart.js와의 궁합이 최고",
4: "한 파일로 모든 걸 보여줄 수 있음 (중요!)"
};
특히 단일 HTML 파일로 만든 건 심사위원분들이 별도 설치 없이 바로 실행해볼 수 있게 하려는 전략이었습니다.
Step 2: 핵심 기능 설계
🗺️ 지도 레이어 시스템 구현기
가장 고민이 많았던 부분이 바로 지도였어요. 처음엔 그냥 마커만 찍으면 되겠지 싶었는데...
// 초기 아이디어: 단순 마커
// ❌ 문제: 너무 심심하고 실제 데이터처럼 안 보임
// 개선 1차: 히트맵 추가
// ❌ 문제: 여전히 정적임
// 최종: 다중 레이어 + 실시간 애니메이션
const layers = {
fires: true, // 산불 위험도 (빨간색 그라데이션)
ndvi: false, // 식생 건강도 (초록색 스펙트럼)
rainfall: false, // 강우량 (파란색 폴리곤)
drones: true // 드론 순찰 (실시간 이동)
};
특히 드론 애니메이션은 정말 신경 썼어요. RequestAnimationFrame을 써서 부드럽게 움직이도록:
// 드론 비행 경로 애니메이션
let progress = 0;
const animateDrone = () => {
progress += 0.005; // 속도 조절
if (progress > 1) progress = 0;
// 원형 경로로 순찰
const lng = 127.7 + Math.sin(progress * Math.PI * 2) * 0.5;
const lat = 36.5 + Math.cos(progress * Math.PI * 2) * 0.3;
droneMarker.setLngLat([lng, lat]);
animationRef.current = requestAnimationFrame(animateDrone);
};
이렇게 하니까 진짜 드론이 순찰하는 것처럼 보이더라고요!
📊 차트 구현의 시행착오
Chart.js 연동하면서 겪은 문제들:
// 문제 1: 차트가 중복 생성됨
// 해결: useRef로 인스턴스 관리
const chartsRef = useRef({});
// 문제 2: 리렌더링 시 차트 깨짐
// 해결: cleanup 함수에서 destroy()
useEffect(() => {
return () => {
Object.values(chartsRef.current).forEach(chart => {
if (chart) chart.destroy();
});
};
}, []);
// 문제 3: 데이터 업데이트가 안 됨
// 해결: update() 메소드 활용
chartsRef.current.sensor.update('none'); // 애니메이션 없이 즉시 업데이트
Step 3: 자원 관리 시스템 - 현실감 더하기
공모전 심사위원들이 "이게 진짜 쓸 수 있겠네?"라고 생각하게 만들고 싶었어요.
// 실제 한국에서 사용하는 장비들로 구성
const resources = [
{
id: 'heli1',
name: '소방헬기 KA-32', // 실제 모델명
type: '항공자원',
status: '대기중',
capacity: 3000, // 실제 스펙
range: 150 // 실제 작전반경
},
{
id: 'heli2',
name: '소방헬기 S-64', // 스카이크레인
capacity: 8000, // KA-32보다 훨씬 큼
// ...
}
];
그리고 현장배치 버튼을 누르면 실제처럼 3단계로 진행되도록:
const handleDeploy = (resource) => {
// 1단계: 배치 명령
showToast('success', '현장 배치', `${resource.name} 배치 명령 전달`);
// 2단계: 이동 중 (2초 후)
setTimeout(() => {
showToast('info', '이동 중', `목표 지점까지 15km 남음`);
}, 2000);
// 3단계: 도착 완료 (5초 후)
setTimeout(() => {
showToast('success', '도착 완료', `현장 도착, 작업 시작`);
}, 5000);
};
이런 디테일이 있으니까 정말 운영 중인 시스템 같아 보이더라고요.
코드 완성 기록: 문제 해결 과정
문제 1: 왜 자꾸 새로고침 되지?
초기 코드에서 가장 큰 문제였어요. 상태가 바뀔 때마다 전체가 리렌더링...
// ❌ 문제의 코드
useEffect(() => {
generateTableData(); // 이게 매번 실행됨
generateResourceData();
const interval = setInterval(() => {
updateRealTimeData(); // 이것도 상태 변경
}, 3000);
}, []); // 의존성 배열이 비어있어도 문제
// ✅ 해결: 데이터 분리 & useMemo
const tableData = useMemo(() => {
// 한 번만 생성
return generateInitialData();
}, []);
// 실시간 데이터는 별도 관리
const [dynamicData, setDynamicData] = useState(initialState);
문제 2: 맵 레이어가 안 보여요!
Mapbox 초기화 타이밍 문제였습니다:
// ❌ 레이어 추가가 너무 빨라서 맵이 준비 안 됨
useEffect(() => {
initMap();
addLayers(); // 맵이 아직 로드 안 됨
}, []);
// ✅ 해결: load 이벤트 활용
mapRef.current.on('load', () => {
// 이제 레이어 추가
addNDVILayer();
addRainfallLayer();
});
문제 3: "메모리 누수 경고가..."
Chrome DevTools에서 메모리가 계속 증가하는 걸 발견:
// ✅ 완벽한 cleanup
useEffect(() => {
const animation = requestAnimationFrame(animate);
const timer = setTimeout(doSomething, 1000);
const interval = setInterval(update, 5000);
return () => {
// 모든 걸 정리
cancelAnimationFrame(animation);
clearTimeout(timer);
clearInterval(interval);
// 차트도 정리
if (chartRef.current) {
chartRef.current.destroy();
}
// 맵도 정리
if (mapRef.current) {
mapRef.current.remove();
}
};
}, []);
📈 공모전을 위한 전략적 선택들
1. 시각적 임팩트 극대화
/* 그라데이션으로 고급스러움 연출 */
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
--danger-gradient: linear-gradient(135deg, #f97316 0%, #ef4444 100%);
/* 호버 효과로 인터랙티브함 강조 */
.kpi-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
2. 실제 데이터처럼 보이게
// 실제 지명 사용
const locations = ['강원도 평창', '경북 안동', '경남 산청'];
// 실제 같은 수치
const fireRiskPoints = [
{ lng: 128.7, lat: 37.5, name: '평창 대관령' }, // 실제 좌표
{ lng: 128.9, lat: 36.9, name: '안동 일대' }
];
// 현실적인 온도 범위
temperature: (20 + Math.random() * 15).toFixed(1) // 20-35도
3. AI 시뮬레이션 - 핵심 차별화 포인트
const runSimulation = (params) => {
// 실제 같은 계산식 적용
const tempScore = (temperature - 15) * 3; // 15도 기준
const humidityScore = (60 - humidity) * 2; // 60% 기준
const windScore = windSpeed * 5; // 풍속 가중치
// 복합 위험도 산출
const riskScore = (tempScore + humidityScore + windScore) / 3;
// 파생 지표 계산
return {
riskScore,
fireProb: riskScore * 1.2, // 발생 확률
spreadRate: windSpeed * 0.5, // 확산 속도
responseTime: 60 - riskScore * 0.5 // 대응 시간
};
};
이 부분이 심사위원들에게 "아, 진짜 AI를 활용하는구나"라는 인상을 줄 거라 생각했어요.
공모전 제출을 위한 최종 점검
체크리스트
제출 전에 확인한 사항들:
- [x] 단일 HTML 파일로 모든 기능 작동
- [x] Mapbox 토큰 없어도 기본 UI는 보임
- [x] 모든 버튼이 실제로 작동
- [x] 반응형 디자인으로 모바일에서도 확인 가능
- [x] 2,500줄 이상의 substantial한 코드
- [x] 새로고침 없이 모든 전환 부드럽게
- [x] 실시간 애니메이션으로 살아있는 느낌
심사위원 관점에서 생각해본 포인트
- 첫인상 - 로딩 후 바로 보이는 KPI 카드들
- 기술력 - 3D 지도와 다중 레이어
- 실용성 - 자원 관리와 현장 배치
- 혁신성 - AI 시뮬레이션
- 완성도 - 모든 기능이 실제 작동
개발하면서 느낀 점
좋았던 점
- React Hooks의 강력함
- useState, useEffect, useRef, useCallback, useMemo를 조합하니 복잡한 상태 관리가 깔끔하게 해결
- Mapbox GL JS의 유연성
- 원하는 대로 커스터마이징 가능
- 3D 지형이 진짜 멋있음
- Chart.js의 간편함
- 복잡한 차트도 옵션 몇 개로 끝
아쉬웠던 점
- 번들 크기
- 단일 HTML이라 모든 라이브러리를 CDN으로...
- 초기 로딩이 좀 느림
- 타입 안정성
- TypeScript 쓰고 싶었는데 공모전 제출 편의상...
- 테스트 코드
- 시간 관계상 생략했는데 아쉬움
앞으로의 계획
공모전 결과와 상관없이 이 프로젝트를 계속 발전시킬 예정입니다:
Phase 1 (공모전 후 즉시)
- TypeScript 마이그레이션
- 컴포넌트 분리
- 실제 API 연동
Phase 2 (3개월 내)
- WebSocket 실시간 통신
- TensorFlow.js 모델 통합
- PWA 지원
Phase 3 (6개월 내)
- React Native 모바일 앱
- 백엔드 서버 구축
- 실제 산림청 데이터 연동
마무리하며
처음엔 "산림 재해를 개발로 어떻게 해결하지?"라는 막막함으로 시작했지만, 하나씩 구현하다 보니 정말 의미 있는 프로젝트가 된 것 같아요.
특히 이 시스템이 실제로 사용된다면:
- 산불 조기 감지로 인명 피해 90% 감소
- 재선충병 확산 차단으로 연간 840억원 절감
- 산사태 72시간 전 예측으로 대피 시간 확보
이런 실질적인 도움을 줄 수 있다는 게 뿌듯합니다.
공모전 준비하시는 다른 개발자분들께 드리는 팁:
- 아이디어만이 아닌 작동하는 프로토타입을 만드세요
- 실제 데이터와 유사하게 구성하세요
- 시각적 임팩트를 놓치지 마세요
- 사용자 경험을 최우선으로 생각하세요
이 프로젝트 코드가 도움이 되셨다면, 공모전에서 좋은 결과 있으시길 바랍니다!
혹시 비슷한 프로젝트 준비 중이시거나 궁금한 점 있으시면 댓글 남겨주세요. 제가 삽질한 경험이 도움이 될 수 있다면 기쁘게 공유하겠습니다 😊
🔗 참고 자료
📌 프로젝트 스펙
- 총 코드: 2,543줄
- 개발 기간: 2주 (공모전 마감에 쫓기며...)
- 파일 크기: 약 180KB (압축 시 45KB)
- 브라우저 지원: Chrome, Firefox, Safari, Edge
🔗 개발 웹페이지 사진



