음악 재생 앱 개발 뒷이야기

sunclock·2022년 3월 3일
0

프론트엔드

목록 보기
7/8
post-thumbnail

pickcl

깃허브 링크
로컬 스토리지에 있는 오디오 파일을 재생하는 앱이다.
React Native 기반이고, 저작권 문제로 인터넷 연결은 전혀 포함하지 않은 Client-only 앱이다.

좋았던 부분을 픽!하고 클릭!해서 듣는다는 의미로 서비스 이름은 피클(pickcl)로 정했다.

Android 테스터 상시 모집중

Android 1.0.1 테스트 참여 링크
iOS 테스트는 추후 공개 예정입니다.

배경

평소 CD로 된 오디오북을 자주 듣는데, 마땅한 앱이 없어 불편한 점이 많았다.
무엇보다 책 한 권은 서너시간 분량을 족히 넘어서, 꼭 북마크 기능이 있었으면 했다.
그래서 프로그래밍 연습도 할 겸 직접 만들기로 했다.

개발 기간

100시간 정도 걸린 것 같다.

  • 2021년 10월 퇴근 후 3~4시간씩 2주 정도
  • 2022년 2월 27, 28, 3월 2, 3 4일간 하루 10시간 정도

작년 10월에는 JS로 개발을 하다가 bookmark를 구현하다가 멈췄고,
올해 2월에 이전 코드를 참고해 프로젝트를 다시 만들고 TS로 개발했다.

기능

  • 재생 - 로컬 스토리지의 오디오 파일을 재생하기
  • 북마크 - 오디오 파일의 특정 타임스탬프에 대한 북마크 작성하기
  • 리스트 - 재생 목록과 북마크를 리스트로 조회하기
  • 바로가기 - 북마크를 클릭하면 해당 오디오 파일의 해당 부분을 재생하기

어려웠던 점

로컬 파일 경로 구하기

앱의 요구사항은 로컬 환경에서 인터넷 연결 없이 음악을 재생하는 것이었다.
안드로이드 기기의 경우, react-native-document-picker를 이용해 파일을 읽어오면content://로 시작하며 암호화된 uri를 리턴한다.
그런데 이 주소를 그대로 이용하면 파일 경로를 읽어오질 못해서 재생이 안 됐다.

인터넷이나 공식 문서의 사례는 url밖에 없어서, content uri를 재생 가능하게 조작하는 방법을 찾느라 며칠을 보냈다.
파일의 절대 경로를 찾으려고 react-native-fs나 rn-fetch-blob과 같은 여러 라이브러리를 직접 사용해보고 나서야 내게 필요한 방법을 찾았다.

어떻게? 이렇게..

import { getAllExternalFilesDirs } from 'react-native-fs';
import DocumentPicker from 'react-native-document-picker';

getAllExternalFilesDirs().then(dirs => {
    if (Platform.OS === 'android') {
        let rootPath = dirs[0].split('Android')[0]
        setRoot(rootPath);
    } 
});

const res = await DocumentPicker.pickMultiple({
            type: [DocumentPicker.types.audio],
        });

let pathname = decodeURI(file.uri.split('%3A')[1]);
            pathname = pathname.replace(/%2F/gi, "/");
            newFile.url = "file://" + root + pathname;

Promise 객체 다루기

음악 재생 라이브러리, 도큐먼트 피커 라이브러리 모두 프로미스 객체를 사용했다.
프로미스는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체다.
처음에는 프로미스를 resolve하지 않고 사용하려다가 UI render error, 기대한 값이 나오지 않는 에러를 여럿 겪었다.

프로미스의 특징은 다음과 같다.

  • 순서 보장 - 현재 처리중인 콜 스택을 완료하기 전까지는 콜백 함수를 절대 호출하지 않는다.
  • 연쇄(Chaning) - then()을 이용해 여러 번의 콜백을 추가할 수 있고, 각각의 콜백은 주어진 순서대로 실행된다.
    출처: MDN Using promises

Async, Await?

ECMAScript 2017부터 비동기 코드와 Promise를 더욱 사용하기 쉽게 만드는 Async, Await 키워드가 추가되었다.

  • async는 await 키워드가 비동기 코드를 호출할 수 있게 해주는 키워드다.
  • async를 함수와 같이 사용하면 결과를 직접 반환하는 게 아니라 Promise를 반환하게 한다.
  • await 키워드는 Promise가 fulfill될 때까지 실행을 잠시 중단하고, 결과를 반환한다. 이때 실행을 기다리는 다른 코드들은 중지되지 않고 그대로 실행된다.
    출처: MDN async와 await를 사용하여 비동기 프로그래밍을 쉽게 만들기

Event(Player)와 UI 동기화

1번 곡을 재생중인데, 다음 곡 재생 아이콘을 5번 빠르게 연타했다고 가정하자.
오디오 플레이어는 정상적으로 6번 트랙을 재생하고 있다.
그런데 내가 만든 컴포넌트는 여전히 1번 곡 화면을 보여주고 있어서 문제였다.

track의 상태관리가 정말 큰 이슈였다!
플레이어 모듈의 track 객체를 그대로 끌어다 쓰면 좋겠지만...
우리 프로젝트의 track 객체에 더 많은 필드가 들어가야 해서 동일한 객체를 사용할 수 없었다. 그래서 문제가 조금 복잡해졌다.

  • route.params로 현재 재생중인 track을 줘볼까?
  • useState()의 setState()로 현재 재생중인 track을 제어해볼까?
  • useSelector()로 track을 useDispatch()를 사용해 제어해볼까?

해결책은 3번이었다.

  • 1번의 경우, track 스크린 내에서 다음 곡 재생 버튼을 누르면 route가 변경되지 않기 때문에 사용할 수 없다
  • 2번의 경우, setState()가 너무 많이 호출되면 ui render error가 뜨고, 터치를 하면 이벤트가 처리되기 까지의 시간이 너무 오래 걸려서 사용자 경험을 저해한다.
  • 3번의 경우, redux를 이용하면 속도를 손해보지 않고 ui와 이벤트를 거의 동시에 처리할 수 있었다.

undefined, null, 예외 처리!!!

다음 곡, 이전 곡으로 넘기다보면 index out of boundary 에러가 참 많이 발생한다.
처음에는 공식 문서에 직접 언급된 내용이 없어서 이걸 어쩌지 고민이 많았다.
하는 수 없이 TrackPlayer 라이브러리의 동작을 콘솔로 찍다보니, track의 index를 tracklist의 length만큼 modulo 연산을 하고 있다는 사실을 발견했다.
마치 처음과 끝이 이어진 circlular linked list처럼 말이다!
그래서 나도 changeTrack()이라는 리덕스 액션 함수를 구현하고 modulo 연산을 사용했다.

dispatch(changeTrack(tracks[track % tracks.length]));

좋았던 점

세.넓.라.많.

세상은 넓고 좋은 라이브러리는 많구나!
native-base라는 ui component를 처음으로 사용해봤다.
전체적으로 UI에 일관성이 생기니 단순한 디자인으로도 화면이 가득차고 깔끔해 보였다.

오픈 소스에 감사하기

이렇게 작은 앱을 완성하기 위해서 사용한 라이브러리가 꽤 된다.

  • react-native라는 프레임워크부터 시작해서,
  • 오디오 재생을 도와준 react-native-track-player,
  • 화면 간 이동에 필요한 react-navigation,
  • 편리한 파일 업로드에 큰 도움이 됐던 react-native-document-picker,
  • 이해하기 쉬운 아이콘을 제공해 준 react-native-vector-icons,
  • 상태 관리를 편리하게 해준 redux, react-redux, redux-dev-tools-extension, redux-thunk,
  • 일관성 있는 UI 컴포넌트를 제공해준 native-base
  • 그리고 수많은 의존성 라이브러리들까지

수많은 사람들의 손을 거쳐간 코드 덕분에 편리하게 원하는 앱을 만들 수 있어서 감사했다.

나도 다른 사람에게 도움이 되는 오픈 소스 기여를 꼭 해보고 싶다.
또, 큰 규모의 팀에서도 협업하기 쉽고 에러가 잘 나지 않는 코드를 작성하는 개발자가 되고 싶다는 생각도 들었다.

TypeScript

타입스크립트를 한 번도 안 써본 사람은 있어도, 한 번만 써본 사람은 없다...

내 첫 TS 경험은 작년 9월로 거슬러 올라간다.
회사에서 TS로 작성된 gatsby 웹사이트를 관리하는 역할을 맡아서 TS를 처음 써봤다.
처음에는 JS였다면 코드 한 줄 수정이면 될 것을 파일 여러 개를 고쳐야 해서 귀찮았다.

한 줄로 짤 코드를 누가 열 줄로 만들어놨어요? 에러 찾기 힘들게. (잠시 뒤) 코드가 길어서 프로그램만 무거워졌잖습니까? 이러니까 서비스가 버벅대지.

😅
4개월간 같은 코드를 관리하다보니 TS 덕분에 기술 부채를 사전에 예방할 수 있어 좋았다.
개인 프로젝트에서도 TS를 사용해야겠다는 생각을 그때 처음 했다.

실제로 사용해 본 소감은 이렇다.

  • JS를 사용할 때에 비해 자료형이나 nullable과 관련해 생기는 오류가 1/4로 줄어들었다.
  • 미리 type을 선언해둔 덕분에 모듈간의 interface를 하나하나 기억하지 않아도 돼서 편리했다.
  • type을 이랬다 저랬다 고치는 게 일이긴 하지만, 생산성 향상이 엄청나서 사용할 가치가 충분하다!

보완 사항

firebase 추가

현재 AsyncStorage를 이용해 북마크와 트랙 정보를 저장하고 있다.
그런데 AsyncStorage는 외부로 유출될 위험이 있고, 데이터 복원이 불가능하다는 치명적인 단점이 있다.
그래서 firebase로 유저 관리, 북마크 관리 기능을 추가할 계획이 있다.

편의 기능 추가

앞으로 내가 직접 사용할 앱이기 때문에 편의를 위한 기능을 더 개발해야 한다.

소감

드디어 처음으로 혼자서 앱 하나를 완성했다.
글로만 읽는 것보다 직접 코드를 짜면서 더욱 많은 배움을 얻을 수 있었다.
코딩하며 보낸 밤마다 내게 힘을 준 말로 후기를 끝맺음 짓고자 한다.

성공한 사람은 끝까지 포기하지 않은 사람이다!

profile
안녕하세요.

0개의 댓글