들어가며
안녕하세요. 저는 QUICK Game Platform Dev 팀에서 게임에서 필요한 여러 기능을 제공하는 QUICK Game SDK를 개발하고 있는 이재호입니다. QUICK Game Platform이란 HTML5 기반의 게임을 서비스하기 위해 LINE에서 제공하는 플랫폼입니다. 저는 이번 글에서 QUICK Game SDK를 개발하고 개선하면서 SDK의 크기를 줄이고 성능을 개선했던 사례를 공유하려고 합니다.
환경
실험은 다음과 같은 환경에서 진행되었습니다.
Dependency | 모듈명 | 버전 |
---|---|---|
devDependencies | webpack | 4.28.2 |
devDependencies | @babel/core | 7.4.3 |
devDependencies | @babel/cli | 7.4.3 |
devDependencies | babel-loader | 7.1.5 |
devDependencies | mini-css-extract-plugin | 0.7.0 |
devDependencies | webpack-bundle-analyzer | 3.3.2 |
QUICK Game Platform SDK 성능 개선 결과
먼저 결과부터 말씀드리겠습니다. 아래는 본문에서 설명드릴 개선 방법을 QUICK Game Platform SDK에 적용한 후 각 지표별로 얼마나 개선되었는지 측정한 결과입니다.
실험 환경에 따라 아래 실험 결과는 달라질 수 있으니 단순 참고용으로만 보시기 바랍니다.
지표 | 결과 |
---|---|
SDK 코어(core) 모듈 로딩 시간 | 약 83% 감소 |
SDK 코어 모듈 파일 크기(청크(chunk) 처리한 파일 배제) | 약 87% 감소 |
총 자원 로딩 시간(이미지 자원을 제외하고 SDK의 플러그인 모듈까지 모두 로드하는데 걸리는 시간) | 약 31% 감소 |
성능 개선 방법 - 최소화(minimization)
1. 개발 모드 확인
webpack의 개발 모드가 'production' 모드인지 'development' 모드인지 확인합니다. production 모드에서 빌드하면 몇몇 모듈은 자동으로 최소화되어 크기를 줄이는 데 도움이 됩니다.
예시 코드
//webpack development mode.
process.env.BABEL_ENV = 'development'
process.env.NODE_ENV = 'development'
module.exports = {
mode: 'development',
...others
}
//webpack production mode.
process.env.BABEL_ENV = 'production'
process.env.NODE_ENV = 'production'
module.exports = {
mode: 'production',
...others
}
실험 결과
React 모듈을 각각의 모드로 빌드한 후 결과를 살펴보았습니다. webpack 분석 플러그인 화면에서 production 모드의 React 영역이 상당히 줄어든 걸 확인할 수 있습니다.
모드 | 파일 크기 | webpack 분석 플러그인 화면 |
---|---|---|
development | js: 99KB, gzipped: 34KB |
|
production | js: 82KB, gzipped: 28KB |
2. Tree shaking 개선
Tree Shaking이란 사용하지 않는 모듈을 제거한 후 빌드하는 것을 의미합니다. webpack에서는 빌드할 때 기본적으로 사용하지 않는 모듈을 제거하지만, 사용 여부가 모호한 모듈은 제거되지 않고 빌드됩니다. 아래 예시 코드의 TO-BE와 같이 임포트하면 사용하지 않는 서브 모듈이 포함되는 것을 막을 수 있습니다.
예시 코드
//AS-IS
import moduleA from 'moduleA'
moduleA.get('')
//TO-BE
import moduleA from 'moduleA/get'
moduleA.get()
실험 결과
Lodash 모듈에서 사용하는 서브 모듈만 포함시켜 파일 크기가 줄어든 것을 확인할 수 있습니다.
모드 | 파일 크기 | webpack 분석 플러그인 화면 |
---|---|---|
Lodash 모듈 전체 임포트 | js: 74KB, gzipped: 26KB |
|
Lodash 모듈에서 get만 임포트 | js: 11KB, gzipped: 3KB |
3. 자원 외부화
자바스크립트로 개발할 때 코드와 함께 필요한 자원(asset)을 파일에 포함해야 하는 경우가 있습니다. 이때 자원을 Base64로 변환하여 같이 빌드하거나 외부로 빼내는 작업을 진행하는데요. 만약 자원의 크기가 크다면, 외부로 빼서 따로 두는 형태로 빌드한 후 CDN(contents delivery netword)에서 자원을 가져오게 하면 처음 자바스크립트 파일을 로드할 때 걸리는 시간을 줄일 수 있습니다. 다만 이때 주의해야 할 점이 있는데요. 자원의 크기가 별로 크지 않고 네트워크의 성능이 좋지 않다면 로드할 때 오히려 더 오랜 시간이 걸릴 수 있습니다.
예시 코드
//AS-IS
module.exports = {
...others,
module: [
...otherModules,
{
test: /.(sa|sc|c)ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
}
]
}
]
}
//TO-BE
module.exports = {
...others,
module: [
...otherModules,
{
test: /.(sa|sc|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] // style-loader에서 MiniCssExtractPlugin.loader로 변경
},
{
test: /.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader', // url-loader에서 file-loader로 변경
options: {
emitFile: true,
name: '[name].[ext]',
},
}]
}
]
}
실험 결과
자원 외부화로 test.jpeg이 빠지면서 파일 크기가 많이 줄어든 걸 확인할 수 있습니다.
모드 | 파일 크기 | webpack 분석 플러그인 화면 |
---|---|---|
자원을 포함했을 때 | js: 112KB, gzipped: 47KB |
|
자원을 포함하지 않았을 때 | js: 82KB, gzipped: 28KB, image: 23KB |
4. 자원 파일 크기 축소
어떤 상황이나 환경에선 고화질의 이미지 파일보다 빠른 로딩 속도가 중요할 수 있습니다. 따라서 제공하는 자원의 크기를 각 상황에 맞게 최적화하면 사용자 경험에 좋은 영향을 줄 수 있습니다. 몇 가지 툴을 이용하면 이미지 파일의 크기를 쉽게 조절할 수 있는데요. 가장 쉽게 사용할 수 있는 툴로는 imageOptim이 있습니다.
5. 필요 없는 모듈 제외
서비스에 필요없는 몇 가지 모듈이 파일에 포함되는 경우가 있는데요. 특히 애플리케이션에서 특정 국가의 언어만 지원할 때는 moment.js에 포함된 그 외 국가들의 로캘(locale) 정보는 필요하지 않습니다. 이를 제거하면 파일 크기를 줄일 수 있습니다.
예시 코드
//webpack.config.js
module.exports = {
...others,
plugins: [
...otherPlugins,
new webpack.IgnorePlugin(/^./locale$/, /moment$/),
]
}
실험 결과
필요하지 않은 로캘 정보를 제외시킨 결과, 파일 크기가 많이 줄어든 것을 확인할 수 있습니다.
모드 | 파일 크기 | webpack 분석 플러그인 화면 |
---|---|---|
로캘을 모두 포함했을 때 | js: 430KB, gzipped: 115KB |
|
필요한 로캘만 포함했을 때 | js: 166KB, gzipped: 65KB |
6. 압축 파일 활용
일부 브라우저에서는 압축 파일 형식을 지원합니다. 따라서 gzip 형식의 파일을 함께 올려 자원을 선택적으로 내려 받을 수 있게 지원하는 것이 좋습니다.
예시 코드
//webpack.config.js
module.exports = {
plugins: [
...otherPlugins,
new CompressionPlugin(),
]
}
성능 개선 방법 - 최적화(optimization)
Dynamic Import와 모듈 chunk, Promise.all 조합
어떤 모듈은 처음 실행되는 시점에선 꼭 필요하지 않습니다. 또한 여러 리소스를 동시에 다운 받아 좋은 사용자 경험을 만들 수도 있습니다. 이런 점을 활용하여 Promise.all로 청크 처리한 모듈을 Dynamic Import하여 로딩에 걸리는 시간을 줄일 수 있습니다.
예시 코드
//webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.js(x)?$/,
use: {
loader: 'babel-loader',
options: {
plugins: [
...others,
'@babel/plugin-syntax-dynamic-import',
]
}
}
}
]
}
}
//AS-IS
import m1 from './m1'
import m2 from './m2'
import m3 from './m3'
//TO-BE
return Promise.all([
import(
'./module1'
/* webpackPreload: true */
/* webpackChunkName: "m1" */ )
.then(({default: m1}) => m1),
import(
'module2'
/* webpackPreload: true */
/* webpackChunkName: "m2" */)
.then(({ default: m2 }) => m2),
import(
'module3'
/* webpackPreload: true */
/* webpackChunkName: "m3" */)
.then(({ default: m3 }) => m3)])
.then(([m1, m2, m3]) => window.context = { m1, m2, m3 })
실험 결과
모듈을 청크 처리하면 각각에 해당하는 plugin.js 파일이 생성되는데요. 아래 webpack 분석 플러그인 화면에서 확인할 수 있습니다(서로 색깔이 다른 부분을 참조). 사실 처리 전후 전체 용량은 차이가 없습니다. 하지만 처음 실행할 때는 꼭 필요한 모듈만 불러오고, 그 외는 이후 필요할 때 불러오기 때문에 초기 로딩 시간을 단축시킬 수 있습니다.
모드 | 파일 크기 | webpack 분석 플러그인 화면 |
---|---|---|
모듈 청크 처리한 경우 | core: 33KB, gzipped: 20KB plugin1: 8KB, gzipped: 3KB plugin2: 72KB, gzipped: 25KB plugin3: 54KB, gzipped: 17KB |
|
모듈 청크 처리하지 않은 경우 | js: 166KB gzipped : 65KB |
마치며
지금까지 SDK 성능을 높이기 위한 몇 가지 방법과 이를 실제로 적용한 사례를 말씀드렸습니다. 이외에도 더 많은 방법이 있겠지만, 제가 직접 경험해 보고 쉽게 적용할 수 있겠다고 생각한 방법 위주로 말씀드렸습니다. 이런 방법을 적용하여 좀 더 성능이 개선된 여러 서비스가 나오기를 기대하고 있습니다. 단, 글 서두에 말씀드린 대로 모듈 버전과 빌드 방법에 따라서 각 방법을 적용하는 방식과 결과가 상이할 수 있습니다. 이점 참고하시기 바랍니다.
혹시 본문에 제가 오기한 내용이 있다면 가감없는 의견을 여기로 전달해 주시면 감사하겠습니다.