Tinder의 프로그래시브 웹 앱 성능 케이스 스터디

원글: https://medium.com/@addyosmani/a-tinder-progressive-web-app-performance-case-study-78919d98ece0

Tinder는 최근 웹 앱을 출시했다. (swiped right: 아이폰의 밀어서 잠금 해제와, Tinder 앱에서 우측으로 밀어 마음에 드는 상대를 확인하는 것을 연상시키는 여러 가지 의도를 포함했다). Tinder Online은 새로운 반응형 프로그래시브 웹 앱(Progressive Web App)으로, 데스크탑과 모바일의 모든 사용자는 자바스크립트 성능 최적화, 네트워크 회복성을 위한 서비스 워커(Service Worker) 그리고 채팅 참여를 도울 푸시 알림(Push Notifications)이 주는 이점을 누리게 되었다. 오늘은 Tinder가 웹 성능에 대해 배운 것들 알아보자.

프로그래시브 웹 앱으로의 여정

Tinder Online은 새로운 시장(웹)에 진출하기 위해 시작되었고, 다른 플랫폼의 Tinder V1과 같은 수준의 사용자 경험을 전달하려 노력했다.

UI를 React로, Redux 를 상태 관리로, 사용하는데 필요한 최소 기능(MVP)을 구현하는 데 3개월이 걸렸다. 데이터가 비싸거나 부족한 사람들이 10%의 데이터만으로 Tinder의 주요 기능을 사용할 수 있는 PWA가 이러한 노력으로 만들어졌다.

Tinder Online vs 네이티브 앱 데이터 소모 비교. 이것은 동등하게 비교할 수 있는 것이 아님을 말해두겠다. PWA는 새로운 경로가 요청될 때마다 드를 로드하며, 데이터 소모는 애플리케이션의 라이프사이클에 따라 데이터는 분할상환으로 증가된다. 그러나 이어지는 경로 전환에도 여전히 app 다운로드만큼 데이터 소모가 일어나지는 않는다.

상대를 선택하고 메세지를 보낸 횟수, 서비스에 머무른 시간만 봐도 PWA가 네이티브 앱보다 나아졌다는 것을 알 수 있다.

  • 사용자는 네이티브 앱보다 웹에서 이성을 더 많이 선택한다.
  • 사용자는 네이티브 앱보다 웹에서 메시지를 더 많이 보낸다.
  • 사용자는 네이티브 앱에서만큼 웹에서도 구매한다.
  • 사용자는 네이티브 앱보다 웹에서 프로파일을 더 여러 번 수정한다.
  • 네이티브 앱보다 웹의 세션 타임이 더 길다. (사용자가 더 오래 머문다.)

성능

Tinder Online의 모바일 사용자들은 보통 다음과 같은 기기를 사용하고 있다.

  • Apple iPhone & iPad
  • Samsung Galaxy S8
  • Samsung Galaxy S7
  • Motorola Moto G4

사용자 대부분이 4G로 접속한다는 것을 Chrome User Experience report (CrUX)를 사용해 알아냈다.

참고: Rick Viscomi는 최근 PerfPlanet 에선 CrUX를 다뤘고, 유명한 100만 개 사이트의 CrUX를 시각화한 rUXt를 인도 출신 Parameshwaran가 소개했다.

WebPageTest와 Lighthouse (Galaxy S7 / 4G 설정으로)로 테스트한 결과, Consistently Interactive(버튼을 누른다거나, 내용을 갱신하는 등 사용자 입력을 부드럽게 처리할 수 있게 된 시점)가 5초 이내였다.

물론 아래에서 보듯 CPU가 더욱 제한적인 평범한 핸드폰(Moto G4 같은)에서는 이를 향상할 수 있는 여지가 많이 남아있다.

Tinder는 최적화를 열심히 하고 있기 때문에, 웹 성능 개선 소식을 곧 들을 수 있길 기대하고 있다.

성능 최적화

Tinder는 Page Load, Consistently Interactive를 개선하는데 다양한 기술을 사용했다. Tinder는 경로 기반 코드 분할(route-based code-splitting)을 구현, 성능 예산(performance budgets)과 장기 에셋 캐싱(long-term asset caching)을 도입했다.

경로 기반 코드 분할

Tinder는 초기에 큰 한 덩어리의 자바스크립트 번들을 배포했고, 이는 Consistently Interactive 시간을 늦추는 결과를 가져왔다. 이 번들들은 핵심 사용자 경험에 즉시 필요치 않은 코드들을 포함하고 있었기 때문에 코드 분할(code-splitting)로 쪼갤 수 있었다. 일반적으로 사용자에게 먼저 필요한 코드만 담고, 나머지는 필요에 따라 지연 로딩(lazy-load)하는 방식은 유용하다.

Tinder는 코드 분할과 지연 로딩을 위해 React Router 그리고 React Loadable을 활용했다. 모든 경로와 렌더링 정보를 모아서 설정으로 만들었고, 이는 최상위 레벨에서 코드 분할을 간단히 구현할 방법이 되었다.

요약하면

React Loadable은 James Kyle이 컴포넌트 중심으로 React에서 코드 분할을 쉽게 할 수 있도록 만든 작은 라이브러리이다. Loadable은 higher-order 컴포넌트(컴포넌트를 만드는 함수)로, 이것으로 컴포넌트 레벨에서 번들을 쉽게 분할할 수 있다.

“A”, “B” 두 개의 컴포넌트가 있다고 하자. 코드 분할 이전, 메인 번들은 A, B 그리고 기타의 것들까지 모두 가지고 있었다. A, B 모두 당장에 필요치는 않기 때문에 이는 비효율적인 방법이다.

코드 분할을 한 이후, A, B 컴포넌트는 필요할 때 로드된다. Tinder는 이를 위해 React Loadable, dynamic import(), 그리고 웹팩의 magic comment 문법 (분할된 조각에 이름을 부여하기 위해)을 아래처럼 설정했다.

공통으로 여러 경로에서 사용되는 “vendor” 라이브러리(외부에서 가져온) 조각은 오랫동안 캐시될 수 있도록, 웹팩의 CommonsChunkPlugin를 사용하여 하나로 만들었다.

그리고, 다음 페이지에서 쓰일 수 있는 리소스를 미리 로드하기 위해 컨트롤 컴포넌트에서 React Loadable의 preload 지원을 사용했다.

또한, Service Workers를 사용해서 경로에 해당하는 모든 번들을 미리 캐시 했고, 사용자들이 확실히 방문하게 되는 경로는 코드 분할 없이 메인 번들에 담았다. 물론 UglifyJS를 이용한 자바스크립트 압축 같은 보편적인 최적화 방법들 역시 활용했다.

new webpack.optimize.UglifyJsPlugin({
      parallel: true,
      compress: {
        warnings: false,
        screw_ie8: true
      },
      sourceMap: SHOULD_SOURCEMAP
    }),

효과

경로 기반 코드 분할을 도입하여 메인 번들 크기가 166KB에서 101KB로 줄었고, DCL(Dom Content Loaded)이 5.46s에서 4.69s로 좋아졌다.

장기 에셋 캐싱(Long-term asset caching)

[chunkhash]를 이용하여 각 파일에 cache-buster를 추가하면, 웹팩이 만드는 정적 리소스의 장기 에셋 캐싱을 더 확실히 할 수 있다.

Tinder는 많은 오픈소스(vendor) 라이브러리를 사용하고있다. 이 라이브러리들이 변경되면 원래의 [chunkhash]가 변경되고 캐시를 새로 해야한다. 이를 해결하여 캐싱을 개선 하기 위해 외부 라이브러리 화이트리스트를 정의하고 메인 번들에서 잘라냈다. 두 개의 번들 크기는 대략 160KB가 되었다.

나중에 쓰일 리소스 미리 로딩(preload)

복습하자면 <link rel=preload>는 브라우저가 꼭 필요하지만 지금 당장 사용하지 않을 리소스를 미리 로드하게 해주는 서술형 명령(declarative instruction) 이다. single-page application에서 미리 로드할 이 리소스는 종종 자바스크립트 번들이 되기도 한다.

Tinder는 핵심 경험에 중요한 자바스크립트/웹팩 번들을 미리 로드하도록 했다. 이 작업은 로드에 걸리는 시간을 1초 줄였고, first paint를 1000ms에서 500ms 정도로 줄여주었다.

성능 예산(Performance budgets)

Tinder는 성능 목표 달성을 위해 성능 예산을 도입하였다. Alex Russell이 “Can you afford it?: real-world performance budgets”에서 언급했듯, 느린 3G 연결을 가진 성능이 낮은 모바일 하드웨어를 고려하면 예산은 한정적이다.

Tinder는 first interactive, consistently interactive를 빠르게 하려고 메인 번들과 vendor 번들을 155KB 이하, 비동기적으로 지연 로드되는(lazy load) 번들들은 55KB 이하, 그 외의 번들들은 35KB 이하, CSS는 20KB로 제한했다.

웹팩 번들 분석

Webpack Bundle Analyzer를 사용하면 자바스크립트 번들의 의존성 그래프(dependency graph)를 살펴보고, 그중에 쉽게 최적화 가능한 것이 있는지 찾을 수 있다

Tinder는 최적화 가능한 영역을 찾기 위해 Webpack Bundle Analyzer를 다음과 같이 사용한다.

  • Polyfills: 최신 브라우저뿐만 아니라 IE11, 안드로이드 4.4까지도 지원하고 있다. Polyfill과 변환된 코드를 최소화하기 위해, babel-preset-env와 core-js를 사용한다.
  • 라이브러리 다이어트localForage 사용을 IndexedDB를 직접 사용하는 것으로 대체했다.
  • 코드 분할: first paint, first interactive에 필요하지 않은 컴포넌트들을 메인 번들에서 잘라냈다.
  • 코드 재사용: 세 군데 이상에서 쓰이는 코드 조각을 추상화하여 비동기 공통 번들을 만들었다.
  • CSS: 핵심 번들에서 중요 CSS 분리. (서버 사이드 렌더링으로 처리해서 제공)

번들을 분석하는 일은 웹팩의 Lodash Module Replacement Plugin을 활용케 했다. 이 플러그인은 feature set을 동일한, 비슷한, 혹은 아무 동작 하지 않는 것(noop)으로 바꾸어 Lodash 빌드를 작게 만든다.

웹팩에 Webpack Bundle Analyzer를 설정할 수 있으며, Tinder의 설정은 아래와 같다.

plugins: [
      new BundleAnalyzerPlugin({
        analyzerMode: 'server',
        analyzerPort: 8888,
        reportFilename: 'report.html',
        openAnalyzer: true,
        generateStatsFile: false,
        statsFilename: 'stats.json',
        statsOptions: null
      })

메인 번들에 남아있는 대부분의 자바스크립트는 Redux Reducer, Saga Register 구조를 변경하지 않고는 잘라내기 힘든 것들이다.

CSS 전략

Tinder는 재사용성 높은 CSS를 만들기 위해 Atomic CSS를 사용하고 있다. 이 atomic CSS는 모두 초기 페인트 때 인라인 되고, 나머지 몇몇은 스타일시트(애니메이션이나 base/reset 스타일을 포함한)로 로드된다. 중요 스타일은 압축하여 최대 20KB 크기이고, 최근 에는 11KB 이하로 줄었다.

Tinder는 어떠한 변경 점이 생겼는지 추적하기 위해 매번 배포 때마다 CSS stats와 Google Analytics를 사용한다. Atomic CSS를 사용하기 전 평균 페이지 로드 시간은 6.75초 이하였고, 이후에는 5.75초로 떨어졌다.

Tinder는 또한 Can I Use를 기초로 vendor prefix를 자동으로 붙여주는 PostCSS Autoprefixer plugin도 사용한다. 설정은 아래를 참고.

new webpack.LoaderOptionsPlugin({
    options: {
    context: paths.basePath,
    output: { path: './' },
    minimize: true,
    postcss: [
        autoprefixer({
        browsers: [
            'last 2 versions',
            'not ie < 11',
            'Safari >= 8'
        ]
        })
      ]
    }
}),

실시간 성능

매우 중요한 일이 아닌 것은 requestIdleCallback() 으로 미루기

정말 중요한 것이 아닌 것들을 idle time으로 미루어 실시간 성능을 높이기 위해 requestIdleCallback()을 사용한다.

requestIdleCallback(myNonEssentialWork);

여기에는 계기 신호등 같은 일이 포함된다. 또한 밀어서 잠금 해제(swiping)하는 도중 paint count를 줄이기 위해 HTML composite layer들을 간단하게 했다. 

밀어서 잠금 해제 하는 동안 requestIdleCallback()을 계기 신호등에 사용했더니

사용 이전에는..

사용 이후에는..

의존성 업데이트


이전 버전의 웹팩은 모듈을 번들링할 때 개별적인 함수 클로저(closure)로 감쌌었다. 감싸고 있는 함수들은 브라우저에서 실행을 느리게 만들었다. 웹팩 3는 “scope hoisting” 기능을 가지고 있는데, 이것은 모든 모듈을 하나의 클로저에 묶어 브라우저에서 더 빠르게 실행될 수 있도록 한다. 아래처럼 Module Concatenation plugin을 사용하면 된다.

new webpack.optimize.ModuleConcatenationPlugin()

웹팩 3의 scope hoisting은 Tinder의 초기 vendor 번들 자바스크립트 파싱 시간을 8% 줄여주었다.

리엑트 16

리엑트 16은 이전 버전과 비교하면 리엑트 번들 크기 감소를 축소 해주었다. 롤업(Rollup)을 사용해 패키징하고, 사용하지 않는 코드를 삭제하여 이러한 향상을 가져왔다.

Tinder는 리엑트 15에서 리엑트 16으로 업데이트 하면서 압축(gzipped)한 vendor 번들 크기를 7% 줄였다.

react + react-dom의 압축 크기는 ~50KB에서 ~35KB로 줄었다. 리엑트 16 번들 크기를 줄이는 데 중요한 역할을 한 Dan AbramovDominic Gannaway 그리고 Nate Hunzaker에게 감사한다.

네트워크 탄력(resilience)과 오프라인 에셋 캐싱을 위한 Workbox

메인, vendor, manifest 번들과 CSS 같은 주요 정적 에셋과 Application Shell을 캐시 하기 위해 Workbox Webpack plugin을 사용했다. 이것으로 재방문하는 사용자들에게 네트워크 탄력성과 더 빠른 웹 앱을 제공할 수 있게 되었다.

기회

또 다른 bundle analysis tool인 source-map-explorer를 사용하여 Tinder의 번들을 파보면 아직도 번들 크기를 줄일 수 있는 여지가 있다. 로그인 이전에 페이스북 사진, 알림, 메시지, 캡챠(captcha)가 로드되는데, 이것을 주요 경로(critical path)에서 제거하면 메인 번들 크기를 20%까지 줄일 수 있다.

200KB 크기의 페이스북 SDK 스크립트가 주요 경로에 포함되어있다. 이 스크립트를 제거하면(나중에 필요할 때 지연 로드 하면 된다), 초기 로딩 시간을 1초 깎아낼 수 있다.

결론

Tinder는 여전히 Progressive Web App 작업을 진행 중이지만, 이 작업으로 벌써 결실을 보기 시작했다. Tinder.com을 계속 지켜보며 진전 사항을 확인하자.

Tinder Online을 런칭하고 이 글을 작성하는 재료가 된 Roderick Hsiao, Jordan Banafsheha, 그리고 Erik Hellenbrand에게 감사와 축하를 전한다. 이 글을 리뷰해준 Cheney Tsai 에게도 감사를 전한다.

더 읽을거리

이 글은 Performance Planet와 함께 교차 게재 했다. 만약 당신이 리엑트 초보자라면 초보자를 위한 리엑트를 추천하겠다.

KyuWoo Choi
FrontEnd Developer
Frontend Developer Skilled in OpenSource and JavaScript. Experienced in Web, Android, Mobile Game development with Java, C# languages as well. Please check my current works on GitHub.

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.