프론트엔드 ci/cd 속도 최적화 (feat. docker multi staging)

Sming·2023년 6월 4일
111
post-thumbnail

돌아온 빌드 속도 최적화

기존에 github action에서 빌드속도 최적화, webpack에서의 빌드속도 최적화 부분을 작성한것이 있었는데 이번에 또 빌드 속도 최적화라는 글로 다시 작성하게 되네요.

github action 최적화

이 글을 작성하는 이유는 지난번에 작성한 최적화는 실제 현업에서 빌드를 할떄의 환경과 가볍게 ci만 최적화하던것이 달랐기 때문입니다.

기존에는 docker도 이용하지 않고 package manager를 yarn에 고정하고 다른것을 고려하지 않았었습니다. 또한 이번에는 nextjs를 이용하기 때문이죠.

먼저 최종 결과부터 한번 같이 보시죠. jenkins를 이용하였으며 좌측에는 최적화 전, 우측이 최적화 후입니다. (우측의 상단이 캐싱 x, 하단이 캐싱이 적용된 사진입니다.)

  • 원인 분석
  • docker 이미지 줄이기
    • docker multi staging
  • package manager 선택하기
    • yarn berry
    • yarn berry를 포기한 이유
    • 번외) docker layer cache
    • 번외) 중복된 의존성 제거(dedupe)
  • swcMinify를 통한 빌드속도 최적화
  • 결론

다음과 같은 목차로 알아볼것입니다.

원인 분석

현재 jenkins는 크게 다음과 같은 3개의 과정으로 돌아가고 있었습니다.

  • yarn install (in docker build)
  • yarn build (in docker build)
  • docker push

이 각각을 분석했을때 docker push가 가장 큰시간을, 그 다음 yarn install, 다음 yarn build순으로 시간을 많이 잡아먹었습니다.

docker🐳 push가 느려요

3개의 원인중에서도 docker push가 압도적으로 느리게 동작을 했었는데 그 원인은 바로 docker image의 크기였습니다.

docker build를 했을때 나오는 docker image가 4.07gb였기때문이죠.

docker push가 빠르게 돌아가면 오히려 이상할 정도의 사이즈죠. 이는 물론 서버 메모리에도 굉장히 악영향을 줍니다.

yarn install이 느려요

기존에 사용하는 yarn (v1) 을 이용하여 install할때 속도가 꽤 느렸습니다. 3~4 분정도는 이녀석이 잡아먹었기 때문이죠.

yarn install에 대해 제가 내린 문제상황은 3개였습니다.

  • 패키지 매니저의 한계 (yarn v1 -> yarn berry, pnpm)
  • 캐시가 적용되지않음
  • 불필요한 패키지가 존재하지않는가

build 속도가 느려요.

사실 프로젝트를 빌드하는속도는 크게 느리지않았습니다. 위의 2개의 비해서는 말이죠.

그야 대부분의 것은 nextjs가 최적화해주기때문에 정말 불필요한 패키지가 많이들어있지 않는이상 큰 문제는 없었습니다.

오늘은 미약하게라도 최적화를 시킬방법을 알아볼겁니다.

🐳 docker 이미지 줄이기

아까 docker push의 속도가 느린 docker image를 줄이는 방법에 대해서 알아보겠습니다.

먼저 불필요한 layer를 제거하는 과정이 필요합니다. 예를 들어 동일한 command를 연달아 이용하는 레이어가 있다면 그것을 하나의 커맨드와 줄띄움(\)과 이용할 수 있습니다.

그 다음 경량화된 패키지를 이용하는 것이 좋습니다. 여기서 말하는 패키지란 무엇일까요? javascript를 돌릴때는 node, python을 돌릴때는 pip를 이용하는것처럼 처음에 이러한 패키지를 dockerfile에서 받아주는데요.

일단 프론트엔드에서는 javscript를 이용하니 node를 이용할것입니다.

기존에 이용하던 node16의 크기는 ... 입니다. 하지만 node16의 경량화 버전인 node16-alpine을 이용한다면 ...까지 줄어들게 됩니다.

그리고 마지막으로 제목에도 적혀있듯이 가장 중요한 docker multi staging 입니다.

docker multi staging이란 실제 docker를 run시킬때 필요한 파일들만 마지막에 남길 수 있도록 하는 것입니다.

실제로 yarn install, yarn build 등등 여러가지 동작을 하지만 실제로 운영에 필요한것은 .next, yarn.lock, node_modules등등의 정적파일만 존재하면 됩니다.

그것을 위해 실제 필요한것만 처리하는 stage들을 나누는 전략이 docker multi staging입니다.


🐳 docker multi staging

staging을 나눌때 저는 크게 의존성 설치하는 deps, build를 하는 builder, 실제 실행하는 runner 이렇게 3개로 나누어서 이용합니다.

docker multi staging을 이용할때는 From ... As stage 문법과 --from={stage} 만 기억하시면 됩니다.

첫번째의 From ... As stage는 stage를 정의하는 것이며, 각각의 스테이지는 완전히 독립적인 환경을 지닙니다. 예를 들어서 deps stage에서 온갖 파일을 만들어낸다하더라도 builder stage에는 기존처럼 초기화되어있는 것이죠.

두번째의 --from={stage}는 초기화된 환경에서 지난 stage의 정적파일만을 가져올 수 있도록 합니다. 예를 들어 deps stage에서 yarn install을 하면 그때서야 node_modules가 생길것입니다. 그러면 builder stage에서는 yarn install을 따로 하지않고 node_modules만 가져올 수 있는것이죠.

다음은 docker multi staging을 적용한 간단한 예시입니다.


FROM node:16-alpine AS deps // deps stage -> 의존성을 설치
ARG PROFILE
ENV PROFILE=${PROFILE}
ARG VERSION=0.0.1
WORKDIR /web/src

COPY ["yarn.lock", "package.json", "./"]
COPY [".dockerignore", "lerna.json", ".gitignore", "./"]
COPY ./package.json ./package.json

RUN yarn install

FROM node:16-alpine As builder // build stage
WORKDIR /web/src
ARG PROFILE
ENV PROFILE=${PROFILE}
COPY --from=deps /web/src/package.json ./ //deps stage에서 필요한 파일을 가져온다.
COPY --from=deps /web/src/lerna.json ./lerna.json
COPY --from=deps /web/src/yarn.lock ./yarn.lock
COPY --from=deps /web/src/node_modules ./node_modules
COPY ./web.src ./

RUN yarn run build:${PROFILE}

FROM node:16-alpine As runner // docker run stage
WORKDIR /web/src
COPY --from=builder /web/src/lerna.json ./lerna.json // --from=builder에서 필요한 파일을 가져온다.
COPY --from=builder /web/src/yarn.lock ./yarn.lock
COPY --from=builder web/src/.next ./.next
COPY --from=builder web/src/public ./public
COPY --from=builder web/src/package.json ./package.json
COPY --from=builder web/src/node_modules ./node_modules

COPY  ./packages/docker/init.sh ./

CMD ["yarn", "start"]

최종 결과로 4.1GB였던 docker image가 660MB로 줄어들었고 docker push 시간 역기 30초 내외로 되었습니다.

🔑 package manager 선택하기

yarn install, 즉 의존성을 설치할때 최적화하는 방법중 하나인 package manager에 대해서 알아보겠습니다.

사실 package manager의 npm, yarn classic, yarn berry, pnpm 등을 모두 다루는 것은 이미 다른 게시물에도 많이 존재하기 때문에 본 게시물에서는 다루지 않겠습니다.

이번에는 yarn berryzero-install을 이용하여 빌드속도를 최적화하였는데요.

간단하게 yarn berry에 대해 설명입니다.

yarn berry

yarn v1을 yarn classic, yarn v2이상을 yarn berry라고 한다. 현재 yarn에서는 yarn classic에 대한 업데이트는 멈춘 상태이며 yarn berry에 대한 업데이트만을 진행합니다. (현재 yarn 3.3.1)
기존 yarn에서 yarn berry로 변경하려면 yarn set version berry 명령어를 이용하면 된다. 그런뒤 yarn install을 진행하면 node_modules대신에 pnp.cjs, pnp.loader.cjs, .yarn 이 생기게 됩니다.

yarn berry는 node_modules가 아닌 zip으로 패키지를 저장하기 때문에 훨씬 가볍고 install을 하는데 시간을 소비하지 않습니다.
그리고 yarn berry부터는 .yarnrc.yml 파일이 필수로 필요하기 때문에 로컬에서 이를 만들어준뒤 내부에 config를 설정해야만 합니다.

  • .yarnrc.yml
nodeLinker: pnp

packageExtensions:
  next@*:
    dependencies:
      react: "*"
      react-dom: "*"

yarnPath: .yarn/releases/yarn-3.3.1.cjs

다음은 yarn berry를 이용하기 위한 .yarnrc.yml 파일입니다. 가장 중요한 3개의 옵션을 따로 뽑아 왔습니다.

nodeLinker는 node_modules, pnp, pnpm 이렇게 3개의 모드가 존재하며 node_modules는 기존 yarn classic과 같으며, pnpzero install을 지원하며 node_modules대신 .yarn과 pnp.cjs를 통하여 의존성을 확인합니다., pnpm은 pnpm패키지 매니저와 동일하게 동작을 합니다.

여기서 zero-install을 이용하기 위해 저는 pnp모드를 이용했습니다.

packageExtensions는 yarn berry에게 의존성을 알려주는 역할입니다. 만약 yarn install을 했을때 A require … provides B 에러가 나온다면 packageExtensions에 다음과 같은 형식으로 넣어 주어야 합니다.

yarn berry를 포기한 이유

이렇게 yarn berry를 통하여 zero-install을 이용하려 했지만 yarn berry는 다양한 문제가 있었습니다. 사실 대부분 yarn berry를 이용하는 곳에서 어느정도 한계를 느끼고 pnpm으로 전환을 많이 하시는것 같았습니다.

ex) https://engineering.ab180.co/stories/yarn-to-pnpm

저 역시 특정 라이브러리를 이용하려할때 yarn berry와의 호환이 잘 맞지 않은 문제가 존재하였고 pnp모드를 이용하면서 생기는 .yarn/cache폴더의 비대함 문제와 의존성이 변경되면 .yarn/cache내부의 zip파일들이 모두 file changes로 되어 코드리뷰가 힘들어 진다는 점에서 yarn berry를 포기하였습니다.

최종적으로는 pnpm을 이용하고 있으며 node_modules를 동일하게 이용하지만 yarn classic 사용할때 보다 빌드 시간이 1분가량 줄어든것을 확인할 수 있었습니다.

번외) 🐳 docker layer cache

install속도를 최적화한다기보다 docker build를 할때 의존성의 변경이 없으면 따로 install을 하지 않도록 하는 docker layer cache에 대해서 알아보겠습니다.

실제로 docker는 이전 빌드결과물과 내용이 동일한것을 COPY하면 cache를 이용하여 그 과정을 진행하지 않습니다.

그래서 위의 Dockerfile을 보시면 COPY ./web/src ./ 이렇게 통째로 복사하면 될것을 COPY ./web/src/package.json ./package.json 와 같이 따로 쪼개서 COPY를 하고 있습니다.

그 이유는 COPY ./web/src ./ 와 같이 통째로 COPY를 하면 무조건 안의 내용이 변하기 때문에 docker layer cache가 동작하지 않습니다.

하지만 COPY ./web/src/package.json ./package.json 로 쪼개서 이용을 하면 package.json만의 변경을 확인하기때문에 의존성에 대한 docker layer cache를 이용할 수 있습니다.

번외) 중복된 의존성 제거하기 (dedupe)

yarn berry의 yarn dedupe, pnpm의 pnpm dedupe등 최신 패키지 매니저에는 dedupe라는 명령어로 중복으로 설치된 불필요한 패키지들을 제거해줍니다.

의존성 자체가 가벼워지기 때문에 의존성 설치 뿐만 아니라 후의 build를 하는데에도 최적화 효과를 볼 수 있습니다.

🛠 swcMinify를 통한 빌드속도 최적화 (Next.js)

사실 이 부분은 아까도 말씀드렸듯이 별것 없습니다. Nextjs에서 대부분 최적화를 진행해주기 때문이죠. 그래서 next.config.js의 옵션을 이용하여 약간이나마 최적화가 가능합니다.

Nextjs 12.3.0 버전부터 정식 지원하는 swc 컴파일러를 이용하면 되는데요. 이 옵션은 기존 prodution 에 정적파일을 내보낼때는 번들사이즈를 줄이기 위해 terser라는 것으로 minify를 진행하는데요. 이 terser는 javascript로 작성되어있기 때문에 컴파일 속도가 느립니다.

하지만 swc라는 컴파일러를 이용할 경우에는 low level의 rust언어를 이용하기 때문에 javascript보다 훨씬 빠른 컴파일 속도를 자랑합니다.

설정하는법은 매우 간편한데요. 단지 next.config.jsswcMinify: true로 설정하시면 됩니다.


만약 Nextjs가 아니라 직접 webpack을 이용해서 구성하신다면 다음 링크를 보시면 됩니다.

webpack 빌드속도 최적화 - 1
webpack 빌드속도 최적화 - 2

결론

간단하게 빌드속도 최적화 하는 방법에 대해서 알아봤습니다. 물론 이 부분은 현재 작업하고 있는 환경에 따라 최적화 방법이 달라질텐데요.

오늘은 Nextjs, pnpm, docker, jenkins 를 이용하는 프로젝트 설정일 경우에 최적화 할 수 있는 방법에 대해서 알아봤습니다.

만약 github action을 이용한다면 docker layer cache가 작동하지 않기 때문에 docker/build-push-action을 이용해야 되는등의 추가옵션이 존재할 수 있습니다.

마지막으로 기술스택에 제약받지 않는 프론트엔드에서의 빌드속도 최적화 방법을 원하신다면 다음과 같은 흐름으로 찾아보시면 수월합니다.

  • 일단 느린 문제원인을 정확히 파악하자
  • 빌드속도 최적화의 가장 핵심중 하나는 캐시다. 굳이 다시 해도되지 않을 과정일것 같다면 캐시가 가능할지 부터 생각해보자.
  • js로 처리하여 느린것들에 대한것들은 swc로 대체되어있는 방법이 있나 알아보자. terser -> swcMinify, ts-jest -> @swc/jest 생각보다 swc로 최적화 되어있는 패키지들이 존재한다.
  • webpack을 사용하고 있다면 다른 모듈 번들러 고민도 해보자. 최근에는 vite, esbuild, turbopack(실험)등등 많은 것들이 나오고 있다.
profile
딩구르르

3개의 댓글

comment-user-thumbnail
2023년 6월 9일
답글 달기
comment-user-thumbnail
2023년 6월 13일

Thank you for this awesome comment for learning more about https://takesurvey.onl/kfc-allergen-menu/

답글 달기
comment-user-thumbnail
2023년 11월 22일

A survey website called https://italktostopandshop.com/ seeks to understand what customers think of the products and services that the company offers. You simply need to complete and submit the Stop and Shop Feedback Survey as a regular and devoted customer to claim your thrilling rewards.

답글 달기