Skip to content

기술 특장점

teaheepp edited this page Dec 16, 2020 · 12 revisions

기술 특장점

알고리즘

알고리즘

Kim's 알고리즘

  • Algorithm을 사용한 이유
    1. 기존 K Means Clustering에서는 K 값을 도출하기 위한 연산이 있다. 해당 연산은 성능 면에서 이슈가 될 수 있다.
    2. 결국 군집이란, 가까운 것을 묶는 것을 목표로 한다고 생각하고 있고 우리의 프로젝트에서는 군집에 대한 별도의 조건이 없으므로 디바이스 디스플레이의 크기를 12로 나누어 해당 거리에 있는 것들을 묶는 방식으로 연산을 최소화 하였다.
    let scaled = sqrt(pow(bounds.northEastLat - bounds.southWestLat, 2) + pow(bounds.northEastLng - bounds.southWestLng, 2)) / 12
    • FlowChart
      FlowChart

Kmeans

  • Kmeans는 클러스터링 알고리즘 중 가장 기본적인 알고리즘이라고 할 수 있습니다. K개의 군집으로 클러스터링을 하는 Kmeans는 간단한 알고리즘이고 또 그렇기 때문에 강력한 알고리즘이기도 합니다. 하지만 단점으로는 이름답게 K값에 따라 군집에 결과가 크게 차이 날 수 있고, 또 초기 군집의 위치에 따라 결과가 달라질 수 있습니다. 저희는 이것을 해결하기 위해 Kmeans에 몇 가지 연산을 더 하여 주었습니다. 첫 번째로 초기 클러스터링의 중심 위치를 어떻게 잡느냐입니다.
  • 기본 Kmeans는 랜덤으로 위치를 생성하지만 이렇게 된다면 같은 데이터로 클러스터링을 하더라도 클러스터링을 할 때마다 결과가 달라질 수 있습니다. 동일한 데이터에선 동일한 클러스터링 결과를 얻어내고 싶어서 초기 중심 위치를 적용하기 위해 약간의 연산을 진행했는데 이 클러스터링 위치를 최대한 멀리 떨어 뜨려 놓는 작업을 진행했습니다. 이렇게 함으로써 동일한 데이터에선 동일한 초기 위치를 결정 할 수 있었으며 Kmeans과정에서 중심값을 이동시키며 재조정하는 작업을 줄일 수 있었습니다. 왜냐하면 초깃값부터 어느 정도 분산이 되어있기 때문입니다.
  • 다음으로는 K값을 결정하는 방법입니다. 사실 초기값보다 더 중요한 게 K를 어떻게 설정할지 정하는 것입니다. K값을 결정하는 방법은 Rule of thumb, Elbow Method 등이 있을 수 있는데 저희 팀은 Penalty라는 개념을 도입했습니다. Kmeans 알고리즘의 목적함수는 클러스터링의 분산의 합의 최소화인데 이 값은 K값이 커질수록 값이 작아져서 이 값을 그대로 사용해서 최소인 클러스터링을 판단하면 그냥 K가 제일 큰 클러스터링이 뽑히게 됩니다. 이것을 해결하고자 K값을 늘리는 데 Penalty를 부여하였습니다. 예를 들어 주어진 데이터를 K를 1~20까지 해보고 각각의 목적함수의 값을 계산한 후 평균 감소 비용을 구해서 이것을 Penalty의 값으로 결정하였습니다.
  • 그렇다면 주어진 데이터에서 K값을 1부터 몇 가지 검증을 해야 하느냐도 문제였습니다. Maximum K는 원활한 사용자 상호작용을 위해서 주어진 데이터의 개수에 반비례해서 어느 정도 연산량을 보장해줄 수 있게 결정했습니다.

관련 PR1
관련 PR2

하단 풀업 뷰

하단 풀업 뷰

Pan Gesture의 다양한 Case에 대응

  • 개요

    • 하단 풀업 뷰의 상태는 3가지가 있습니다.
      상태 설명
      Short 화면 하단부터 1/3이 차도록 위치하는 상태. CollectionView의 데이터 수와 첫 번째 데이터 정도를 간단히 확인할 수 있습니다.
      Half 화면 하단부터 절반을 차지하는 상태. 적절한 Interaction을 통해 지도와 CollectionView 목록을 함께 볼 수 있습니다.
      Full 화면의 90%를 차지하는 상태. CollectionView를 자세히 들여다 보며 원하는 데이터를 찾을 수 있습니다.
    • 사용자의 Interaction에 따라 3가지 상태가 자연스럽게 전환되도록 하였습니다.
  • 잡아 끌다가 놓는 Pan Gesture

    • 기본적으로 풀업 뷰는 사용자의 Pan Gesture에 따라 끌어 올려지거나 끌어 내려지도록 하였습니다.
    • 그리고 놓아지는 위치에 맞는 Position으로 달라붙도록 하였습니다.
      • 화면을 3등분 했을 때 하단 영역에서 놓아지면 Short
      • 화면의 중간 영역에서 놓아지면 Half
      • 화면의 상단 영역에서 놓아지면 Full
  • 빠르고 짧게 스와이프 하듯 던져 올리는 Pan Gesture

    • 임의의 Pan Gesture 속도 기준 값을 정하고 기준 값을 넘기는 Gesture가 들어올 때 빠르게 다음 상태로의 전환이 되도록 하였습니다.
    • 빠른 Gesture에 적절히 대응함으로써 더 나은 UX를 이끌어냈습니다.
  • 지도 뷰 위로의 Gesture를 자연스럽게 피하면서 접혀 내려지는 상태 전환

    • 풀업 뷰가 Half, Full 상태에서 지도 뷰 위로의 Gesture가 생기면 풀업 뷰를 무조건 내리는 것이 아닌 풀업 뷰가 방해가 되는 경우만 피해서 내려지도록 하였습니다.
    • Half 상태에서 지도 뷰를 움직이며 CollectionView를 같이 살펴볼 수 있도록 하였습니다.
  • 이와 같이 사용자의 Pan Gesture에 꼼꼼히 대응하기 위해서 애플 지도의 풀업 뷰를 참고하고 네이버, 카카오 지도 앱과 비교해보며 어떻게 하면 사용자가 처음 사용해도 익숙하듯이 쉽게 사용할 수 있을지, 더 나은 UX를 위한 고민을 하며 개발하였습니다.

    시연 더 나은 화질의 비디오로 보기⤵️

내부 CollectionView의 비동기 데이터 처리

  • 개요

    • 풀업 뷰의 CollectionView에는 POI들이 나열되어 있습니다. Cell은 각 POI의 이름, 주소, 이미지를 포함합니다.
    • 이 때, POI 데이터 모델에는 이름, 위도, 경도, 이미지 url 정보만 있기 때문에 위도와 경도는 네이버 SDK를 이용해서 도로명 주소로 변환, 이미지 url은 이미지 파일로 변환하는 작업이 필요합니다.
    • 이 작업들을 비동기로 수행하고 캐싱을 통해서 사용자가 Interaction중에 불편함이 생기지 않도록 하였습니다.
  • 비동기 Prefetch, 캐싱

    • Cell을 DataSource로부터 생성하여 그릴 때 캐시 데이터를 가져와서 넣어줍니다.
    • 캐시 데이터가 없다면 임시로 "불러오는 중"을 표시하는 데이터를 넣어둡니다. 그리고 Cell의 데이터를 로딩해서 캐시처리하는 AsyncFetchcompletionHandler와 함께 요청을 보냅니다.
    • 캐시 데이터를 미리 만들어 놓기 위해서 UICollectionViewDataSourcePrefetching 프로토콜을 채택하여 구현합니다. 여기서 AsyncFetch에 미리 캐시 요청을 보내놓습니다.
    • AsyncFetch는 전달 받은 데이터 모델을 가지고 Cell이 뷰를 그리는 데에 사용할 수 있게 변환하고 캐시 데이터를 만듭니다. 이 때 completionHandler도 같이 전달 받으면 Prefetch 작업이 아닌 바로 그려야 하는 작업이기 때문에 캐시 데이터를 만들고 바로 completionHandler에 전달해줍니다.
    • CollectionView의 Cell이 재사용될 때마다 이미지 url을 이미지로 변환하는 것은 지극히 비효율 적이고 UX면에서도 좋지 않은데, 모든 데이터 모델을 캐싱해서 사용함으로써 Cell 재사용에 따른 이미지 반복 변환 문제를 해결하였습니다.
    • 풀업 뷰를 닫으면 바로 NSCache 인스턴스 해제, 멀티 스레드에 쌓인 이미지 변환을 위한 URLSession의 비동기 Task 취소를 통해 불필요한 메모리, CPU 점유율을 바로 낮추도록 하였습니다.
    시연 더 나은 화질의 비디오로 보기⤵️
애니메이션

애니메이션

  • 우리는 애플리케이션과 사용자의 상호작용을 최대한 높이려 노력했습니다.

    • 카메라의 줌 인/아웃이 빠른 핀치 동작과 느린 핀치동작 모두 대응할 수 있습니다.
  • 코어애니메이션으로 구현했습니다. 하나의 마커는 각각의 백그라운드 스레드에서 수행됩니다. 따라서 main 스레드와 cpu의 부담을 줄일 수 있습니다.

  • 사용자가 천천히 핀치 동작을 할때

    • 지속적으로 변하는 카메라 줌 레벨에 맞춰 마커들의 애니메이션 역시 동적으로 수행됩니다. 마커들이 이동하는 조건은 자신에게 속해있던 POI가 새로 클러스터링된 마커들 중 특정 마커에 속해있다면, 해당 마커로 이동하는 애니메이션이 수행됩니다. 기존의 마커가 변화가 없다면 애니메이션을 주지 않아 사용자가 시각적으로 느끼는 어색함을 크게 줄였습니다.

    • 클러스터링 알고리즘을 거쳐 나오는 타입은 클러스터 배열입니다. 각 클러스터는 POI들을 가지고 있습니다. 마커 이동 애니메이션을 위한 조건을 따질 때, POI로 결정되기 때문에 최악의 경우 시간복잡도는 O(n^2)입니다. 그래서 탐색 시간을 줄이기 위해 클러스터에 POI를 가진 딕셔너리를 만들어 탐색 시간을 최악의 경우 O(N)으로 줄였습니다.

      스크린샷 2020-12-16 오후 9 47 47
  • 사용자가 빠른 핀치 동작으로 카메라 줌 레벨이 급격하게 변할때

    • 화면에 이미 진행되고 있는 애니메이션이 있다면, 현재 진행중인 애니메이션을 모두 취소하고 현재 카메라 줌 레벨에 맞는 마커들을 화면에 바로 나타내줍니다. 사용자의 빠른 핀치 동작에 즉각적으로 대응되기 때문에 사용자는 앱과 높은 상호작용을 느낄 수 있습니다.

    • 진행중인 애니메이션을 취소한 방법

      • 마커의 애니메이션은 각각 백그라운드 스레드들에 의해 수행됩니다. 애니메이션을 수행하는 스레드들은 애니메이션의 취소 여부를 나타내는 Bool 값 프로퍼티를 확인합니다. 만약 취소된 것을 확인한다면 애니메이션중인 레이어를 모두 지우고, 현재 줌 레벨(바운드)에 맞는 마커들을 바로 화면에 추가합니다.
      스크린샷 2020-12-16 오후 9 50 14
    • 취소할 필요가 없는 경우

      • 애니메이션을 수행하기 위한 스레드들은 자신이 수행하는 애니메이션을 끝마치면 스레드에 safe한 프로퍼티를 카운트합니다. 마지막으로 애니메이션을 끝마친 스레드는 해당 프로퍼티로 자신이 마지막으로 수행되는것을 인지합니다. 마지막 스레드에서 애니메이션의 취소 여부를 나타내는 Bool 값 프로퍼티를 확인하고, 취소된 상태가 아니면 새로 클러스터링된 마커들을 올바른 위치에 그립니다.
  • Search Animation

    • 현재는 로컬데이터와 빠른 알고리즘으로 결과가 바로나오지만, 네트워크를 사용하고 좀 더 무거운 클러스터링 알고리즘을 사용한다면 사용자가 기다리는 시간이 생길것이라 판단하였고 그동안 조금이라도 지루하지 않게 Search 애니메이션을 적용하였습니다. Core Animation의 CABasic, CAKeyFrame Animation을 사용하여 구현하였습니다. 글자 하나하나가 움직이는 애니메이션을 구현하기 위헤 AniTextLayer를 만들었고 주어진 텍스트를 폰트사이즈의 맞게 width height를 결정하여 각각 TextLayer로 만들고 Layer들은 전체 frame의 중앙에 위치시킵니다. 그 후 Keyframe transfrom translateY 애니메이션을 적용 하여서 튀는 느낌을 주었습니다.

    관련PR

  • Marker Layer

    marker-2 marker-4
    marker1 marker2
    • BezierPath와 Layer를 사용하여 마커를 직접 구현하였습니다. 기본 네이버 SDK에서 제공하는 마커에 비해 둥글둥글한게 특징입니다. BezierPath의 호를 그리는 함수를 통해 두개의 호를 통해 가장 바깥쪽의 layer를 만든 후 TextLayer를 add 하여주었습니다. 그후 shadow를 적용후 완료 하였습니다. 숫자의 font size는 text의 길이와 frame값을 통해 결정됩니다.
  • Star layer

    star star-rounded
    star1 star2
    • Star animation할때 사용되는 별 Layer입니다. 인터넷에서 돌아다니는 뾰족뾰족한 별 Layer에 라운드를 준 둥글둥글한 별 Layer를 만들어 봤습니다. 별의 꼭지점을 BezierPath로 원의 호를 그려서 연결해주었습니다. BezierPath를 그린뒤 ShapeLayer를 통해 잘라주고 그 ShapeLayer를 GradientLayer에 add한뒤 완성했습니다.
  • Logo layer

    Logo
    스크린샷 2020-12-15 오전 11 52 01
    • 앱 splash 시작화면에서 사용되는 Logo layer입니다. bezierPath로 그린 뒤 마커를 그린 뒤, shadow와 rotation을 주었습니다.

Reference

Core Animation Programming Guide
Concurrency Programming Guide
Threading Programming Guide

Clone this wiki locally