React + OpenLayers 최적화(VectorSource 편)

DongHyun Park·2024년 11월 11일

OpenLayers

목록 보기
1/1
post-thumbnail

OpenLayers는 Map, View, Layer, Source, Feature 등 다양한 구성 요소들로 이루어져 있습니다. 각각의 구성 요소들은 웹 지도를 구현하는 데 있어 중요한 역할을 담당하고 있죠. 오늘은 이 구성 요소들 중에서 VectorSource의 최적화에 대해 이야기해보려고 합니다.
VectorSource는 지도상에 표시되는 벡터 데이터를 관리하는 핵심 컴포넌트입니다. 특히 대량의 데이터를 처리하거나 실시간으로 데이터가 변경되는 경우, VectorSource의 성능 최적화는 매우 중요합니다. 오늘은 제가 React 프로젝트를 진행하면서 겪었던 VectorSource 관련 성능 이슈들과 그 해결 방법들을 공유하고자 합니다.

목차

  1. VectorSource 기본 이해
  2. 메모리 관리와 성능 최적화
  3. 대량의 피처 처리
  4. 동적 데이터 처리
  5. 이벤트 최적화
  6. 실전 사용 예제

VectorSource 기본 이해

OpenLayers에서 VectorSource는 벡터 데이터를 관리하는 핵심 클래스입니다. React 환경에서 이 컴포넌트를 사용할 때는 특히 생명주기 관리에 주의를 기울여야 합니다. 제가 프로젝트를 진행하면서 발견한 최적화 포인트 중 하나는 바로 VectorSource의 wrapX 옵션 활용입니다.

import { useEffect, useMemo } from 'react';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';

function OptimizedVectorLayer() {
  // VectorSource 메모이제이션
  const source = useMemo(() => {
    return new VectorSource({
      wrapX: false,  // 성능을 위해 wrapX 비활성화
    });
  }, []);
  
  // ... 추가 구현
}

여기서 주목할 점은 wrapX 옵션입니다. 이 옵션은 지도의 날짜변경선(180도 경도)을 넘어가는 상황에서의 데이터 처리 방식을 결정합니다. 기본값인 true로 설정되어 있다면, 마치 지구본을 돌리듯이 지도를 계속 좌우로 스크롤할 수 있고 벡터 데이터도 반복해서 표시됩니다. 하지만 이는 각 피처를 여러 번 복제하고 위치를 계산해야 하는 추가적인 연산이 필요합니다.
반면 wrapX: false로 설정하면 다음과 같은 이점이 있습니다:

  • 날짜변경선을 넘어가는 데이터 복제가 비활성화됩니다.
  • 좌우 스크롤이 제한되고, 벡터 데이터가 한 번만 렌더링됩니다.
  • 결과적으로 불필요한 연산이 줄어들어 성능이 향상됩니다.

메모리 관리와 성능 최적화

VectorSource를 사용할 때 가장 중요한 것은 메모리 관리입니다. 특히 피처를 추가하고 제거할 때 메모리 누수가 발생하지 않도록 주의해야 합니다.

function VectorLayerComponent({ features, map }) {
  const source = useMemo(() => new VectorSource(), []);
  const layer = useMemo(() => new VectorLayer({ source }), [source]);

  useEffect(() => {
    map.addLayer(layer);
    
    return () => {
      // 정리(cleanup) 함수
      source.clear();  // 모든 피처 제거
      map.removeLayer(layer);
    };
  }, [map, layer, source]);

  // 피처 업데이트 최적화
  useEffect(() => {
    source.clear();  // 기존 피처 제거
    source.addFeatures(features);  // 새 피처 추가
  }, [source, features]);
}

대량의 피처 처리

대량의 피처를 처리할 때는 배치 처리가 필수적입니다. 배치 처리란 많은 양의 데이터를 한 번에 처리하는 대신, 적절한 크기로 나누어 순차적으로 처리하는 방식을 말합니다. 예를 들어, 10,000개의 피처를 한 번에 지도에 추가하는 대신, 1,000개씩 10번에 나누어 추가하는 것이죠.
이는 마치 무거운 짐을 옮길 때, 한 번에 모두 들기보다는 여러 번 나누어 옮기는 것과 비슷한 개념입니다. 이렇게 하면 브라우저가 한 번에 처리해야 하는 부하가 줄어들어 전체적인 성능이 향상되고, 사용자 경험도 더 부드러워집니다.

function BatchFeatureProcessor({ features, source }) {
  useEffect(() => {
    if (!features.length) return;

    const BATCH_SIZE = 1000;
    let currentIndex = 0;

    function processBatch() {
      const batch = features.slice(
        currentIndex, 
        currentIndex + BATCH_SIZE
      );
      
      if (batch.length === 0) return;

      // 배치 단위로 피처 추가
      source.addFeatures(batch);
      currentIndex += BATCH_SIZE;

      // 다음 배치 처리를 위한 스케줄링
      if (currentIndex < features.length) {
        window.requestAnimationFrame(processBatch);
      }
    }

    source.clear();  // 기존 피처 제거
    processBatch();  // 배치 처리 시작

    return () => {
      currentIndex = features.length;  // 진행 중인 배치 처리 중단
    };
  }, [features, source]);

  return null;
}

동적 데이터 처리

실시간으로 데이터가 변경되는 경우, 불필요한 re-render를 방지하기 위한 최적화가 필요합니다.

function DynamicVectorSource({ data, updateInterval = 1000 }) {
  const source = useMemo(() => new VectorSource(), []);
  
  // 데이터 업데이트 디바운싱
  useEffect(() => {
    let timeoutId;
    
    const updateSource = () => {
      source.clear();
      
      // 새 피처 추가 전에 기존 피처의 상태 저장
      const existingFeatures = new Map(
        source.getFeatures().map(f => [f.getId(), f.getProperties()])
      );

      // 변경된 데이터만 업데이트
      data.forEach(item => {
        const id = item.id;
        const existing = existingFeatures.get(id);
        
        if (!existing || JSON.stringify(existing) !== JSON.stringify(item)) {
          // 새로운 피처 추가 또는 기존 피처 업데이트
          const feature = createFeatureFromData(item);
          source.addFeature(feature);
        }
      });
    };

    timeoutId = setTimeout(updateSource, updateInterval);

    return () => {
      clearTimeout(timeoutId);
    };
  }, [data, source, updateInterval]);

  return null;
}

이벤트 최적화

VectorSource의 이벤트 처리도 성능에 큰 영향을 미칠 수 있습니다.

function OptimizedVectorEvents({ source, onFeatureChange }) {
  useEffect(() => {
    const debouncedHandler = _.debounce((event) => {
      onFeatureChange(event.feature);
    }, 100);

    // 이벤트 리스너 등록
    const changeListener = source.on('changefeature', debouncedHandler);

    return () => {
      // 이벤트 리스너 제거
      unByKey(changeListener);
    };
  }, [source, onFeatureChange]);
}

실전 사용 예제

이제 위의 모든 최적화를 종합한 실제 사용 예제를 보여드리겠습니다:

function OptimizedVectorLayer({
  features,
  map,
  style,
  onFeatureSelect
}) {
  // 소스와 레이어 메모이제이션
  const source = useMemo(() => new VectorSource({
    wrapX: false,
    strategy: loadingstrategy.bbox
  }), []);

  const layer = useMemo(() => new VectorLayer({
    source,
    style,
    updateWhileAnimating: false,
    updateWhileInteracting: false
  }), [source, style]);

  // 맵에 레이어 추가/제거
  useEffect(() => {
    map.addLayer(layer);
    return () => {
      source.clear();
      map.removeLayer(layer);
    };
  }, [map, layer, source]);

  // 피처 배치 처리
  useEffect(() => {
    const worker = new Worker('featureProcessor.js');
    
    worker.postMessage({ features });
    
    worker.onmessage = (e) => {
      const processedFeatures = e.data;
      source.clear();
      
      // 배치 처리로 피처 추가
      let added = 0;
      function addBatch() {
        const batch = processedFeatures.slice(added, added + 1000);
        if (batch.length === 0) return;
        
        source.addFeatures(batch);
        added += batch.length;
        
        if (added < processedFeatures.length) {
          requestAnimationFrame(addBatch);
        }
      }
      
      addBatch();
    };

    return () => {
      worker.terminate();
    };
  }, [features, source]);

  // 이벤트 처리 최적화
  useEffect(() => {
    if (!onFeatureSelect) return;

    const selectInteraction = new Select({
      layer,
      hitTolerance: 5
    });

    const debouncedSelect = debounce((e) => {
      const selected = e.selected[0];
      if (selected) {
        onFeatureSelect(selected);
      }
    }, 100);

    selectInteraction.on('select', debouncedSelect);
    map.addInteraction(selectInteraction);

    return () => {
      map.removeInteraction(selectInteraction);
    };
  }, [map, layer, onFeatureSelect]);

  return null;
}

마치며

VectorSource의 최적화는 OpenLayers와 React를 함께 사용할 때 가장 중요한 부분 중 하나입니다. 특히 대량의 데이터를 처리하거나 실시간 업데이트가 필요한 경우, 위에서 설명한 최적화 기법들을 적절히 활용하면 훨씬 더 나은 성능을 얻을 수 있습니다.

주의할 점은, 모든 최적화 기법을 무조건 적용하는 것이 아니라, 프로젝트의 요구사항과 데이터의 특성에 맞게 선택적으로 적용해야 한다는 것입니다. 실제 성능 측정을 통해 최적화가 필요한 부분을 파악하고, 적절한 기법을 선택하는 것이 중요합니다.

다음 글에서는 OpenLayers의 다른 컴포넌트들의 최적화 방법에 대해 다루도록 하겠습니다.

0개의 댓글