깃허브 링크
로컬 스토리지에 있는 오디오 파일을 재생하는 앱이다.
React Native 기반이고, 저작권 문제로 인터넷 연결은 전혀 포함하지 않은 Client-only 앱이다.
좋았던 부분을 픽!하고 클릭!해서 듣는다는 의미로 서비스 이름은 피클(pickcl)로 정했다.
Android 1.0.1 테스트 참여 링크
iOS 테스트는 추후 공개 예정입니다.
평소 CD로 된 오디오북을 자주 듣는데, 마땅한 앱이 없어 불편한 점이 많았다.
무엇보다 책 한 권은 서너시간 분량을 족히 넘어서, 꼭 북마크 기능이 있었으면 했다.
그래서 프로그래밍 연습도 할 겸 직접 만들기로 했다.
100시간 정도 걸린 것 같다.
작년 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;
음악 재생 라이브러리, 도큐먼트 피커 라이브러리 모두 프로미스 객체를 사용했다.
프로미스는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체다.
처음에는 프로미스를 resolve하지 않고 사용하려다가 UI render error, 기대한 값이 나오지 않는 에러를 여럿 겪었다.
프로미스의 특징은 다음과 같다.
- 순서 보장 - 현재 처리중인 콜 스택을 완료하기 전까지는 콜백 함수를 절대 호출하지 않는다.
- 연쇄(Chaning) - then()을 이용해 여러 번의 콜백을 추가할 수 있고, 각각의 콜백은 주어진 순서대로 실행된다.
출처: MDN Using promises
ECMAScript 2017부터 비동기 코드와 Promise를 더욱 사용하기 쉽게 만드는 Async, Await
키워드가 추가되었다.
- async는 await 키워드가 비동기 코드를 호출할 수 있게 해주는 키워드다.
- async를 함수와 같이 사용하면 결과를 직접 반환하는 게 아니라 Promise를 반환하게 한다.
- await 키워드는 Promise가 fulfill될 때까지 실행을 잠시 중단하고, 결과를 반환한다. 이때 실행을 기다리는 다른 코드들은 중지되지 않고 그대로 실행된다.
출처: MDN async와 await를 사용하여 비동기 프로그래밍을 쉽게 만들기
1번 곡을 재생중인데, 다음 곡 재생 아이콘을 5번 빠르게 연타했다고 가정하자.
오디오 플레이어는 정상적으로 6번 트랙을 재생하고 있다.
그런데 내가 만든 컴포넌트는 여전히 1번 곡 화면을 보여주고 있어서 문제였다.
track의 상태관리가 정말 큰 이슈였다!
플레이어 모듈의 track 객체를 그대로 끌어다 쓰면 좋겠지만...
우리 프로젝트의 track 객체에 더 많은 필드가 들어가야 해서 동일한 객체를 사용할 수 없었다. 그래서 문제가 조금 복잡해졌다.
해결책은 3번이었다.
다음 곡, 이전 곡으로 넘기다보면 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에 일관성이 생기니 단순한 디자인으로도 화면이 가득차고 깔끔해 보였다.
이렇게 작은 앱을 완성하기 위해서 사용한 라이브러리가 꽤 된다.
수많은 사람들의 손을 거쳐간 코드 덕분에 편리하게 원하는 앱을 만들 수 있어서 감사했다.
나도 다른 사람에게 도움이 되는 오픈 소스 기여를 꼭 해보고 싶다.
또, 큰 규모의 팀에서도 협업하기 쉽고 에러가 잘 나지 않는 코드를 작성하는 개발자가 되고 싶다는 생각도 들었다.
타입스크립트를 한 번도 안 써본 사람은 있어도, 한 번만 써본 사람은 없다...
내 첫 TS 경험은 작년 9월로 거슬러 올라간다.
회사에서 TS로 작성된 gatsby 웹사이트를 관리하는 역할을 맡아서 TS를 처음 써봤다.
처음에는 JS였다면 코드 한 줄 수정이면 될 것을 파일 여러 개를 고쳐야 해서 귀찮았다.
한 줄로 짤 코드를 누가 열 줄로 만들어놨어요? 에러 찾기 힘들게. (잠시 뒤) 코드가 길어서 프로그램만 무거워졌잖습니까? 이러니까 서비스가 버벅대지.
😅
4개월간 같은 코드를 관리하다보니 TS 덕분에 기술 부채를 사전에 예방할 수 있어 좋았다.
개인 프로젝트에서도 TS를 사용해야겠다는 생각을 그때 처음 했다.
실제로 사용해 본 소감은 이렇다.
현재 AsyncStorage를 이용해 북마크와 트랙 정보를 저장하고 있다.
그런데 AsyncStorage는 외부로 유출될 위험이 있고, 데이터 복원이 불가능하다는 치명적인 단점이 있다.
그래서 firebase로 유저 관리, 북마크 관리 기능을 추가할 계획이 있다.
앞으로 내가 직접 사용할 앱이기 때문에 편의를 위한 기능을 더 개발해야 한다.
드디어 처음으로 혼자서 앱 하나를 완성했다.
글로만 읽는 것보다 직접 코드를 짜면서 더욱 많은 배움을 얻을 수 있었다.
코딩하며 보낸 밤마다 내게 힘을 준 말로 후기를 끝맺음 짓고자 한다.
성공한 사람은 끝까지 포기하지 않은 사람이다!