Task Manager를 옮기며 라는 글을 작성한 지 2달의 시간이 흘렀습니다. 여느 때처럼 마음 한구석에 담아 두었던 해야한다는 생각이 더는 미루면 안되라는 죄책감과 이를 시도해볼 수 있는 환경이 마련되고 나서야 등 떠밀리 듯 또 하나의 무언가를 해보려고 합니다.
무엇을 하든지 저는 동기가 생기지 않으면 잘 시작하지 않는 편입니다만 대체로 여기저기서 씨앗을 받아 심어두었다가 시간이 지나 새싹이 필 때즘 하나씩 시작하게 됩니다. 1년 전쯤 경로를 따라 차량이 경로를 이동하는 것처럼 시뮬레이션을 해봐야 할 일이 있었습니다. @turf/along 과 이것저것을 잘 활용해서 짜면 될 것 같았지만, along에 한번 성능 문제를 겪었던 저로서는 선뜻 손이 가지 않았습니다. 그래서 예제로 만들어 두었던 line-arrow의 코드를 참조하여 경로를 특정한 개수로 쪼개어 사용하는 만행을 저지르게 됩니다.
이 방식대로 사용하다 보니 경로가 길어져도 대로 동일한 개수로 나누어지어 무언가 동작이 어색하다는 점이 너무 아쉬웠습니다. 잠시 임시로 사용한 코드였기 때문에 곧 사라질 운명이어서 더 이상의 시간을 사용할 필요가 없어 아쉬움을 또 하나의 씨앗으로 심어두고 다른 것들을 먼저 하게 되었습니다.
Firebase 동적 호스팅에 Next.js 배포하기 라는 글을 통해 한번 말씀드렸던 것처럼 Labs 공간을 만들어 두어 환경적으로 시도할 만한 공간과 잊을 때마다 한 번씩 떠올리게 해주는 주변 자극에 드디어 첫 삽을 떠보고자 합니다.
똥손이지만 제가 생각하는 구조를 그려보면 상단의 그림과 같이 모습이 될 것 같습니다. 일부는 이 글을 작성하는 시점에 구현이 진행되었고 일부는 구성만 해둔 상태입니다.
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
는 실제 구현 작업이 전혀 진행되지 않았지만, 현재 기준의 생각으로는 View
가 Simulator
를 생성하고 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
를 작성하여 사용하였습니다.
매번 한 번의 작업이 일단락되면 글을 작성하곤 하였습니다. 그럴 경우에는 글을 작성하기도 힘들고 작업의 페이스를 조절하기도 힘들어 이번 글부터는 조금씩 나누어 글을 작성해보고자 하였습니다(블로그를 개편할 때 시리즈 기능을 구현해 둔 것을 사용하고자 하는 목적도 있습니다. :)). 간단한 준비 작업에 대한 글은 이 정도로 마치고 다음 글로 찾아뵙겠습니다.