네이버 클로바노트팀의 눈으로 보며 듣는 음성 기록, 클로바 노트 서비스의 웹 기술 톺아보기라는 발표를 듣고 배운점과 생각해볼 것들에 대해서 정리했습니다.
발표의 CONTENTS를 보면 흥미로운 키워드들이 많았고 이런 키워드들 중에서도 가장 집중한 키워드는 오프라인 지원이었습니다.
오프라인 지원이라는 키워드에 집중한 이유는 작년에 소프트웨어 마에스트로에서 진행한 프로젝트에서 PWA를 만들며 PWA의 다양한 기능들 중 웹에서 오프라인 환경을 제공하기 위해 고민했었고, PWA를 만들며 했던 기술적 도전들과 겪었던 문제점들에 대해 다시 생각해보고 싶었기 때문입니다.
먼저 클로바 노트라는 서비스가 어떤 서비스인지 알아보겠습니다.
클로바노트는 서비스는 말을 녹음하면 글로 변환해주는 서비스입니다. 수업, 강의, 미팅등에서 녹음을하고 놓쳤던부분을 글로 보여주고 회의록으로도 제공해줍니다.
목소리 인식을 통해 화자별로 UI를 분리해서 보여주기도 하고 AI 요약기능을 통해 내용을 요약해주기도 합니다.
더욱 흥미로운점은 줌과 클로바노트를 연동하여 줌 회의를 할 때마다 자동으로 노트로 변환할 수 있는 기능도 제공한다는 점입니다.
클로바노트가 어떤 서비스인지 알아보았으니 이제 클로바노트를 지탱하는 웹 기술에 대해 알아봅시다.
클로바노트 V2에서는 오프라인 환경을 지원합니다. 그리고 이를 지원하기 위해 Next.js와 Next-PWA 모듈을 사용한 것을 볼 수 있습니다.
먼저 클로바노트에서는 왜 Next.js와 Next-PWA 모듈을 사용했을까요? 우선 리액트를 사용하면 개발자가 DOM을 직접 조작하지 않고 최적화된 재조정 알고리즘을 통해 변경사항이 자동으로 UI에 그려지고 컴포넌트 주도적으로 개발을 할 수 있는등 다양한 장점이 존재합니다.
리액트를 사용하기로 했으면 리액트 프레임워크인 Next.js는 CSR뿐만 아니라 다양한 렌더링 패턴을 쉽게 적용할 수 있게 해주며 리액트의 기능을 더 확장해서 사용할 수 있게 해줍니다. 따라서 Next.js의 선택 또한 자연스럽습니다.
Next-PWA 모듈은 왜 사용했을까요?
서비스 워커를 자바스크립트를 통해 제어하다보면 서비스워커 로직뿐만 아니라 코드가 읽기 어려워지고 이해하기 어려워지는것을 쉽게 경험할 수 있습니다. Workbox는 이러한 문제점을 해결할 수 있습니다. Workbox의 목표는 서비스워커를 쉽게 사용함에 있고 동시에 필요한 경우 복잡한 응용 프로그램 요구 사항을 수용할 수 있는 유연성을 제공하는 것을 목표로 합니다.
Workbox를 사용해 코드를 개선한 경험
Cache Storage API를 Workbox를 사용하지 않고 제어하는 코드와 Workbox를 사용해 Cache Storage API 제어하는 코드를 비교해봅시다. Workbox는 다양한 캐싱 전략을 쉽게 애플리케이션에 적용할 수 있게 해줍니다.
웹을 오프라인에서도 동작하게 하려면 우선 정적 파일들을 Caching해야 합니다. 정적 파일들을 캐싱해두면 사용자가 오프라인에서 접근했을 때 캐싱된 정적 파일을 제공할 수 있습니다.(Read)
하지만 사용자가 오프라인에서 작업(Create, Update, Delete)을 하면 어떻게 해야할까요? CUD가 필요한 경우에는 IndexedDB에 오프라인에서 했던 행동들을 기록하고 Background Synchronization API를 사용하여 서버에 동기화 시킵니다.
여기서 많은 문제가 발생하게됩니다. 쉽게 생각할 수 있는 문제로는 네트워크가 연결되어 동기화를 할 때 오프라인에서 작업한 내용과 서버에 있는 데이터와 충돌이 일어난다면 어떻게 처리할 것인가?
여기에는 다양한 대답이 있을 수 있을 것 같습니다. 개발할 당시 생각했던 것은 데이터를 수정할 때마다 최근 수정 시간을 기록하여 가장 최근에 수정한 것이 최신이라고 생각하고 덮어씌우는 방법을 생각했었습니다.
클로바노트에서는 설정값들은 모두 localStorage에 저장하고 노트 목록, 노트 상세등의 데이터는 IndexedDB에 저장합니다. 그리고 일단 CUD 부분을 막았습니다.
클로바노트는 댓글 같이 추가로 작성하는 서비스가 아니고 동시에 편집가능한 서비스를 지향하고 있기 때문에 로컬DB에 저장하고 서버에 Sync를 맞추려면 충돌이 무조건 발생하게 됩니다. 클로바노트팀에서는 이 부분을 해결하기 위해서 CRDT(Conflict Free Replicated Data Types)라는 기법을 준비중에 있고 Naver에서 개발중인 yorkie 라이브러리를 접목하려고 준비중이라고 합니다.
CRDT 기법이 자연스럽게 궁금해졌고 이에 대해서 찾아보니 피그마에서 실시간 동시 편집 기술을 구현하기 위해 사용되었다고 합니다.
다음은 반응형에 관한 내용입니다. 클로바노트는 PC, Tablet, Mobile의 UI를 하나의 소스에서 개발하고 있습니다.
반응형 홈페이지를 개발하면 지원하는 디바이스가 자연스럽게 많아지게되고 손가락, 펜슬, Multi Input Device등 사용자의 입력이 다양한 방식으로 들어오게 됩니다. 따라서 이에 대응되는 적절한 이벤트 핸들링이 필요합니다.
여기서 짚고 넘어가는 포인트는 현재는 PC에서도 터치를 할 수 있다는 점과 모바일에서도 마우스를 사용할 수 있다는 점입니다.
이렇기 때문에 클로바팀에서는 modernizr를 사용하여 현재의 input device가 터치인지 마우스인지를 구별하고 이를 통해 반응형 UX를 지향한다고 합니다.
이제 User-Agent를 사용해서 사용자를 구분하는 시대는 끝났다.
클로바팀에서는 User-Agent는 단순히 참고용으로만 사용하고 있다고 합니다.
목록 선택은 반응형 UX의 좋은 예시입니다.
목록을 선택할 때 마우스를 사용하면 hover시에 체크박스를 노출하는 것이 자연스럽고 hover이벤트가 없는 테블릿과 같은 터치환경에서는 체크박스를 기본적으로 노출하는 것이 자연스럽습니다.
다음은 보조 기능 메뉴에 대한 예시입니다.
마우스를 사용하면 우클릭을 통해 컨텍스트 메뉴 노출이 자연스럽고 터치환경에서는 스와이프를 하여 컨텍스트 메뉴를 노출시키는 것이 자연스럽습니다.
아이템 이동시에는 마우스로는 드래그앱 드랍을 통해 이동하는 것이 자연스럽고 터치 환경에서는 길게 누른 후 드래그앤 드랍을 통해 이동하는 것이 자연스럽습니다.
클로바노트에서 PoC로 시작했다가 클로바노트의 기능까지 연결된 기능이 있는데, 바로 ZOOM 연동 기능입니다. ZOOM 회의 중 기록이라는 기능을 사용하면 회의의 녹음된 파일을 로컬이나 클라우드에 저장할 수 있습니다.
이 파일을 클로바노트에 업로드하면 노트에서 제공하는 화자분리와 에디터 기능을 사용할 수 있습니다.
클로바노트 팀은 ZOOM 회의에서 기록을 누르고 수동으로 클로바노트에 업로드하는 이 과정을 자동화 하기를 원했다고 합니다.
클로바 노트에서는 ZOOM에서 제공하는 다양한 API중 REST API와 WebHook을 사용합니다. 줌 회의 중 회의 기록과 같은 버튼을 REST API를 실행하여 대신 눌러줄 수 있었고, Webhook은 특정 이벤트를 받아볼 수 있어서 줌 미팅이 시작되고 참여자가 들어오고 줌미팅이 끝난 뒤에 녹음 파일이 생성되었다는 이벤트를 받아볼 수 있으므로 이 두가지를 이용해서 회의 기록 및 클로바노트에 업로드를 자동화 했습니다.
업로드 자동화를 적용한 시퀀스 다이어그램은 위와 같습니다.(훅을 받을 Receiver 서버를 만들어주고 연결해줍니다.)
클로바노트팀은 여기서 한발자국 더 나아가서 진행중인 줌의 오디오 데이터를 실시간으로 가져오기로 합니다.(진행중인 회의를 실시간 요약!)
이를 위해 ZOOM에서 제공하는 기능중 라이브 스트림이라는 기능을 활용합니다.
유명한 플랫폼뿐만 아니라 규격만 맞춰준다면 커스텀으로 만들어진 다른 브로드캐스트 서버도 연결될 수 있어서 라이브 스트림을 받을 수 있는 서버를 만들어준다면 현재 진행 중인 ZOOM의 데이터를 실시간으로 받을 수 있습니다.
이 때 RTMP(Real Time Message Protocol)를 사용합니다. 라이브를 한다면 거의 표준으로 사용하고 있는 실시간 관련 프로토콜입니다.
이 RTMP를 통해 데이터를 받을 서버를 만들어야 했고 Node-Media-Server를 fork해서 필요없는 부분을 걷어낸 후 RTMP 백엔드 서버를 구축했다고 합니다.
결국 RTMP를 사용한 서버를 추가하면 위와 같은 시퀀스 다이어그램을 가지게 됩니다. 클로바노트팀에서는 점진적으로 라이브 스트리밍 기능을 통한 실시간 기능을 부착할 예정이라고 합니다.
클로바노트는 모노레포를 사용합니다. 팀 내에서 간단하게 PoC하는 프로젝트도 모노레포에서 추가되고 지워지고 있다고 합니다.
모노레포의 장점은 명확합니다. 반복적인 세팅, 코드 중복 최소화, 효율적인 의존성 관리의 장점이 있고 팀과의 협업을 유연하게 만들어줍니다.
클로바노트팀에서는 yarn에 기본 내장되어 있는 workspace 기능을 사용하고 있고 기본 기능만으로 격리 및 공유하는 환경을 만드는데 충분했다고 합니다.
모노레포 위에서 개발을 하다보면 항상 작은 단위로의 분리와 공통된 부분의 재사용성에 대해서 고민하게 됩니다. 이런 반복되는 로직들을 shared에 다 넣기보다는 Atomic Design Pattern을 적용해서 컴포넌트들을 분류할 수 있습니다.
Atomic Desing Pattern에서 Atom이라함은 순수함수처럼 외부 요소에 영향 받지 않고 props에 의해서만 컴포넌트의 모양이 결정되어야 하며 더이상 분해되지 않는 것으로 취급됩니다.
이때 Atom을 정말 쪼개지지 않을때까지 쪼개는 것은 무의미합니다. 예를들어서 테마나 다국어화등에서 Atom에서 Context를 사용할 때가 그렇습니다.
따라서 클로바노트팀에서는 Atom의 정의를 더 이상 분해되지 않는 것이아닌 UI 라이브러리에서 제공하는 수준을 Atom이라고 살짝 변형해서 적용하기로 합니다.
또한 Atomic Pattern은 UI에만 집중되어 있기 때문에 UI가 없이 로직만 들어가 있는 컴포넌트를 분류할 때는 businesses 폴더를 추가로 도입하여 사용하고 있습니다.(예시 코드는 테마 코드)
이 패턴을 적용했을 때 컴포넌트가 어떤 폴더에 들어가야할지 고민해야할 수도 있지만 팀원 대부분이 가리키는 폴더가 거의 동일했다고 합니다.
아토믹 디자인 패턴은 완벽한 해결방법이라기보다는 팀에 맞는 규칙을 만들 때 시작 가이드가 될 수 있다.
리코일은 리액트의 동시성을 위한 렌더링 스케줄러를 그대로 이용할 수 있는 상태 관리 라이브러리이기 때문에 채택했다고 합니다.
Recoil은 Redux의 Store가 여러개 있는거라고 생각하면 좋습니다. 분리된 상태들이 각각 개별적으로 동작하면서 동시성을 만들어 낼 수 있습니다.
Recoil에서 문제는 atom값이 업데이트되면 참조하고 있는 selector도 업데이트되는 방식이기 때문에 여러 기능들이 추가되고 서로가 서로를 참조하게 되면 예상치 못한 Side Effect가 발생할 수 있다는 점입니다.
따라서 atom과 selector들을 도메인별 훅으로 묶어줍니다. 훅에서 리턴되는것은 읽기 전용 값과 그 값을 변경하기 위한 callback 함수들뿐입니다. Recoil 함수를 직접 사용하지 않고 hook을 통해서만 접근하도록 격리시킵니다.
도메인별 독립된 훅만을 이용해서 Recoil에 접근하기 때문에 서로가 서로를 참조하는 것을 염려하지 않아도 됩니다.
만약 Recoil 훅이 순차적으로 실행되어야한다면(Redux 처럼) 비즈니스 컴포넌트가 그 역할을 대신합니다. 문제가 생겨도 비즈니스 컴포넌트로 묶여있기 때문에 문제를 파악하기 쉽습니다.
오프라인 대응을 하기 위한 고민, 사용자를 위한 반응형 UI를 넘어선 반응형 UX, ZOOM 실시간 기능에 대한 설명, 모노레포 속에서 자연스러운 아토믹 디자인 패턴의 선택 그리고 상태 관리까지.
클로바노트라는 하나의 서비스를 만들기 위한 네이버 클로바노트팀이 한 노력을 볼 수 있는 멋있는 발표였다고 생각합니다.