클로저를 사용해서 지도의 상태 관리하기

김정훈·2023년 5월 18일
0

프로젝트

목록 보기
1/1
post-thumbnail

프로젝트 링크

개요

클로저는 자바스크립트에서 매우 강력하면서도 유용한 개념이다. 프로젝트를 리팩토링하면서 클로저라는 개념을 접했고, 이를 내가 했던 프로젝트에서 어디에 적용할 수 있을지 생각해봤다.

클로저는 다음과 같은 장점을 가지고 있다. 클로저를 사용하면 내부 함수를 통해 외부 함수의 변수를 감출 수 있고, 외부 함수의 변수 상태를 기억하여 데이터를 보존할 수 있다.

개발한 서비스에서는 경로를 그리거나, 지도 마커를 추가해야 할 때는 생성되었는 지도 인스턴스를 활용하여야 한다. 클로저를 접하기 이전에는 .js파일의 전역변수로 지도 인스턴스를 기억하고 있었는데, 아무래도 전역변수는 소프트웨어의 문제를 일으킬 가능성이 있으므로 사용하지 않는 것이 좋다고 생각했다.

클로저란?

클로저란 외부함수의 렉시컬 환경과 그 환경에 접근할 수 있는 함수의 조합이다. 클로저는 외부 함수의 변수와 그 변수에 접근하는 함수들로 이루어져 있으며, 외부 함수가 내부 함수를 반환하는 형식이다.

function outer() {
  const outerScope = 'I am in the outer scope';

  function inner() {
    console.log(outerScope);
  }

  return inner;
}

const innerFn = outer();
innerFn(); // "I am in the outer scope"

위 예제에서 inner함수는 outer함수의 렉시컬 스코프인 outerScope변수에 접근할 수 있으며, outer함수가 반환된 후에도 inner함수로 outerScope변수에 접근할 수 있다.

실행 컨텍스트 스택에서 inner 함수의 실행 컨텍스트가 생성될 때, outer 함수의 렉시컬 환경 참조가 저장된다. 이렇게 inner함수가 outer 함수의 렉시컬 환경을 참조하고 있기 때문에, inner 함수에서 outerScope 변수를 참조할 수 있다.

클로저를 어디에 사용했나?

웹 상에 표시되는 지도는 하나의 인스턴스로 경로를 그리거나 마커를 지도위에 찍고 싶을 때, 생성되어 있는 지도위에 그려야 한다.

그리고 경로나 마커도 인스턴스로 생성해야 하는데, 경로와 마커 인스턴스 생성시 자신이 표시될 지도의 인스턴스를 등록해야한다.

일단 근본적인 질문을 해보자.

왜 지도와 경로, 마커의 데이터를 기억하고 있어야 할까?

지도는 API 서버에서 이미지를 받아오는 형식이었다. 만약 경로가 생성되고, 마커를 만들어야 할 때마다 지도를 새롭게 생성하려고 한다면 상당한 시간이 소요된다. 따라서 이미 만들어진 지도 위에 경로나 마커를 그리거나 지우는 것이 효율적일 것이다.

그렇다면 경로와 마커 정보는 왜 저장하고 있는데?

만약 현재 지도에 그려진 경로와는 다른 결과를 얻기 위해 다시 길찾기를 수행했다고 가정하자. 그렇다면 이미 그려진 경로나 마커를 지우고 새로운 경로를 그려야 하는데, 이 정보를 기억하고 있지 않다면 지우지 못할 것이다.

따라서 클로저에서 기억할 데이터들은 다음과 같다.

지도 인스턴스 지도위에 그려진 경로 인스턴스들 지도위에 그려진 마커 인스턴스들

추가적으로 지도위에 그려진 경로나 마커의 인스턴스를 지워주는 함수도 있어야 할 것이다.

서비스에서 지도는 하나지만 경로 인스턴스나 마커 인스턴스를 여러개가 존재할 수 있으므로 배열을 통해 관리했다.

function init() {
  let map; // 지도
  let resultMarkerArr = []; // 지도에 찍힌 마커 인스턴스 배열 
  let resultDrawArr = []; // 지도에 그려진 경로 인스턴스 배열
	
  // other code ...
}

데이터를 정했으니 해당 데이터에 접근하는 함수를 만들어 init함수 외부에서도 map, resultMarkerArr, resultDrawArr에 접근 할 수 있도록 클로저를 만들어보자.

function init() {
  let map;
  let resultMarkerArr = [];
  let resultRouteArr = [];

  function getMap() {
    return map;
  }

  function getResultMarkerArr() {
    return resultMarkerArr;
  }

  function getResultRouteArr() {
    return resultRouteArr;
  }
  
  function resettingMap() {
    // T map api는 setMap 메소드를 사용해 마커나 경로를 지울 수 있다.
    if (resultMarkerArr) {
      resultMarkerArr.forEach((marker) => {
      	marker.setMap(null);
      })
    }

    if (resultRouteArr) {
      resultRouteArr.forEach((route) => {
      	route.setMap(null);
      })
    }

    resultMarkerArr = [];
    resultRouteArr = [];
  }
  
  return {
  	getMap: getMap,
    getResultMarkerArr: getResultMarkerArr,
    getResultRouteArr: getResultRouteArr,
    resettingMap: resettingMap
  }
}

const app = init();

이런식으로 작성하면 init함수를 호출하여 반환받은 객체를 통해 지도에 관련된 데이터에 접근 할 수 있게 된다.

지도 데이터에 접근 할 수 있는 객체는 하나면 충분하고, 계속해서 기억하고 있어야 하니까 const로 선언해 참조의 변경을 막으면서 접근하도록 하자. 지도에 관한 데이터는 app이라는 상수만을 통해 접근 할 수 있을 것이다.

프로젝트에 적용하기

프로젝트에서 클로저가 사용된 세 가지의 경우가 있다.

drawRoute(path, color): 경로를 그리는 함수.

  • path는 GPS (위도,경도)의 배열로 해당하는 경로를 color에 전달된 색상 코드로 지도에 그려주는 함수이다. 경로의 인스턴스를 생성하고 resultRouteArr에 push한다.
function drawRoute(path, color) {
  let drawInfoArr = []; // GPS 좌표 인스턴스를 저장할 배열
  let routeLine; // 경로 인스턴스를 저장할 변수
  const map = app.getMap();
  const resultdrawArr = app.getResultRouteArr();

  path.forEach((coordinate) => {
    // GPS 좌표 인스턴스 생성
    let latLng = new Tmapv2.LatLng(coordinate[1], coordinate[0]); 
    drawInfoArr.push(latLng);
  });

  routeLine = new Tmapv2.Polyline({
    path: drawInfoArr,
    strokeColor: color,
    strokeWeight: 4,
    map: map // 경로 인스턴스가 표시될 지도 등록
  });

  resultdrawArr.push(routeLine);
}
app.getResultRouteArr().push(routeLine);

setMarker(points): 마커를 지도에 찍어주는 함수.

  • markers는 (위도, 경도)의 배열로 각 좌표마다 마커를 찍어주는 함수다. 해당 프로젝트에서는 마커를 출발지, 경유지, 목적지에 찍어준다. 마커의 인스턴스를 생성하고 resultMakerArr에 push한다.
function setMarker(points){
  const map = app.getMap(); // 현재 지도 인스턴스 가져오기
  const resultMarkerArr = app.getResultMarkerArr();	  
  let marker;
  
  points.forEach((point) => {
    marker = new marker(point);
    resultMarkerArr.push(marker);
    
    // ...
  })
}

setMapBound(points): 지도의 보여지는 화면을 설정하는 함수.

  • points는 (위도, 경도)의 배열로, 전달된 좌표를 기준으로 보여지는 지도의 범위를 설정해준다.
function setMapBound(points) {
  const map = app.getMap();
  // ... 바운딩
  
  map.fitBounds(bounds);
  // fitBounds: T map api에서 제공하는 전달된 범위에 맞춰 지도를 설정하는 메소드
}

클로저를 사용하여 지도의 상태를 관리할 수 있으며, 지도 상태에 직접적으로 접근을 막을 수 있기 때문에 코드의 안정성을 높일 수 있을 것이다.

클로저 사용 시 주의사항

클로저를 사용할 때 주의해야할 점이 몇 가지 있는데, 그 중하나가 메모리 누수문제이다. 진행했던 프로젝트에도 같은 문제가 있었다.

메인 페이지 메모리 스냅샷

지도 페이지에서 경로를 생성하고 메인 페이지로 다시 돌아와봤다.

메모리 스냅샷을 살펴보면 메인 페이지는 지도상의 인스턴스를 사용지 않음에도 여전히 지도 페이지에서 생성한 인스턴스들이 메모리상에 존재하는 것을 볼 수 있다.

이 문제를 해결하기 위해 내가 처음으로 떠올린 방법

  • 서비스의 각 페이지마다 클로저 초기화하기(내가 떠올렸지만 너무 번거롭고 서비스가 커질 경우엔?...)

따라서 지도 페이지를 나가면 클로저를 해제하도록 하기로 결정했다. 그렇다면 지도 페이지를 벗어난다는 것을 어떻게 알 수 있을까?

beforeunload 이벤트를 활용해보자.

beforeunload 이벤트는 window, 문서(document) 및 해당 리소스가 언로드(unload) 되려고 할 때 시작됩니다. 문서는 계속 보이고 있으며 이벤트는 이 시점에서도 취소할 수 있습니다.
https://developer.mozilla.org/ko/docs/Web/API/Window/beforeunload_event

먼저 클로저 외부 함수에 변수들의 메모리를 해제하는 clear라는 함수를 만들고 beforeunload 이벤트를 등록해 clear함수를 콜백함수로 실행하도록 했다.

function init() {
  // ...
  function clear(){
    // 지도에 표시된 마커와 경로 인스턴스를 null로 reset
    resettingMap(); 
    map = null;
  }
  
  window.addEventListener('beforeunload', clear);
  
  // return ...
}
  • 경로를 생성했을 때 지도 페이지 메모리 스냅샷

    클로저에 경로 인스턴스 배열, 마커 인스턴스 배열, 지도 인스턴스가 존재하는 것을 확인할 수 있다.

  • clear함수 적용 후 메인 페이지 메모리 스냅샷

    깔끔하게 메모리가 해제된 것을 볼 수 있다. 지도 페이지에서 경로를 생성했을 때는 약 13MB의 메모리를 소모하고 있었는데 메인페이지에서는 6MB정도로 감소한 모습을 보였다.

자바스크립트에서는 클로저를 해제할 때 이벤트를 등록해야 하는 번거로움이 있다. 깊이 알지는 못하지만 리액트 컴포넌트의 라이프 사이클 중에서 해당 컴포넌트가 더 이상 사용되지 않을 때, 상태관리를 통해 메모리를 해제할 수 있지 않을까?

0개의 댓글