Table of Contents
  1. 프로젝트 소개
    1. 개발 동기
    2. 기능 소개
  2. 개발 내용
    1. Design
    2. Kakao API
    3. Hosting
    4. Firestore
    5. Firebase Functions & Authentication
  3. 마치며

Vue.js & Firebase 기반으로 주소가 담긴 Excel 파일을 파싱하여 지도를 만들어주는 반응형 웹 어플리케이션 GentleMap을 개발한 이야기를 공유합니다.

프로젝트 소개

사이드 프로젝트로 전체 개발 기간은 3개월 정도 소요되었고, 혼자서 개발하였습니다.

최종 결과물은 https://gentle-map.devstory.co.kr에서 이용하실 수 있습니다.


개발 동기

대학생 때 KT&G 상상발룬티어 소속으로 100명의 자원 봉사자들과 팀을 이루어 120가구에 반찬을 나누는 사랑의 반찬나눔 봉사와 취약계층 60가구를 대상으로 화재감지기 설치 봉사를 기획하고 참여했습니다. 두 봉사활동은 모두 취약계층의 명단을 팀 별로 나눈 지도를 제작 해야했는데, 당시 네이버 주소 API와 Google Map API을 이용한 Android 지도 어플리케이션을 만들어 봉사활동을 편하게 진행할 수 있었습니다.

이 후에도 지도가 필요한 봉사활동을 할 때 직접 주소를 위도 경도로 변환하고, 팀 수에 맞추어 가까운 거리순으로 마커를 분배하여 지도 어플리케이션을 만들었습니다. 문득 다른 봉사활동을 하는 사람들도 비슷한 니즈를 가지고 있을것이라 생각이 들어 누구든지 손쉽게 지도를 만들 수 있는 웹 플랫폼인 Gentle Map을 만들게 되었습니다.


기능 소개

  • Excel 데이터를 지도상에 시각화
  • 활동하는 팀 수에 맞추어 동선이 최소화되도록 마커 자동 분배
  • 관심있는 팀의 마커만 볼 수 있는 마커 필터링 기능
  • 모든 마커의 상태가 실시간으로 동기화 되어 진행상황로 실시간으로 모니터링 가능
  • 카카오톡 계정을 이용한 소셜 로그인 기능
  • 카카오톡 링크 이용하여 손쉽게 프로젝트 초대 기능
  • 프로젝트 참가자는 회원가입 없이 비밀번호만으로 참여 가능
  • 별도의 어플리케이션 설치 없이 모바일 웹을 이용하여 이용 가능
  • 모바일에서도 이용가능한 반응형 디자인
  • HTML5의 Geolocation을 이용한 GPS 기능

개발 내용

Vue.js 기반의 반응형 웹 어플리케이션과 Firebase 기반의 백엔드로 설계하였습니다.

✔︎ FrontEnd

✔︎ BackEnd


Design

처음에는 GPS 기능 때문에 모바일 어플리케이션 형태를 고려하다가, HTML5 Geolocation 성능이 생각보다 괜찮아 모바일 환경에서도 별도의 설치 없이 이용가능하도록 반응형 웹사이트로 구현하였습니다.

별도로 협업 하는 디자이너가 없었기 때문에 UI 라이브러리인 Element UIVusax를 조합하여 사용하였습니다. 두 라이브러리 모두 테마 변경 기능과 반응형 디자인을 지원합니다.

✔︎ Element UI와 Vuesax 사용시 주의할 점 (2019.08 기준)

  • Element UI의 Dialog 컴포넌트는 모바일에 지원이 안됨
  • Vuesax Dialog 컴포넌트 위에 Element UI의 Select 컴포넌트 사용시 선택창이 보이지 않음
  • Vuesax는 Safari에서 Select 컴포넌트가 작동하지 않음
  • Vuesax는 Safari에서 Button 컴포넌트의 클릭 effect 효과가 사라지지 않음

Kakao API

이번 프로젝트에서는 로그인, 지도, 주소 좌표변환 및 공유 등 다양한 Kakao API를 이용하여 개발하였습니다.

✔︎ 이용한 Kakao API 목록

지도 API는 Google Map, Naver Map, Kakao Map을 검토하였는데, Web Dynamic Map은 Kakao에서만 월 30만회 무료 사용량을 제공하여 Kakao Map을 선택하였습니다. (20.05.02 기준으로 네이버도 월 천만건 무료로 풀렸네요)

Kakao API를 이용하여 지도 및 Marker Clustering 기능을 구현하였습니다.Kakao API를 이용하여 지도 및 Marker Clustering 기능을 구현하였습니다.

프로젝트를 생성 및 관리를 위해선 로그인이 필요합니다. 이때 회원가입 과정을 손쉽게 할 수 있도록 Kakao 소셜 로그인 기능을 이용하였는데, Firebase에서는 카카오톡 소셜 로그인을 정식적으로 지원하지 않기 때문에 Firebase Functions에 Custom Token을 발급하는 함수를 만들고, 클라이언트에서 signInWithCustomToken을 이용하여 로그인하도록 구현하였습니다.


Hosting

HTML5 Geolocation을 이용하기 위해선, HTTPS가 필요합니다. Firebase Hosting에서는 기본적으로 SSL을 제공해주기 때문에 별도로 인증서를 구매하지 않고 HTTPS를 이용할 수 있습니다. 또한 Custom Domain도 HTTPS가 지원되기 때문에 gentle-map.devstory.co.kr 를 연결하여 호스팅하였습니다.


Firestore

Firestore를 이용한 실시간 마커상태 동기화 영상
(왼쪽 : Desktop, 오른쪽 : Mobile)

마커 상태를 새로고침 없이 실시간으로 동기화 되도록 Firestore를 이용하였습니다. 초기에 마커들의 상태를 하나의 Document 안에 배열로 구현하였는데, Firestore에서는 배열의 특정 원소만을 업데이트하는 기능을 지원하지 않아 변경된 배열 전체를 업데이트하도록 구현하였습니다. 그런데 짧은 시간 안에 두 유저가 업데이트를 요청하는 경우, 아래 sequence diagram 처럼 늦게 요청한 유저의 요청값이 이전 유저의 값으로 동기화되어 화면상 결과가 데이터베이스와 달라지는 문제가 발생하였습니다.

두 유저가 서로 다른 마커의 상태를 동시에 변경한 경우, 늦게 요청한 유저의 변경한 사항이 날아가고, 다른 유저의 요청값으로 동기화되는 문제 발생두 유저가 서로 다른 마커의 상태를 동시에 변경한 경우, 늦게 요청한 유저의 변경한 사항이 날아가고, 다른 유저의 요청값으로 동기화되는 문제 발생

해당 문제는 마커의 상태 변경시, 관심 마커 이외에 모든 마커를 함께 업데이트하기 때문에 발생하는 문제입니다. 따라서 전체 마커를 하나의 문서에 속한 배열이 아닌, 마커 하나당 개별 문서로 구현하고 각 문서 별 리스너를 등록해주어 마커들의 상태 값을 개별적으로 업데이트 할 수 있도록 구현하여 문제를 해결하였습니다.


Firebase Functions & Authentication

유저에 따른 Firestore 접근 규칙을 설정하려면 Firebase Authentication과 로그인 기능이 연동되어야합니다. Firebase Authentication에서 카카오톡 소셜 로그인을 지원하지 않기 때문에, Firebase Functions에 Node.js 기반의 Custom Token 발급 함수를 만들고, 클라이언트에서 signInWithCustomToken을 이용하여 로그인하도록 구현하였습니다.

Node.js 기반 Custom Token 발급 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 도쿄리전 사용
const functions = require('firebase-functions').region('asia-northeast1')
const express = require('express')
const cors = require('cors')
const admin = require('firebase-admin')
const app = express()

// Automatically allow cross-origin requests
app.use(cors({ origin: true }))

// Init auth
admin.initializeApp({
credential: admin.credential.cert({
// credential 정보 생략
}),
})

app.get('/', (req, res) => {
if (!req.query.uid) return res.status(400).send('uid cannot be empty')
const uid = req.query.uid
admin
.auth()
.createCustomToken(uid)
.then((token) => {
return res.send({
token: token,
})
})
.catch((err) => {
return res.send({
error: err,
})
})
})

// Expose Express API as a single Cloud Function
exports.token = functions.https.onRequest(app)

전체 로그인 순서는 먼저 카카오톡에 로그인 한 뒤, Firebase Functions에서 토큰을 발급받고, 해당 토큰으로 signInWithCustomToken를 호출하여 최종 로그인을 하게 됩니다. 이후 Firestore에 접근하는 경우 아래와 같은 규칙에 따라 권한을 확인하게 됩니다.

firestore 규칙 예시
1
2
3
4
5
6
7
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid != null;
}
}
}

마치며

이번 프로젝트는 Vue.js Framework 기반의 SPA(Single Page Application) 형태로 개발하였기 때문에 네이버와 구글 검색엔진에 등록이 불가능한 점이 아쉬었습니다. 다음번 프로젝트에서는 Nuxt.js와 같은 SSR(Server Side Rendering)을 지원하는 Framework를 고려하는 것도 좋을 것 같습니다.