메모리 부족으로 인한 CRA build fail 해결하기

Yeri Kim
6 min readMay 8, 2018

--

문제

프로젝트 규모가 커지면서 CI 서버에서 docker build할 때, react build 시 메모리 부족으로 죽는 현상

해결

  1. GENERATE_SOURCEMAP=false
  2. code splitting
  3. analyze 후 너무 큰 용량이면서 대체가능한 dependency 제거

주로 cloud 환경에서 메모리 용량이 1GB이하일 때 build fail이 날 수 있다고 한다(https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#npm-run-build-exits-too-early). 우리는 CI 서버에 2GB 설정해두었다고 하는데 최근들어 build fail 횟수가 점점 늘어났다.

CTO님이 조만간 3GB로 늘려주신다고 했지만, 2GB도 작은 것은 아니기에 원인을 파악해서 수정하는 것이 좋겠다고 하셨다.

Creating an optimized production build...
The build failed because the process exited too early. This probably means the system ran out of memory or someone called `kill -9` on the process.

항상 멈추는 시점은 “Creating an optimized production build… “ 메시지가 뜨고 몇 분 후이다. 여기에서 js파일들을 bundle하고 source map을 만드는 구간인 것 같다.

관련 이슈

많이들 비슷한 경우가 있나보다. CRA(Create React App) issue 답변에서 해결방법을 찾을 수 있었다.

해결해보자.

  1. GENERATE_SOURCEMAP=false

source map을 disable한다. 원래도 development 환경일 때만 디버깅을 했고, build하고 production에 올릴 때는 소스를 공개하면 안 되기 때문에 아래의 명령어로 따로 source map을 삭제하고 있었다.

"build": "react-scripts build && rm build/static/**/*.map"

위같이 삭제하지 않아도 source map을 disable하는 환경설정이 추가 됐다.

react-script 1.0.11 버전부터 가능하다고 하여, 이참에 1.1.4 버전으로 올렸다. 프로젝트에 충돌이나 dependency 버전 문제는 없는 듯 하다.

root 디렉토리의 .env 파일에 추가해도 되고, script에 추가해도 된다.

"build": "GENERATE_SOURCEMAP=false react-scripts build"

2. code splitting

CRA는 build 과정에서 모든 js파일을 하나로 합친다. 프로젝트 규모가 커질 수록 component가 많아지면 아무래도 bundle하기 위해 파일간 dependency 계산에 오래걸릴 수밖에 없을 것이다.

두 번째 해결방법은 code splitting인데 dynamic import를 통해서 파일을 나누는 것이다. 보통 route를 설정한 곳에서 하는데, dynamic import를 하면 각 route마다 필요한 component(page)에 dependency된 부분만 chunk 파일이 생기며 해당 path로 들어가면 그 chunk js만 로드하게된다.

dynamic import를 위해 react-loadable을 사용했다.

import ReactLoadable from 'react-loadable';
import LoadingIndicator from './LoadingIndicator';
const Loadable = options => ReactLoadable({
loading: LoadingIndicator,
delay: 300,
...options
});
const AsyncMainPage = Loadable({
loader: () => import('./MainPage')
});
const AsyncLoginPage = Loadable({
loader: () => import('./LoginPage')
});
<Route path="/main" component={AsyncMainPage}/>
<Route path="/login" component={AsyncLoginPage}/>

모든 route에 하려니 연이은 Loadable 호출때문에 코드가 지저분해졌는데, 맞는 것인지 잘 모르겠다. 결국은 component가 많은 부분만 async로 가져오기로 했다.

3. analyze

2번에 async할 페이지를 결정하기 위해 analyze를 돌렸다. component가 많고 js logic이 많은 페이지가 용량이 클 줄 알았는데 한글이 몇 백줄 되는 약관페이지 류가 용량이 상당히 컸다 ㅎㅎ

메모리 용량때문에 build fail난 시점이 emoji 관련 라이브러리를 추가하고부터 인데 역시나 수많은 모듈이 있음에도 emoji 모듈이 20% 정도의 크기를 차지하고 있었다 -_-

앞으로 추가할 모듈이 많아질 것 같은데, 어떻게 해야하는지 잘 모르겠다. 용량 검사를 하고 그나마 가벼운 것을 골라야 하는지. 일단은 emoji 모듈은 삭제하고 다른 것으로 대체하기로 했다.

build success!

부족한 지식과 해석일 수 있으니 틀린 내용, 다른 의견이 있다면 댓글 남겨주시면 감사하겠습니다!

--

--