벨로그에서 다른 분들은 플러터로 어떤 재미난 일을 하고 계신지 궁금해서 둘러보던 중 LOCKED님이 밀리의 서재 뷰어에 대한 글을 쓰신 것을 보았다. Drawer에 대해 글을 쓰셨는데 그것을 보니 예전에 텍스트 뷰어를 만들다가 접었던게 생각이 났고 이번 기회에 다시 한 번 구현에 도전해보았고 이를 공유하고자 한다.
처음에는 단순히 화면의 크기를 구하고 그 화면에 들어가는 글자 갯수를 구한 다음 전체 텍스트에서 그만큼 잘라서 배열에 담아두고 페이지 번호를 통해 읽어서 화면에 뿌리는 방법을 사용했다.
이 방법을 사용하기 위해서는 한 화면에 들어가는 글자 수를 높이를 이용해서 구하는 것이 관건이었다. 시도한 방법들은 다음 4가지이다.
1. RenderBox
final constraints = BoxConstraints(maxWidth: maxWidth);
RichText rt = RichText(
text: content,
strutStyle: StrutStyle(
forceStrutHeight: true,
height: fontSize * n,
),
...
);
final ro = rt.createRenderObject(context);
ro.layout(constraints);
// 높이 = ro.size.height;
주어진 문자열로 RichText 위젯을 만들고 렌더한 후 크기를 구하는 방법이다.
글자마다 높이가 달라서 strutStyle로 모든 글자의 높이를 맞춰주지 않으면 높은 글자가 중간에 있으면 뒷부분이 다음 페이지로 넘어가서 페이지 끝자락이 안 예쁘게 나온다.
수천 자의 글로 테스트할 때는 잘 동작했지만 대략 책 1권 정도인 20만자의 글로 테스트하면 굉~장히 오래 걸린다. 오래 걸려도 괜찮지 않을까 싶을 수 있지만 다트는 단일 스레드를 사용하기 때문에 20만 자에 대한 전체 페이지 높이를 계산하는 동안 화면이 멈춘다. 그래서 로딩도 구현이 불가능한듯 하다.
isolate를 이용해서 해결할 수 있을 것 같았지만 root isolate만이 ui에 접근할 수 있다고 나오며 오류가 났다.
UI actions are only available on root isolate.
그리고 context가 필요해서 불-편
놓아주었다..
2. TextPainter
TextPainter textPainter = TextPainter(
strutStyle: StrutStyle(
forceStrutHeight: true,
height: fontSize * n,
),
...
);
textPainter.text = TextSpan(
text: content,
...
);
textPainter.layout(maxWidth: maxWidth);
// 높이 = textPainter.height;
방법도 성능도 1번과 거의 흡사하다.
좀 더 빠를 것 같았는데 실험해보니 별 차이가 없었다.
느리고 isolate로 해결할 수 없었다.
패쓰~
3. LineMatrics
Flutter Line Metrics
구글링하다가 발견한 아주 좋은 방법이었다. RenderBox와 TextPainter로 높이를 계산하는 방법을 엄청 빠르고 쉽게 할 수 있었다. 속도 개선이 많이 되어서 답을 찾은줄 알았는데 밀리에 비하면 현저히 느렸다.
4. 미리 계산하기
실시간으로 페이지별 텍스트를 변경하니 대기 시간 때문에 사용하기가 너무 별로인 것 같아서 미리 계산을 다 해놓고 불러오기만 하는 방법을 사용해보았다. 그런데 밀리는 폰트도 여러 개를 지원했고 폰트 크기도 여러 개를 지원했다. (폰트 개수 * 폰트 크기 수)의 횟수 만큼 연산해야 해서 시간이 꽤나 오래 걸렸고 또 사용자가 폰트를 추가할 수 있었는데 그렇게 되면 폰트를 추가한 만큼 연산 시간이 증가한다. pc기준 기본 폰트 7개에 크기는 110단계씩 있다.
최소 770번...? 이거도 아니다.
이때 플러터로 만든게 아닐 수 있다는 의구심이 들었다.
직접 사용해보기 위해 밀리의 서재 계정을 생성했다. pc 버전과 모바일 버전을 직접 사용해보니 감이 왔다. pc 버전은 Electron이라는 것으로 만든 것 같았고 모바일 버전은 플러터로 만든 것 같았는데 성능이나 기능은 거의 동일 했다.
두 가지를 따로 만들지는 않았을 것 같아서 밀리에서 어떤 사람들을 뽑고 있나 봤는데 재밌는 것을 발견했다. 밀리의 서재 프론트엔드 개발자 채용 공고에 주요업무가 Epub 웹 뷰어 개발이라 적혀있는 것이었다. Epub 웹 뷰어를 만들고 플러터 웹뷰로 띄우면 된다!
Epub.js라는 Epub 웹 뷰어 라이브러리가 이미 있었다.
깃허브랑 문서를 보고 사용법을 익히고 페이지를 구성했다.
문서 내용이 너무 부실해서 깃허브 이슈 답변을 보고 많이 배웠다.
방법을 찾고 나니 구현은 별 거 없었다.
그냥 플러터 assets에 html과 js 등을 넣고 웹뷰로 열면 된다.
webview_flutter를 사용했다.
주의할 점은 javascriptMode를 unrestircted로 설정해줘야 한다는 것이다.
기본값이 disabled라서 자바스크립트 실행이 안된다.
final Completer<WebViewController> _controller =
Completer<WebViewController>();
...
body: WebView(
initialUrl: 'about:blank',
javascriptMode: JavascriptMode.unrestricted, // 꼮!!꼮!
onWebViewCreated: (WebViewController controller) async {
_controller.complete(controller);
(await _controller.future)
.loadFlutterAsset('assets/reader/index.html');
},
),
...
웹뷰에 있는 웹의 자바스크립트를 제어하려면 이런 식으로 하면 된다.
(await _controller.future).runJavascript(자바스크립트 코드);
Completer 문법은 처음 써봤는데 신기했다.
이상한 나라의 앨리스 영문판(약 52000자)로 테스트한 결과이다.
너무 잘 된다!







끝인줄 알고 정리하려 했는데 쎄한 느낌이 들어서 뭔가 놓친게 없나 생각해보니 영어로 된 Epub 파일 밖에 못 구해서 한글도 잘 되는지 테스트를 하지 않았던 것이다. 그래서 Epub 파일을 수정할 수 있는 프로그램을 받아서 한글을 넣고 테스트 해보니 폰트 변경이 안된다. 웹에서는 한글도 잘 됐는데 왜 이런지 잘 모르겠다. 별 짓을 다 해봐도 안된다.
Epub.js가 페이지를 원래 책의 페이지로만 나타내서 글꼴을 바꿔서 새로 생성되는 페이지들의 페이지 번호를 알아낼 수가 없다. 계산하는 방법을 찾아야 할 듯 하다.
평소 같았으면 승부욕이 발동해서 계속 매달렸을테지만 지금 엄청난 생각이 떠올라서 그 생각을 실현해야 해서 일단 넘어가기로 했다. 그래도 예전에 못 만들었던 것을 만들어서 좋다. 재밌었따
제 경험상 대용량의 텍스트를 페이지 분할 할 때 분할된 텍스트의 배열을 직접 저장하는 것은 효율이 지나치게 떨어져서 분할할 index의 배열을 저장하는 쪽이 효과적이었습니다.
https://pub.dev/packages/text_pagination
이와 관련해서 라이브러리도 만든 적 있었는데 손놓고 있던 사이에 플러터 버전이 올라가서 호환이 안되네요