LINE 메신저 앱의 공유 모듈 통합 개발기 – 1

안녕하세요. LINE iOS 클라이언트 개발을 담당하고 있는 조현지, 이건홍입니다. 이번 글에선 LINE iOS 공유 기능 모듈을 개발하면서 겪었던 어려움과 이를 해결해나간 방법에 대해서 이야기하려고 합니다.

 

공유 모듈 소개

LINE 앱에는 글, 이미지, 비디오 등의 콘텐츠를 자유롭게 업로드하고 접근할 수 있는 대화방, 앨범, 노트, 타임라인, 킵(Keep) 등 다양한 서비스가 있습니다. 또한 메시지를 다른 대화방으로 전달하거나, 킵으로 저장하는 등 서비스에서 서비스로 콘텐츠를 공유할 수 있는 기능을 여러 화면에서 지원하고 있습니다. 기존에는 이러한 공유 기능을 서비스 또는 화면별로 여러 개발자가 각자 구현해서 유지, 보수하고 있었습니다. 모두 유사한 기능을 수행하고 있었지만 각자 관리됐기 때문에 아래와 같은 여러 가지 문제점이 있었습니다.

  • 화면별 UI/UX가 일관적이지 않음
  • 대상 콘텐츠를 나타내는 데이터 모델이 다양함
  • 유사하거나 중복되는 코드가 존재함
  • 로직과 요구 사항을 파악하기 어려워 유지 보수와 기능 확장이 까다로움

이런 문제점을 개선하기 위해서 LINE 내부의 모든 공유 기능을 통합하는 프로젝트를 시작했습니다. 기존 UX의 불편한 점을 개선한 새로운 공유 화면을 도입하고, 여러 서비스의 공유 로직을 통합한 하나의 모듈을 개발해서 현재 LINE 내 대부분의 공유 기능에 적용했습니다.

이미지 뷰어 화면이전 저장 및 공유 선택 화면현재 공유 선택 화면
 

저희의 개발 목표는 레거시 코드를 통합, 위에서 살펴보았던 문제점을 해결하고 앞으로 공유 기능을 추가하고 관리하는 데 사용될 개발 리소스를 줄이는 것이었습니다. 서비스와 콘텐츠의 종류가 매우 다양한 상황에서 현존하는 시나리오를 모두 처리할 수 있는 하나의 모듈을 만들려다 보니 기획하고 개발하는 데 많은 어려움이 있었습니다.

이번 글에서는 그 과정에서 저희가 고민했던 점과 그 해결 방법을 크게 여섯 가지로 나누어 공유하려고 합니다. 그중 이번 글에서는 2. 데이터 공유 로직 통합까지 말씀드리고 이후 나머지는 2편에서 마무리하겠습니다.

  1. 데이터 모델 규약 통합
  2. 데이터 공유 로직 통합
  3. 사용 가능한 서비스 목록 판단
  4. 커스텀/추가 액션 제공
  5. 예외 처리 일반화
  6. UI/UX 개선

 

1. 데이터 모델 규약 통합

기존엔 데이터 모델의 정의가 서비스마다 달라서, 서비스에서 서비스로 데이터를 전달할 때 복잡한 변환 과정을 거쳐야 했습니다. 이런 복잡한 과정을 제거하기 위해서 모든 서비스에서 사용할 수 있는 데이터 모델 규약을 정의했습니다.

그림 1-1. 서비스 간 데이터 변환 구조그림 1-2. 통일된 모델 적용 후 서비스 간 데이터 변환 구조

 

문제점

LINE의 여러 가지 서비스에 공유 기능이 존재했지만 전달하려는 데이터를 나타내는 데이터 모델은 통일되어 있지 않았습니다. 그래서 서비스 간 공유할 땐 그림 1-1처럼 각 서비스에 맞게 데이터를 변환해야 했습니다. 예를 들어 대화방에서 킵으로 공유하기 위해서는 메시지를 킵에 맞는 데이터로 변환해야 했고, 킵에서 타임라인으로 공유하기 위해서는 킵의 데이터를 타임라인에 맞는 데이터로 변환해야 했습니다. 그래서 LINE 전체적으로 서비스 간 데이터 변환이 각각 구현되어 있었습니다. 

오랫동안 유지 보수되었던, 데이터 유형이 복잡한 형태를 띠고 있어서 이해하기 쉽지 않았던 대화방 공유 코드를 예로 들어보겠습니다. 대화방으로 전달하는 데이터를 나타내는 NLSharableObject라는 클래스는 아래와 같은 정보를 가지고 있지만, 모든 서비스에서 같은 형태로 정보를 다루지 않아서 대화방으로 공유할 때만 사용할 수 있었습니다.

  • 공유할 콘텐츠의 데이터 – StringUIImageNSDictionary 등
  • 콘텐츠 유형 정보 – 텍스트, 이미지, 파일, 위치 정보 등

NLSharableObject 내부에서 콘텐츠의 종류를 구별하기 위해 선언된 유형 정보는 데이터 전송의 내부 로직과 매우 밀접하게 연관되어 있었습니다. 예를 들면 이미지를 공유할 때도 아래 세 가지 경우에 따라 다른 유형으로 나뉘었습니다. 유형 정보가 매우 상세하게 구별되어 있다 보니, 코드를 보고 단번에 어떤 유형이 무엇을 나타내는지 파악하는 게 어려웠습니다. 

  • 메모리에 UIImage 인스턴스가 존재할 때
  • 기기에 저장된 파일이 존재할 때
  • 서버에서 받아올 수 있는 이미지일 때

또한 대화방으로 데이터를 공유하는 역할을 담당하는 InAppShareContext라는 클래스가 있습니다. 이 클래스에서는 UIImage나 String 등의 다양한 공유 데이터를 처리하고 있는데요. NLSharableObject 또한 여러 가지 공유 데이터 중 하나로 처리되고 있었습니다. 공유 데이터 자체와, 공유 데이터를 한 번 감싼 클래스가 같은 레벨의 프로퍼티로 관리되고 있었던 것입니다. InAppShareContext가 공유 데이터를 다양한 데이터 유형으로 처리할 수 있기 때문에 사용하는 측에서는 상황에 따라 적절한 데이터 유형을 선택해서 코드를 자유롭게 작성할 수 있었습니다. 하지만 어떤 유형의 콘텐츠가 전송되고 있는지 알려면 확인해야 할 코드가 많아서 데이터 유형에 따라 달라지는 예외 처리와 같은 작업을 하기가 까다로웠습니다.

그 외에도, 공유 데이터를 Dictionary를 이용해 주고받는 경우가 있어서 어떤 콘텐츠가 전송되고 있는지 파악하기 힘들었습니다. Dictionary에 정보를 담아서 전송하면 자유롭게 값을 추가할 수 있지만 불필요한 데이터도 같이 전송될 수 있다는 위험이 있고, 어떤 데이터가 어떤 키로 저장되었는지 파악하기 힘들다는 문제가 있습니다. 

마지막으로, 이미 정의되어 있었지만 사용하기 어려웠던 데이터 모델을 쉽게 사용하려고 추가한 모델이 있었습니다. 기존 클래스를 한 번 더 감싼 ShareTarget이라는 모델이 사용되고 있었는데요. 이로 인해 결국 하나의 데이터를 전송할 때 정보가 어떻게 흘러가는지 추적하기 위해서는 한 단계를 더 거쳐야 했습니다.

그림 2. 기존 이미지 메시지 공유 시 데이터 변환 흐름

결과적으로, 대화방에 이미지 하나를 공유할 때도 위 그림 2와 같이 데이터를 여러 번 변환해야 하는 구조가 되었습니다. Image 데이터를 ShareTarget으로 만들어 InAppSharing에 전달하면, InAppSharing에서 다시 데이터를 추출해 InAppShareContext에서 처리할 수 있게 만듭니다. InAppShareContext에서는 이를 NLSharableObject로 변환한 다음 다시 Message 데이터로 변환해서 전송합니다. 이렇게 복잡한 데이터 변환 과정 때문에 전달되는 데이터들의 유형을 관리하기가 힘들고 정보의 흐름을 추적하기가 어려웠습니다. 또한 데이터 변환과 관련된 코드를 유지 보수하는 것도 어려웠습니다.

 

해결책

서비스 간 데이터를 1:1로 변환하는 코드를 제거하기 위해서, 모든 서비스에서 필요한 정보를 표현할 수 있는 최소한의 변수를 정의하여 아래와 같이 모든 서비스에서 사용할 수 있는 단일화된 데이터 모델을 정의했습니다. 

그림 3. SharableObject

공유하려는 콘텐츠를 나타내는 정보는 여러 서비스에서 공통으로 사용할 수 있는 정보도 있고, 진입 서비스에 따라서 다르게 요구되는 정보도 있는데요. 지금부터 이러한 정보들을 분류한 방법과, 그렇게 분류한 데이터 모델을 어떻게 정의했는지에 대해 자세히 설명하겠습니다.

 

공통 정보

먼저, 공유될 콘텐츠를 나타내는 텍스트, 이미지, 섬네일 이미지 등의 기본 데이터를 정의했습니다. 그리고 이미지, 비디오 등 일부 콘텐츠에서 공통적으로 사용할 수 있는 여러 가지 정보는 별도의 구조체(struct)로 정의했습니다. 이미지나 비디오, 파일 콘텐츠의 크기, 오디오 또는 비디오의 재생시간 등 콘텐츠의 특성을 나타내는 정보를 ContentInfo로, 서버에 존재하는 콘텐츠라면 공통적으로 필요한 식별자나 URL 등을 OBSInfo로 나타냈습니다.

 

서비스에 따라 다른 정보

단일화시킨 데이터 모델을 도입한 뒤 이상적인 경우라면 공유 모듈에서 필요한 데이터 외에 특정 서비스와 관련된 데이터가 필요하지 않을 수도 있습니다. 하지만 저희의 경우엔 각 서비스가 이미 서로 다른 특성을 갖고 있었기 때문에 여전히 진입 서비스에 따라 별도로 필요한 정보가 있었습니다. 대화방에서 메시지를 공유하는 경우를 예로 들면 아래와 같이 추가적인 정보가 필요합니다.

  • 진입 서비스에 따라 공유 콘텐츠를 다루기 위해서 추가로 필요한 정보
    • 메시지 콘텐츠의 종류와 관계없이 메시지를 전달할 때는 메시지 고유의 ID나 전송한 사람의 ID가 필요합니다. 또 메시지에 LINE sticon이 포함되어 있다면, sticon의 메타데이터도 같이 전달해야 합니다. 이러한 정보는 대화방에서만 의미 있는 정보이므로 킵이나 앨범 등에서 공유할 때는 전혀 필요하지 않습니다. 
  • 공유를 시작할 때 진입한 서비스의 상태 값 등을 표현하는 정보
    • 어떤 대화방에서 메시지가 많이 전달되었는지 관련 통계 데이터를 수집하기 위해서 대화방 ID나 대화방 종류와 같은 정보를 전송해야 합니다. 공유 화면을 연 진입 서비스에 관련된 정보로, 공유하려는 콘텐츠와는 관련이 없는 정보입니다.

이러한 데이터는 대화방뿐만 아니라 킵, 타임라인 등에서도 다양한 형태로 필요합니다. 그래서 첫 번째로 설명한 진입 서비스에 따라 별도로 필요한 콘텐츠 정보는 각 서비스별로 MessageInfoKeepInfoPostInfoAlbumInfo 등의 구조체로 정의했으며, SharableObject의 데이터로 사용합니다. 두 번째로 설명한 진입 서비스와 관련된 정보는 InAppShareSourceInfo라는 구조체로 정의했습니다. InAppShareSourceInfo는 공유하려는 콘텐츠와는 관련이 없는 정보이므로 공유 데이터 전송을 담당하는 클래스인 InAppShareContext에서 사용하도록 분리했습니다. 따라서 SharableObject에는 콘텐츠 자체와 관련이 있는 최소한의 변수만 유지할 수 있게 되었습니다.

이제 모든 서비스에서 각자의 데이터를 SharableObject로 변환할 수 있기 때문에 SharableObject 외의 정보는 더 이상 공유 모듈에서 필요하지 않습니다. 그래서 사용자가 공유할 서비스를 결정하면 그 서비스에서 데이터를 처리할 수 있도록 알맞은 형태로 데이터를 다시 변환해서 전달하기만 하면 되도록 단순하게 수정되었습니다. 정리하자면, 기존에 복잡하게 데이터를 변환하던 로직이 진입 서비스의 데이터 → SharableObject → 최종 공유 서비스의 데이터 순서로 변환하는 형태로 변경되었습니다. 이런 구조를 그림으로 나타내면 아래 그림과 같습니다. 이전의 데이터 변환 구조보다 훨씬 명료해진 것을 알 수 있습니다.

통일된 SharableObject 모델 적용

 

2. 데이터 공유 로직 통합

모든 서비스-서비스 관계에 대하여 일일이 구현했던 데이터 전송 로직을 통합하여 중복 코드를 제거했습니다. 그리고 진입 서비스의 종류와 관계없이 언제나 같은 로직을 사용하기 위해서 모든 서비스로의 공유를 담당하는 InAppShareContext 클래스를 구현했습니다.

그림 4-1. 서비스 간 데이터 전송 로직 구조그림 4-2. 공유 로직 통합 후 데이터 전송 로직 구조

 

문제점

기존 코드는 공유를 시작하는 서비스에서 데이터를 전송하려고 하는 최종 서비스, 즉 진입 서비스에서 목적 서비스로 데이터를 전송하는 로직이 통일되지 않은 형태로 각자 구현된 상태였습니다. 데이터를 전달하는 방식 또한 필요한 정보를 Dictionary에 담아서 클래스에서 클래스로 전달하거나, iOS NotificationCenter를 이용하여 전달하는 등 각자의 방식으로 구현되어 있었습니다. 기존의 InAppShareContext 클래스에서 콘텐츠를 대화방으로 간단히 공유할 수 있는 메서드를 제공하고 있었지만, 다른 서비스로 공유하는 것은 다루고 있지 않았습니다. 

따라서 위 그림 4-1과 같이 공유를 시작할 수 있는 서비스가 n 개, 콘텐츠를 받아서 처리할 수 있는 서비스가 m 개라고 한다면 최대 n*m 가지의 다른 공유 코드가 생길 수 있는 형태여서 관련 코드의 양이 방대할 뿐만 아니라 중복된 코드가 많아 유지 보수하는데 많은 비용이 들었습니다.

 

해결책

모든 진입 서비스에서 모든 목적 서비스로의 공유를 연결하는 클래스를 구현하였습니다. 기존에 대화방으로 공유하는 것을 담당하고 있던 InAppShareContext라는 클래스의 기능을 확장해서 대화방뿐만 아니라 모든 서비스로의 데이터 전송을 할 수 있도록 변경하고, 각 서비스에서 각자 구현되었던 공유 로직을 제거했습니다.

1. 데이터 모델 규약 통합에서 소개했던 SharableObject와 InAppShareSourceInfo를 이용해서 공유 데이터와 진입 서비스의 상태 정보를 전달하면 InAppShareContext를 이용해서 어떤 서비스로든 데이터를 전송할 수 있게 되었습니다. 그림 4-2처럼 모든 진입 서비스에서 모든 목적 서비스로 공유하는 작업이 하나의 클래스를 거치도록 구조를 변경했습니다. 

마지막으로, 진입 서비스에서 더 편리하게 공유 코드를 작성할 수 있는 메서드를 제공합니다. InAppShareContext를 사용하기 위해서는 언제나 SharableObject를 생성해서 전달해야 하는데요. LINE 내에서 공유되는 콘텐츠에선 보통 텍스트, 이미지, 위치 정보 등의 몇 가지 데이터 유형이 자주 사용됩니다. 그래서 진입 서비스에서 매번 SharableObject를 직접 생성하지 않고 텍스트, 이미지, 위치 정보 등의 일반적인 공유 데이터만 전달해서 공유 모듈을 호출할 수 있는 아래와 같은 메서드를 제공하고 있습니다. InAppSharing이라는 정적(static) 클래스에 공유 데이터를 전달하면 내부에서 SharableObject를 생성하는 방식입니다.

결과적으로 일반적인 데이터를 공유할 때는 SharableObject를 명시적으로 생성해서 전달하는 코드를 작성할 필요가 없습니다. InAppSharing의 메서드를 호출하는 코드 한 줄로 공유 화면을 실행할 수 있습니다.

 

마치며

이번 글에서는 공유 모듈을 구현하기 위해 필요한 공용 데이터 모델과 서비스 공유 로직을 통합하는 것에 중점을 두고 살펴보았습니다. 다음 편에서는 서비스와 관련된 다양한 요구 사항을 만족하면서도 유지 보수가 편하고 확장 가능한 구조로 구현하기 위해 저희가 했던 고민과 해결 방법을 말씀드리겠습니다. 다음 편을 기대해 주세요!

Related Post