준비 작업

2020년 12월 14일
Guidance Simulator

Task Manager를 옮기며 라는 글을 작성한 지 2달의 시간이 흘렀습니다. 여느 때처럼 마음 한구석에 담아 두었던 해야한다는 생각이 더는 미루면 안되라는 죄책감과 이를 시도해볼 수 있는 환경이 마련되고 나서야 등 떠밀리 듯 또 하나의 무언가를 해보려고 합니다.

동기

line arrow

무엇을 하든지 저는 동기가 생기지 않으면 잘 시작하지 않는 편입니다만 대체로 여기저기서 씨앗을 받아 심어두었다가 시간이 지나 새싹이 필 때즘 하나씩 시작하게 됩니다. 1년 전쯤 경로를 따라 차량이 경로를 이동하는 것처럼 시뮬레이션을 해봐야 할 일이 있었습니다. @turf/along 과 이것저것을 잘 활용해서 짜면 될 것 같았지만, along에 한번 성능 문제를 겪었던 저로서는 선뜻 손이 가지 않았습니다. 그래서 예제로 만들어 두었던 line-arrow의 코드를 참조하여 경로를 특정한 개수로 쪼개어 사용하는 만행을 저지르게 됩니다.

이 방식대로 사용하다 보니 경로가 길어져도 대로 동일한 개수로 나누어지어 무언가 동작이 어색하다는 점이 너무 아쉬웠습니다. 잠시 임시로 사용한 코드였기 때문에 곧 사라질 운명이어서 더 이상의 시간을 사용할 필요가 없어 아쉬움을 또 하나의 씨앗으로 심어두고 다른 것들을 먼저 하게 되었습니다.

Firebase 동적 호스팅에 Next.js 배포하기 라는 글을 통해 한번 말씀드렸던 것처럼 Labs 공간을 만들어 두어 환경적으로 시도할 만한 공간과 잊을 때마다 한 번씩 떠올리게 해주는 주변 자극에 드디어 첫 삽을 떠보고자 합니다.

구조

structure

똥손이지만 제가 생각하는 구조를 그려보면 상단의 그림과 같이 모습이 될 것 같습니다. 일부는 이 글을 작성하는 시점에 구현이 진행되었고 일부는 구성만 해둔 상태입니다.

View에는 좀 더 풍부한 표현을 하기 위해 기존에 사용하던 react-map-gl 대신 Deck.gl을 사용하여 지도를 렌더링하고자 하였습니다. 막상 작성하고 이 긍를 작성하며 생각해보니 이번의 경우에는 생각보다 풍부하게 표현할 내용이 없을 것 같다는 느낌이 강하게 들었지만, 그래도 잘 구성해 두었으면 필요할 때 사용하면 된다는 점을 위안으로 삼았습니다.

<DeckGL
  {...props}
  onResize={onResize}
>
  <StaticMap
    width="100%"
    height="100%"
    reuseMaps
    preventStyleDiffing
    mapOptions={{
      attributionControl: false,
      localIdeographFontFamily: false,
    }}
    mapStyle={MAP_STYLES[MAP_STYLE_TYPE.MAPBOX_NIGHT]}
    mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN}
  />
</DeckGL>

Simulator는 실제 구현 작업이 전혀 진행되지 않았지만, 현재 기준의 생각으로는 ViewSimulator를 생성하고 Simulator를 통해 Task의 이벤트를 구독할 생각입니다. Simulator는 주어진 옵션에 의해 여러 개의 Worker로 만들어진 차량을 생성하고 차량들은 각기 지도 API로부터 경로를 받아와 작업을 수행합니다. 수행한 작업의 결과물은 Task Manager를 통해 Simulator로 전달되어 View를 그리도록 할 생각입니다.

이번에 사용하기로 생각한 지도 API는 Naver Map API 입니다. 여느 이유와 비교할 수 없는 큰 이유는 현재 수준의 예제에서 사용하기에 무료로 사용할 수 있는 쿼터가 있다는 점이었습니다. Naver Map API를 사용하지만 앞단에 Server 를 Proxy로 두게된 이유는 MAPS API 호출 시 CORS 에러가 발생합니다. 라는 FAQ 글에서 찾을 수 있습니다. 보안상의 이유로 Javascript 환경에서 사용할 수 없게 막혀있고, 이를 우회하고자 한다면 상단의 구성과 같이 호출을 받아 줄 수 있는 Backend가 필요했습니다.

export default class NaverMapProvider extends HttpProvider {
  private static readonly END_POINT = '/api/proxy/naver-map-api';

  constructor(instance: AxiosInstance) {
    super(instance);
  }

  public routes(request: RouteRequest): Observable<Route[]> {
    return this.request<RouteResponse>({
      method: RequestMethod.GET,
      url: `${NaverMapProvider.END_POINT}/map-direction/v1/driving`,
      params: request,
    }).pipe(
      switchMap<RouteResponse, Observable<Route[]>>(response => {
        if (response.code > 0) {
          return of([]);
        }

        return of(Object
          .keys(response.route)
          .map(key => response.route[key].map(route => Route.fromJson(route)))
          .flat()
        );
      })
    );
  }
}

상단의 코드와 같이 내부 Proxy를 호출하는 NaverMapProvider 을 작성하였습니다.

export default class MapRepository implements Repository {
  constructor(
    private provider: NaverMapProvider,
  ) {
  }

  public routes(start: LngLat, goal: LngLat, waypoints: LngLat[] = []): Observable<Route[]> {
    return this.provider.routes({
      start: `${start.lng},${start.lat}`,
      goal: `${goal.lng},${goal.lat}`,
      waypoints: waypoints.length > 0 ? waypoints.map(point => `${point.lng},${point.lat}`).join('|') : undefined,
      option: 'trafast',
    });
  }
}

다른 지도 API들을 사용해봤을 때 출발, 도착, 경유지 정도를 받으면 사용하는 데 큰 문제가 없을 것으로 생각하여 다음 같은 인자들을 받는 MapRepository 를 작성하여 사용하였습니다.

마치며

매번 한 번의 작업이 일단락되면 글을 작성하곤 하였습니다. 그럴 경우에는 글을 작성하기도 힘들고 작업의 페이스를 조절하기도 힘들어 이번 글부터는 조금씩 나누어 글을 작성해보고자 하였습니다(블로그를 개편할 때 시리즈 기능을 구현해 둔 것을 사용하고자 하는 목적도 있습니다. :)). 간단한 준비 작업에 대한 글은 이 정도로 마치고 다음 글로 찾아뵙겠습니다.

Recently posts
© 2016-2023 smilecat.dev