본문 바로가기

upco #2. geo API 본문

개발/프로젝트

upco #2. geo API

자전하는명왕성 2023. 3. 19. 23:45

오늘은 upco 프로젝트 진행 중 알게 된 geo API 에 대해 포스팅한다.

 

geo API

geo API 는 Redis에서 제공하는 서비스 중 하나로 그 이름 말마따나 지리적 위치에 대한 API를 의미한다.

내가 geo API를 선택한 이유로는, 이미 사용해본 적 있는 redis 내에서 서비스한다는 이유도 있었지만

'특정 위치를 기준으로 특정 범위 내에 존재하는 데이터를 반환'할 수 있는 메서드가 존재한다는 것이 그 이유였다.

 

먼저 위치 정보를 저장하는 로직에 대해 다룬다.

async saveLocation({ email, location }): Promise<string> {
    const { lat, lng } = location;

	// redis 환경 셋팅
    const redisInfo: RedisOptions = {
      host: process.env.REDIS_HOST,
      port: Number(process.env.REDIS_PORT),
      db: Number(process.env.REDIS_DB),
    };

    const client = new Redis(redisInfo); // Redis에서 제공하는 생성자를 변수에 담는다.
    const userId = "tempData";

    try {
      // redis 내 ttlSet 객체 안에 유저 아이디와 ttl을 저장하는 로직.
      // 임시로 둔 ttl은 30초. 30초가 지날 때까지 새로 위치 정보가 저장되지 않으면 로그아웃으로 간주한다.
      const ttl = 30000; 
      const value = Date.now() + ttl;
      // redis내에서 set을 다룰 경우 'z'를 붙인다. ('z'는 sorted set의 약자로 쓴다고 함)
      await client.zadd("ttlSet", value, userId); 
      // redis 내 geoSet 객체 안에 유저 아이디와 위치정보를 저장하는 로직.
      client.geoadd("geoSet", lng, lat, userId);
    } catch (error) {
      console.log(error);
      return;
    }

    return `${userId}의 위치정보가 정상적으로 저장되었습니다.`;
  }

(해당 코드에서 인자로 받아온 email은 테스트를 위해 임시로 넣은 내용이니 무시해도 좋다.)

 

해당 코드를 설명하기 위해서는 ttl Set과 geo Set에 대해 먼저 다루어야 한다.

먼저, geo Set은, geo API를 사용하여 위치정보를 저장할 수 있는 공간이다. ( key : 유저 ID, value : 유저의 위도, 경도)

위치 정보가 geo Set 에 저장되어야지만, 앞서 얘기한 '특정 위치에서의 범위'에 접근할 수 있다.

그렇다면, geo Set만 있으면 될 텐데, 위 코드에 등장하는 ttl Set은 무엇인가.

 

사실, 앞서 장점에 대해 얘기했지만, geo API는, 데이터 정보를 특정 시간동안 보관하는 ttl(time to live)이 제공되지 않는다.

따라서, geo Set에 데이터가 저장될 경우, 직접 삭제하지 않는 이상 데이터가 계속 잔류하게 된다는 의미기도 하다.

물론, 실시간 위치 서비스를 제공하려하는 우리 서비스의 입장에서는 위치 정보를 계속 덧씌운다면 상관 없겠지만,

사용자가 로그아웃 했을 경우에는 위치 정보를 가지고 있을 필요가 없기 때문에 삭제하는 과정이 필요했다.

 

때문에 추가로 삽입한 것이 ttl Set이다. 이 ttl에는 { key : 유저 ID, value : ttl } 이 담기게 된다.

아래에서 설명하겠지만, ttl이 지나 삭제된 id의 경우는, ttl set에서 geo set으로 아이디가 전달되고

geo set에서도 아이디와 위칫값이 삭제되게 구현하였다.

 

브라우저에게 보여지는 사각형의 좌표를 받아, 해당 좌표 내에 포함되어 있는 유저들의 데이터를 보여주는 로직이다.

 async findLocation({ findAroundUsersInput }) {
    const { lat1, lng1, lat2, lng2 } = findAroundUsersInput;

	// redis 환경 설정
    const redisInfo: RedisOptions = {
      host: process.env.REDIS_HOST,
      port: Number(process.env.REDIS_PORT),
      db: Number(process.env.REDIS_DB),
    };

    // 주변 user들을 찾기 전, ttl 이 만료된 유저들을 필터링하는 로직.
    // 현재 시간을 기준으로 ttl 이 만료된 유저id를 geo set, ttl set에서 삭제한다.
    const client = new Redis(redisInfo);
    const now = Date.now();
    // 사실 여기 ttl set에서는 1초마다 ttl을 감소시키는 방식이 아니라, 
    // 저장당시 시간 + ttl 과 현재 시간을 비교하는 방식으로 사용하고 있다. 때문에 메서드에서는 'score'라고 표현
    const members = await client.zrangebyscore("ttlSet", "-inf", now);
    const multi = client.multi(); // multi는 다중 명령어를 실행할 수 있도록 도와주는 메서드
    members.forEach((member) => {
      multi.del(member);
      multi.zrem("ttlSet", member);	// ttlSet에서 만료된 id 삭제
      multi.zrem("geoSet", member);	// geoSet에서 만료된 id 삭제
    });
    await multi.exec(); // multi에 저장된 로직을 실행하는 메서드 exec()

    // 전달인자로 받은 사각형 꼴의 위치 좌표 내 모든 유저들의 정보들을 포함할 수 있는 반지름을 구하는 로직이다.
    // 이 함수로 하여금 유저에게 보여지는 지도 내에 위치한 유저들의 정보를 보여줄 때, 불필요한 부분을 최소화할 수 있게 되었다.
    const radius = this.getDistanceFromLatLonInKm({ lat1, lng1, lat2, lng2 });
    
    // 전달받은 좌표에서 중심점을 구하는 함수이다.
    const centerLocation = this.getCenterLocation({ lat1, lng1, lat2, lng2 });
    const [centerLng, centerLat] = centerLocation;

    const result = await client.georadius( // 'georadius' 둥근 좌표 내 데이터를 가져올 때 쓰는 메서드 
      "geoSet",	// geoSet에서
      centerLng, // 가운데 위치정보로 부터
      centerLat,
      radius, // radius만큼 떨어진 걸이까지
      "km", // km를 단위로 하여
      "withCoord", // 좌표를 반환하라는 내용이다.
    );
    console.log(result);
    return result;

 

위와 같이 작성하게 되면, 해당 로직은 해당 좌표안에 있는 유저들의 아이디와 위치정보가 담긴 배열을 이중 배열로 반환하게 된다. 

 

다음 포스팅에서는 유저 DB와 연결하여, 브라우저에 DB에 저장된 유저들의 데이터까지 반환하는 방법에 대해 포스팅한다.

 

추가로, 대한민국에서 불가능한 위도 경도가 입력될 경우(예를 들어, 위도 39이상), 오류를 반환케했다.

 

그리고 오류 메세지는 팀 전원이 통일하여, 외부에 쉽게 뚫리지 않도록 수정하였다.

Comments