프렌즈타임 웹앱 삽질기


안녕하세요, 카카오게임즈에서 프렌즈타임 클라이언트를 개발하고 있는 mark입니다.
지난 11월 13일 진행된 kakao FE meetup에서 ‘프렌즈타임 웹앱 삽질기’ 라는 주제로 발표한 내용을 옮긴 글입니다.

주니어 개발자로 팀에 합류하여, 1년간 프렌즈타임 이라는 웹앱을 개발하면서 경험한 이야기들을 간단하게 소개해 보려고 합니다.

프렌즈타임?

아시는 분들도 계시겠지만, 우선 프렌즈타임이 무슨 서비스인지 소개해 드려야겠네요.

프렌즈타임은 정해진 시간에 사용자들이 실시간으로 모여서 가위바위보를 진행하여, 우승자에게 상금을 지급하는 퀴즈쇼 형태의 서비스입니다. (현재는 매주 목요일 12시에 진행되고 있습니다. ?)
친근한 프렌즈 케릭터와 누구나 알고 있는 간단한 룰의 가위바위보를 섞어 쉽고 가볍게 접근할 수 있는 점이 특징입니다.

2019년 11월일 진행된 라이브 화면

프렌즈타임의 또 하나의 특징을 말씀드리자면, 웹앱으로 만들어졌다는 점입니다.
별도의 설치 없이, 링크만으로 스마트폰에서 바로 실행이 가능합니다.

다만 카카오톡 안에서만 실행될 수 있도록 제한이 되어있기 때문에, 카카오톡은 설치가 되어있어야 합니다.
아래 QR코드를 스캔해 보시면 프렌즈타임에 접속해 보실 수 있습니다.

웹앱으로 개발하기 까지

프렌즈타임이 처음부터 웹앱으로 기획된 건 아니었습니다.
처음으로 고려된 방법은 카카오톡의 게임탭 안에 네이티브로 구현하는 방법이었는데요, 그 이유로는 크게 아래의 두 가지가 있었습니다.

  1. 카카오톡의 게임별 탭을 담당하던 팀이다.당시 저희 팀은 게임별 탭을 관리하던 팀으로, 네이티브 개발자들만 있었기 때문에 당연히 네이티브가 1순위 고려대상이었습니다.
  2. 카카오톡 안의 탭이라서 별도의 앱 설치가 필요 없다.유저가 오랫동안 머무르지 않는 형태의 서비스기 때문에 별도의 어플리케이션을 만들어 새로 설치하는 것보다는, 많은 사용자들이 이미 설치하고 있는 카카오톡을 통해 간편하게 실행시키는 것이 좋다고 판단했습니다.
카카오톡 게임별 탭

그러나 결국 카카오톡 안에서 네이티브로 구현하는 방법은 실제로 사용하지 못했는데요, 카카오톡은 많은 이용자가 오랫동안 이용해온 안정화된 앱이다 보니 업데이트 주기가 긴 편입니다.

하지만 프렌즈타임은 매주 진행해야 하는 서비스이기 때문에 카카오톡의 업데이트 주기에 맞춰 서비스를 유지하기는 무리가 있었기 때문에 결국 다른 선택지를 찾을 수밖에 없었습니다.

그래서 선택했던 Plan B가 카카오톡 안의 브라우저로 실행하는 웹앱이었습니다.
당시 게임별 탭에서는 스낵게임[1]을 위해 제공하던 브라우저가 있었는데요, 이 브라우저에서는 웹앱 환경에서 일부 디바이스 API를 활용할 수 있도록 도와주는 인터페이스를 제공하고 있었습니다.

이 브라우저를 이용하면 사용자가 별도 설치할 필요 없는 장점을 유지하면서도 웹앱의 주요 단점 중 하나인 디바이스 API 사용 문제를 해결할 수 있었기 때문에 웹앱은 충분히 좋은 선택지로 다가왔습니다.

당시 팀 내에서는 웹 개발에 대한 전문성이 부족하다는 문제도 있었지만, 결국 저희는 새로운 도전을 해본다는 마음가짐으로 프렌즈타임을 웹앱으로 제작하게 되었습니다.

프렌즈타임 웹앱 개발기

웹앱은 별도의 설치 없이 간편하게 접속이 가능하며, OS 구분 없이 한 벌의 코드로 만들 수 있다는 장점이 있습니다. 하지만 단점 역시 명확합니다.

웹앱은 모든 자원을 네트워크를 통해서 받아야 하므로, 네트워크 환경이 느려지면 로딩 시간이 길어지면서 전체적인 앱이 느리게 느껴지게 됩니다.
또한 반응속도나 애니메이션의 퍼포먼스 역시 네이티브 앱보다 떨어지기 때문에 결과적으로 사용자 경험이 크게 떨어지게 됩니다.

하지만 사용자들에게는 웹앱인지, 네이티브인지는 크게 중요하지 않습니다. 결국 웹앱으로 서비스를 만들기 위해서는 이런 단점을 극복해서 유저들의 불편을 최소화해야 합니다.

Single Page Application

프렌즈타임은 웹앱의 사용자 경험을 위해 SPA[2]로 만들어졌습니다. SPA는 단일 페이지로 구성되어 있기 때문에, 화면(View) 이동 간에 별도의 로딩 없이 자연스러운 이동이 가능해서 웹의 사용자 경험을 크게 향상할 수 있습니다.

하지만 시작 시점에 앱을 구동하는데 필요한 모든 자원을 받아야 하므로 초기 구동 속도가 느려진다는 단점 역시 존재하기 때문에, 초기에 불러올 자원들을 최적화하여 빠르게 사용자가 앱을 사용할 수 있도록 해야 합니다.

Webpack

프렌즈타임의 정적 자원 관리는 웹팩을 이용하고 있습니다.
웹팩은 앱 내의 모듈들을 모아 정적 자원으로 번들링 하는 기능 외에도 Code SplittingLazy LoadingMinifyTree Shaking 등 여러 가지 추가적인 기능들을 제공하고 있습니다.

웹팩에서는 설정상의 mode를 production으로 바꾸는 것만으로도 배포 환경을 위한 여러 가지 기능들을 사용할 수 있지만, 앱에 따라 필요한 기능들은 설정이나 코드 수정을 통해 반영해야 합니다.

프렌즈타임에서는 초기 구동 시점에 불러오는 자원의 최적화를 위해서 모듈을 초기에 불러오지 않고, 나중에 필요한 시점에 불러오도록 하는 Lazy Loading을 적용하고 있습니다. 이 경우 웹팩 설정뿐만 아니라 코드의 수정도 필요합니다.

아래는 Vue.js에서 Lazy Loading을 구현하기 위해 dynamic import를 이용한 예제입니다.

import Splash from ‘@/view/Splash’

const router = new Router({
    routes: [
        {
            path: RouteKeys.Splash.path,
            name: RouteKeys.Splash.name,
            component: Splash,    // 1) import 로 불러온 모듈
        },
        {
            path: RouteKeys.Lobby.path,
            name: RouteKeys.Lobby.name,
            component: () => import('@/views/Lobby').then(m => m.default),    // 2) dynamic import로 불러온 모듈
        },
    ],
});

위의 예시에서 1) 의 모듈은 일반적인 import로 불러왔기 때문에 초기 구동 시점에 번들에 포함되어 가져오게 됩니다.
하지만 2) 의 모듈은 Lazy Loading 되기 때문에 시작 시점에 사용되는 번들에 포함되지 않으며, 추후 해당 모듈이 사용되는 시점에 불러오게 됩니다.

결국 Lazy Loading을 잘 활용한다면 초기에 가져오는 자원의 양을 줄일 수 있기 때문에 초기 로딩 속도를 좀 더 높일 수 있습니다.

이 외에도 웹팩 분석 플러그인을 이용하여 현재 번들에서 최적화가 필요한 부분을 찾을 수도 있습니다.
아래는 webpack-bundle-analyzer를 이용해 최적화 진행 중인 프렌즈타임을 분석한 이미지입니다.

webpack-bundle-analyzer는 각 모듈이 어떻게 결합해 있는지 보여주며, 크기를 통해 어느 정도 용량을 차지하는지를 가시적으로 알 수 있도록 도와줍니다.

위의 이미지를 보면 vendor 번들이 과도하게 큰 모습을 볼 수 있습니다. vendor 번들은 일반적으로 앱에서 사용 중인 패키지들이 중복으로 사용되지 않도록 모아둔 번들입니다.
하지만 위 같은 경우 moment.js 관련 패키지들(moment-timezone과 moment)이 너무 큰 크기를 차지하고 있는 모습을 볼 수 있습니다.

만약 위 같이 특정 패키지가 너무 크다면 다른 가벼운 패키지/코드 등으로 교체한 후 삭제하거나, 꼭 필요하다면 다른 패키지들과 별도로 필요할 때 가져올 수 있도록 분리해서 관리하는 것이 좋습니다.

위의 moment.js 패키지는 날짜를 쉽게 포맷팅하도록 도와주는 패키지로, 사용하기는 편하지만 매우 무거운 편입니다.
프렌즈타임에서 moment.js는 서버에서 받는 시간의 timezone 설정을 위해 사용되었습니다.
하지만 위의 분석 시점에서는 서버에서 timezone을 미리 설정하여 일자를 보내주도록 수정된 상태로 사용되지 않는 상태였기 때문에 해당 패키지는 삭제 처리했습니다.

결과적으로 불필요한 패키지가 삭제되어 vendor 번들이 작아졌고, 처음에 가져와야 하는 자원의 크기를 더 줄일 수 있었습니다.

위처럼 번들 분석을 통해 모듈 간의 의존성을 파악하거나 필요성에 비해 너무 무거운 패키지를 찾는 등 간접적으로 정적자원을 최적화하는 데 도움을 받을 수도 있습니다.

이미지 미리 불러오기

보통의 웹 환경에서 이미지가 가장 많은 용량을 차지하는 경우가 많습니다.[3]
프렌즈타임의 경우도 마찬가지로 중심에 프렌즈 캐릭터들을 사용하고 있기 때문에, 캐릭터와 관련된 여러 가지 이미지가 전체 자원의 많은 부분을 차지하고 있습니다.

용량이 큰 이미지의 특성상, 시작 시점에 필요한 이미지가 아니라면 정적 자원에 포함하지 않고 필요한 시점에 비동기로 가져오는 경우가 많습니다.
특히 용량이 큰 이미지들이 반복적으로 노출될 수 있는 경우에는 사용자들에게 이미지를 가져오는 시간 동안 이미지 위치에 placeholder를 두거나, 별도의 로딩 indicator를 넣어 아직 이미지를 가져오는 중이라고 표현하기도 합니다.

이미지 Lazy Loading 예시
구글의 지연 로딩 가이드에서 예시로 사용된 퍼블리싱 플랫폼 Medium의 이미지 Lazy Loading

하지만 프렌즈타임의 경우, 대부분의 화면에서 화면전환과 함께 많은 이미지들이 동시에 보여야 합니다.
용량이 크지 않은 이미지더라도, 최대 수십만의 사용자가 동시에 같은 이미지를 요청하게 된다면 서버 부하로 인해서 해당 이미지를 빠르게 가져올 수 있다는 보장을 할 수 없게 됩니다.

특히 짧은 시간 동안 진행되는 가위바위보를 진행 중일 때 버튼 이미지의 로딩이 늦어져서 버튼을 클릭하지 못하게 된다면, 서비스의 핵심 콘텐츠가 완전히 무너지게 되는 문제가 발생할 수도 있습니다.

그래서 프렌즈타임의 경우 이미지를 늦게 가져오되, 해당 화면이 보이기 전에 미리 불러오는 방식을 사용하고 있습니다.

프렌즈타임은 플로우가 어느 정도 정해져 있는 서비스입니다.
스플래시 화면 후에는 라이브를 기다리는 로비 화면이 노출되고, 로비 화면 후에는 가위바위보가 진행됩니다.

프렌즈타임에서는 이런 정해진 플로우를 이용해서 스플래시 화면에서부터 미리 로비에서 사용될 춤추는 라이언 이미지를 가져오고, 로비 화면에서는 가위바위보 중 사용될 고민하는 라이언과 버튼을 가져오는 식으로 이미지를 미리 요청하고 있습니다.

이렇게 미리 이미지를 가지고 오게 되면, 여러 유저가 한 번에 이미지를 요청해서 발생하는 부하도 줄일 수 있고 이미지가 늦게 가져와 져서 빈 화면이 보이는 문제도 줄일 수 있습니다.

자연스러운 애니메이션 처리

애니메이션 역시 이미지와 마찬가지로 이전의 화면부터 미리 가져오도록 처리되고 있습니다.

프렌즈타임의 경우 스프라이트 시트를 미리 불러온 후, 자바스크립트로 프레임별로 재생해서 애니메이션을 보여주는 스프라이트 애니메이션을 사용하고 있습니다.

위의 라이언 애니메이션은 여러 장의 정지된 라이언 이미지를 순서대로 재생 시켜 만들어집니다.

하지만 이 방법은 여러 장의 이미지가 뭉쳐진 스프라이트 시트를 사용하기 때문에, 이미지가 전부 다 다운로드받아지지 않는다면 애니메이션이 의도한 대로 보이지 않을 수도 있습니다.

아래의 애니메이션의 경우 네트워크 환경의 문제로 이미지가 완전히 로드 되지 않은 시점에 애니메이션을 재생시켜서 비어있는 프레임만큼 깜빡임 현상이 발생한 케이스입니다.

애니메이션에 이용되는 이미지들도 네트워크를 통해 받아서 처리하기 때문에, 이전 화면에서부터 이미지를 받기 시작했지만 용량이 큰 스프라이트 시트의 특징과 느린 네트워크 환경이 더해져 이미지가 전부 다 받아지지 않아 발생한 문제였습니다.

이미지 로딩은 어디까지나 비동기로 처리되기 때문에, 애니메이션이 필요할 때 이미지를 완전히 보장할 수는 없습니다.
하지만 비동기더라도 이미지가 완전히 불러와진 시점은 알 수 있기 때문에 저희는 애니메이션을 온전히 보여줄 수 있을때 까지 애니메이션을 정지시켜서 정지된 이미지를 노출시키기로 결정했습니다.

위 이미지는 기존의 애니메이션 실행 순서입니다. AnimationLoader 모듈은 스프라이트 시트의 각 프레임을 재생시켜 애니메이션처럼 보이게 만들어주고, ImageLoader 모듈은 이미지를 미리 불러오는 역할을 합니다.

두 모듈은 각각 독립되어 있기 때문에, 화면에서 라이언 애니메이션 실행 요청이 왔을때 AnimationLoader는 이미지 로드 여부에 상관 없이 바로 애니메이션을 실행하게 됩니다. 이 때 스프라이트 시트가 완전히 로드되지 않았다면 깜빡이는 현상이 발생하게 됩니다.

여기서 애니메이션을 실행하기 전에 이미지 로드 여부를 확인하여 아직 완전히 로드되지 않았다면 실행을 막아 애니메이션의 시작시점의 정지된 이미지를 유지시키도록 수정해보았습니다.

수정 후 AnimationLoader는 애니메이션을 실행시키기 전 ImageLoader에게 애니메이션에 필요한 이미지가 완전히 로드됐는지 확인을 하게되고, ImageLoader는 이 요청에 라이언이 준비된 시점에 resolve되는 Promise를 반환합니다.

만약 이미지가 로드됐다면 resolve 결과를 받고 애니메이션은 바로 실행되며, 이미지가 아직 로드되지 않았다면 애니메이션이 실행되지 않기 때문에 첫번째 프레임의 정지된 이미지가 노출되게 됩니다.

그 결과, 이미지가 로딩중일때는 라이언이 가만히 서있는 것 처럼 정지해 있다가 로딩이 완료된 시점에 춤을 추는 모습으로 보여줄 수 있게 되었습니다.

웹앱에서는 비동기 처리를 이용해 느린 속도를 어느정도 보완할 수는 있지만, 그로 인해 위처럼 사이드 이펙트가 발생할 수도 있기 때문에 항상 주의가 필요합니다.

그리고 몇가지 도움받은 툴들

초보 개발자로서 B2C[4] 서비스와 모던 자바스크립트(ES6, Vue.js)를 처음 경험하면서 개인적으로 여러가지 툴들의 도움을 많이 받았습니다.

이 아래에는 그 중 개인적으로 꼭 추천드리고 싶은 몇가지 툴들을 간단히 소개하면서 글을 마무리할까 합니다.

ESLint

ESLint는 잘못 작성된 코드를 찾아 교정해주는 자바스크립트를 위한 Linter 중 하나입니다.
ESLint는 간단하게 다른 플러그인을 사용하여 여러가지 룰을 한번에 세팅할 수 있고, 필요에 따라 커스터마이징도 쉽게 할 수 있습니다.

ESLint를 추천드리는 이유는 좋은 코딩습관을 쉽게 익힐 수 있기 때문입니다. 예컨데 ES6를 처음 실제로 써보게 되면 좋고 나쁜 습관을 구분하기 힘들기 때문에 코드가 쉽게 지저분해질 수 있습니다.(제 이야기입니다.)

이 때 ESLint가 룰에 맞지 않는 코드를 알려주고, ESLint 홈페이지의 문서에서 해당 룰을 지켜야 하는 이유까지 설명해주기 때문에 놓치고 있던 실수들을 교정할 수 있도록 도와줍니다.

프렌즈타임에서는 airbnb-base 플러그인을 베이스로 필요에 따라 룰을 수정/삭제하여 사용하고 있습니다. airbnb 룰은 타이트한 편이라 코딩 컨벤션을 맞추고 깔끔한 코드를 만들기 좋아서 많이 사용됩니다.

추가적으로 git hook을 이용하면 Lint를 통과하지 않으면 커밋을 하지 못하도록 강제할 수도 있습니다. pre-commit 시점에 Lint를 통과하지 못하면 커밋을 막기때문에, 혹시 발생할 수 있는 실수를 잡는데도 도움을 받을 수 있습니다.

ESLint를 프로젝트 중간에 도입한다면 처음에는 수많은 에러에 압도될 수도 있지만, 모든 파일을 한번에 수정할 필요는 없습니다. 현재 작업중인 파일부터 하나하나 고쳐나가다 보면, 결국엔 깔끔하게 정리된 코드를 보실 수 있을겁니다.

ESLint에서는 코드의 자동 수정 기능도 제공하지만, 코드에서 error나 warning이 발생한다면 이유를 꼭 확인하고 수정해주세요.

Sentry

Sentry는 클라이언트에서 발생하는 에러도 추적할 수 있도록 도와주는 에러 트래킹 툴입니다.

보통 서버에서 발생하는 오류는 로그가 남아 확인할 수 있지만, 클라이언트에서 발생하는 오류는 개발자가 확인하기가 힘듭니다. 대부분의 사용자들은 오류가 발생하더라도 인지하지 못하거나, 인지하더라도 리포팅을 잘 하지 않습니다.
이럴때 Sentry같은 에러 트래킹 툴을 사용하면 클라이언트에서 발생하는 스크립트 오류를 실시간으로 추적할 수 있습니다.

위의 이미지처럼 어떤 에러가 몇명의 유저에게 몇번 발생했는지 한눈에 확인할 수 있으며, 이 외에도 에러가 발생하기 까지의 이벤트 이력, 발생한 유저의 기기 정보, 발생한 코드의 위치까지 세부적인 정보도 확인할 수 있습니다.

실제로 프렌즈타임을 오픈하면서 Sentry를 통해 몇가지 숨겨진 에러를 찾아 수정하기도 했습니다.

예를 들면 순간적으로 트래픽이 몰리면서 동영상 플레이어 sdk를 제대로 받아오지 못한 유저가 발생했고, 이 유저들은 라이브를 보지 못해 새로고침을 시도하면서 반복적으로 오류를 발생시키고 있었습니다.
Sentry에서 에러를 발견한 덕분에 이런 증상을 빠르게 발견하여 조치할 수 있었습니다.

동영상이 나오지 않는다는 증상은 여러가지 이유가 있을 수 있습니다. 하지만 이렇게 직접적으로 클라이언트의 에러로그를 볼 수 있다면, 문제의 원인를 찾고 고치기 훨씬 수월해집니다.

Sentry는 유료 플랜을 제공하기도 하지만, 오픈소스이기 때문에 직접 환경을 구성해서 사용할 수도 있습니다. 사용자의 피드백을 받기 힘든 B2C 어플리케이션을 만들거나 서비스하고 계시다면 에러 트래킹 툴을 강력하게 추천드립니다.

마무리

웹 환경에서는 대부분의 자원을 네트워크 환경을 통해 받아야 하기때문에, 네트워크 지연으로 인한 여러가지 이슈가 발생합니다. 하지만 가능한 사용자 경험을 떨어트리지 않기 위해 여러가지 방법을 생각중이고, 적용해보려고 노력하고 있습니다.

서비스를 오픈하여 사용자들이 모이고, 업데이트로 인해 필요한 자원은 계속 늘어나다 보니 점점 최적화의 중요성을 더욱 깨닫고 있기도 합니다. 웹앱에서는 사용할 수 있는 자원이 제한적이기 때문에, 비동기를 최대한 잘 활용해야 한다고 생각합니다.

프렌즈타임은 아직도 부족한 부분이 많습니다. 그래도 앞으로 더 좋은 서비스를 제공할 수 있도록 노력하겠습니다.

부족한 글을 여기까지 읽어주셔서 감사합니다.

목요일 12시 프렌즈타임 잊지마세요!


  1. 카카오톡 안에서 실행할 수 있는 HTML5 게임 ↩︎
  2. Single Page Application ↩︎
  3. 통계적으로 웹페이지의 전체 리소스의 절반가량을 이미지가 차지. ↩︎
  4. Business to Customer, 일반 사용자들(소비자들)을 대상으로 하는 거래 ↩︎

함께 해요!

카카오톡 공유 보내기 버튼

Latest Posts

제5회 Kakao Tech Meet에 초대합니다!

Kakao Tech Meet #5 트렌드와 경험 및 노하우를 자주, 지속적으로 공유하며 개발자 여러분과 함께 성장을 도모하고 긴밀한 네트워크를 형성하고자 합니다.  다섯 번째

테크밋 다시 달릴 준비!

(TMI: 이 글의 썸네일 이미지는 ChatGPT와 DALL・E로 제작했습니다. 🙂) 안녕하세요, Kakao Tech Meet(이하 테크밋)을 함께 만들어가는 슈크림입니다. 작년 5월에 테크밋을 처음 시작하고,