현재 회사에서 CI/CD 툴로 jenkins를 사용중이다. 배포 기록을 뒤적거리다보니 지나치게 무겁지 않은 서비스인데 빌드 속도가 생각보다 느리다고 느껴졌고 조금 더 빠른 배포를 위해, 웹팩에 대해 자세히 알아보고자 웹팩 빌드 속도 최적화하는 방법을 찾아보고 적용해 보기로 했다. 참고로 리액트를 사용중인 서비스이다.
먼저 가장 최근의 배포 기록을 기반으로 빌드 속도를 먼저 조사했다. 속도 측정은 웹팩 빌드시 처리시간을 측정하여 반환하는 라이브러리인 Speed-Measure-Webpack-Plugin을 활용하려 하였으나 웹팩5 버전에서 원활하게 구동하기 어려운 관계로(관련 링크) 빌드 시에 기본 출력되는 컴파일 완료 시간 값을 수집했다.
실제 배포 서버 : 46630ms 테스트 서버 : 51517ms, 로컬 : 18128ms
웹팩 설정은 개발 환경에서의 설정과 배포 환경에서의 설정, 그리고 양쪽 모두 사용하는 common 설정 나누어져 있었는데 개발 환경에서는 'inline-source-map'을 사용 중이였다. 여기서 sourcemap 파일은 원본 코드의 경량화가 진행 된 .min파일의 특정 부분에서 문제가 발생했을 때 해당 부분이 원본 파일의 어느 부분인지를 알려주는 역할을 하는 것이다. 개발 환경에서는 아무래도 어느 코드 어느 줄에 에러가 났는지 정확한 원인의 확인을 필요로 하기 때문에 'eval-source-map'으로 변경했다.
module.exports = merge(commonm {
mode: 'development',
// devtool: 'inline-source-map'
devtool: 'eval-source-map',
'eval-cheap-module-source-map'을 사용하면 속도는 조금 더 빠르고 코드 원본은 살아있지만 디버깅에서 정확한 매핑이 어려워 코드 원본이 살아있고 정확한 디버깅을 위해 공식 문서에서 개발 단계에서 추천하는 'eval-source-map' 사용했다.
관련 내용을 서칭해보면 블로그에 'eval-cheap-module-source-map'을 사용하는 것을 심심치 않게 볼 수 있는데 직접 적용해본 결과 에러가 난 코드의 라인을 정확히 매핑해주지 못했다. 빌드 속도 차이가 엄청나게 나진 않지만 그 차이를 위해 정확한 디버깅을 포기하는건지 아직까지 의문이다. 개발 환경에서의 빌드 속도는 이 설정에서 크게 감소했다.
배포 환경에서는 'source-map'을 사용중이였고 디버깅이 필요 없는 환경이라 false로 설정하려 했다.
하지만 에러 추적 도구로 sentry를 연동해 적극 활용 중이였는데, sentry에서 에러를 추적할 때 sourcemap 파일을 사용하기 때문에 사용자의 정확한 에러 추적을 위해 sourcemap 파일이 반드시 필요했다.
그런데 여기서 뜻밖의 보안 이슈로 연결 될 수 있는 문제를 찾았다.
'source-map'을 그대로 사용하면 개발자 도구 - 소스 탭에 코드 원본이 그대로 노출되기 때문에 코드 유출의 여지가 있었고, 두가지 방법을 적용할 수 있었다.
첫번째는 package.json에 정의되어 있는 build 스크립트를 수정해 배포 단계에서 소스맵 파일이 들어있는 폴더에서 소스맵 파일을 삭제하는 코드를 적어주는 것이였고, 두번째는 jenkins 파이프라인 스크립트를 수정해주는 것이였다. 하지만 첫번째 방법은 현재 배포 폴더 이름이 브랜치명을 따라가고 있어 의존성에서 자유롭지 못할 것 같다라는 동료분의 피드백이 있었다.
결론적으로 sourcemap 설정은 그대로 두고 jenkins에서 빌드 성공 후 배포가 나갈때 sourcemap 파일을 제외하고 배포해주는 스크립트를 기존 jenkins 파이프라인 스크립트에 추가해주었다.
이렇게 설정하면 sentry에는 sourcemap 파일을 보내주어서 에러를 정상적으로 추적 할 수 있지만, 브라우저에서는 배포를 진행할 때 sourcemap 파일을 제외하고 진행시켰기 때문에 소스 코드가 노출되지 않았다.
// front production/test
def sshPublisherFront(SERVER_CONFIG, params) {
sshPublisher(
...
publishers: [
...
sshTransfer(sourceFiles: "dist/${params.branchName}/^(?!.*\.map$).*$")
]
)
]
)
}
의도치않게 보안 이슈까지 막을 수 있었다.
웹팩 설정 중 cache라는 설정으로 생성된 웹팩 모듈 및 청크를 캐시하여 빌드 속도를 개선할 수 있다. default는 development 모드에서 type: 'memory'로 설정 되고 production 모드에서는 비활성화 되는데 기존에는 따로 설정을 해주지 않아 production 모드에서 캐싱이 전혀 안되고 있었다.
cache: { type: process.env.NODE_ENV === 'production' ? 'filesystem' : 'memory' },
다음과 같이 설정 해주면 production 모드에서는 filesystem으로 캐싱이 활성화 된다. 웹팩 설정이 크게 바뀌지 않으면 빌드 시 모듈을 새로 생성할 필요 없이 가지고 있던 캐시 파일을 사용하면 되기 때문에 빌드 속도가 크게 줄어든다.
캐시 설정 옵션 중 캐시의 경로를 설정해 주는 cache.cacheDirectory라는 옵션이 있는데 따로 설정하지 않으면 node_modules/.cache/webpack 경로에 캐시 파일이 저장된다. 배포 환경에서는 이 설정으로 빌드 속도가 크게 감소되었다.
웹팩 런타임 코드에는 브라우저에서 번들 모듈을 올바르게 실행하는 데 필요한 웹팩 런타임 및 매니페스트가 포함되어 있고, 기본적으로 빌드 프로세스에서 생성된 각 청크에 런타임 코드를 포함해 실행한다. 하지만 optimization의 runtimechunk 옵션을 활성화하면 런타임 코드를 번들링된 코드와 분리를 하여 작업을 해 빌드속도가 빨라진다.
runtimeChunk: { name: 'webpack' },
이름은 webpack으로 설정해 코드를 분리시켜 주었다.
http 요청이 추가적으로 한 번 발생하긴 하지만 한 번만 요청에 성공하면 브라우저에서 캐싱이 가능하기 때문에 성능상 이점이 있을것이라고 판단해 설정을 추가해 주었다.
웹팩 플러그인 중 이전 빌드 결과물을 삭제시켜주는 clean-webpack-plugin이라는 유용한 도구가 있었다. 이 플러그인을 기존에 설치해서 사용 중이였는데, 웹팩에서 관련된 내장 옵션을 5.20 버전부터 지원해 플러그인 대체가 가능하여 삭제하고 내장 옵션을 추가시켜 주었다.
output: {
...
clean: true,
},
실제 배포 서버 : 46630ms -> 18329ms, 테스트 서버 : 51517ms -> 6473ms, 로컬 : 18128ms -> 1521ms
실제 배포 서버의 결과는 마지막 배포 내용에 package-lock 파일이 변경된 커밋이 같이 딸려 배포 되었는데 package-lock 파일이 많이 변경 된 탓인지 캐싱이 제대로 이루어지지 않아 이전과 비슷한 속도를 보여주었다. 그래도 테스트 서버와 빌드 환경은 거의 유사하기에 비슷한 성능 향상을 보여주지 않을까 싶다.

7/19일자 배포로 실제 배포 서버도 시간 단축 확인 완료
생각보다 꽤 효과 있는 개선 결과의 지표를 명확하게 확인 할 수 있어서 보람 있었고, 다음 최적화는 이전에 진행하다 모종의 이유로 중단되었던 코드 스플리팅이 될 것 같다.
ref:
https://webpack.kr/configuration/devtool/
https://fgh0296.tistory.com/20
https://medium.com/hcleedev/web-%EB%B2%84%EA%B7%B8-%EB%A6%AC%ED%8F%AC%ED%8C%85%EC%9D%84-%EC%9C%84%ED%95%9C-sentry-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-react-webpack-b9690b8f5b45
https://webpack.kr/configuration/optimization/#optimizationruntimechunk
https://jeonghwan-kim.github.io/2022/08/21/webpack-output-clean