블로그

turborepo는 가지치기가 필요해

서론요즘 핫하고 핫한 모노레포 빌드 시스템이 있습니다. 이름하야 터보레포(turborepo), 무려 Nextjs를 만들고 있는 Vercel에서 제작했습니다.Nextjs를 무진장 사랑하는 저는 반드시 찍어 먹어봐야 되겠다고 생각을 했습니다. 그래서 간단한 앱을 만들어 빌드까지 해보기로 했죠!오늘의 주제는 바로 그 빌드와 관련 있습니다. 재밌는 부분을 찾았거든요!간단하게 말씀드리자면, 터보레포는 가지치기가 필요합니다. 엥?도커는 빠른 최적화를 원해요갑자기 가지치기라니, 이게 무슨 말일까요? 터보레포가 드디어 그 사악한 속을 드러내고 우리의 소중한 코드를 잘라내 버리겠다는 의미일까요? 다행히도 그건 아닙니다.터보레포의 가지치기에 대해 말하기에 앞서 우리는 도커가 어떻게 nodejs 환경에서 이미지를 만들어 내는지 살펴봐야 합니다. => [separator 2/4] RUN pnpm add turbo 17.1s => [installer 2/5] COPY .gitignore .gitignore 0.0s => [separator 3/4] COPY . . 0.4s => [separator 4/4] RUN pnpm turbo prune --scope=api --docker 1.0s => [installer 3/5] COPY --from=separator /app/out/json/ . 0.0s => [installer 4/5] COPY --from=separator /app/out/pnpm-lock.yaml ./pnpm-lock.yaml 0.0s => [installer 5/5] RUN pnpm install 16.1s => [builder 2/4] COPY --from=installer /app . 6.5s => [builder 3/4] COPY --from=separator /app/out/full . 0.0s => [builder 4/4] RUN pnpm turbo run build --filter=api 7.2s => [runner 2/2] COPY --from=builder /app . 7.5s => CACHED [separator 2/4] RUN pnpm add turbo 0.0s => CACHED [separator 3/4] COPY . . 0.0s => CACHED [separator 4/4] RUN pnpm turbo prune --scope=api --docker 0.0s => CACHED [installer 3/5] COPY --from=separator /app/out/json/ . 0.0s => CACHED [installer 4/5] COPY --from=separator /app/out/pnpm-lock.yaml ./pnpm-lock.yaml 0.0s => CACHED [installer 5/5] RUN pnpm install 0.0s => CACHED [builder 2/4] COPY --from=installer /app . 0.0s => CACHED [builder 3/4] COPY --from=separator /app/out/full . 0.0s => CACHED [builder 4/4] RUN pnpm turbo run build --filter=api 0.0s => CACHED [runner 2/2] COPY --from=builder /app . 0.0s도커는 package.json와 락파일(lockfile)을 전의 빌드와 비교해 변화가 있을 때 비로소 패키지를 설치합니다. 변화가 없다면 이전의 캐시된 결과를 그대로 가져다 쓰는 것이죠. 위의 로그가 그 예시 중 하나입니다.첫번째는 package.json이 변경된 후 빌드된 이미지의 로그입니다. 옆의 시간을 확인해 보세요. 두번째는 첫번째 빌드 이후 package.json을 변경하지 않고 빌드한 이미지의 로그입니다. CACHED라고 나와 있는 것이 보이나요? 다시 한 번 시간을 확인해 보세요. 0.0s, 압도적 시간! 바로 도커가 이 전에 캐시된 것을 그대로 가져와 사용했기 때문에 가능했던 일입니다.천방지축 어리둥절 빙글빙글 돌아가는 터포레포 디펜던시이 좋은 방법을 터포레포에서도 사용해야 되겠죠. 그런데요, 문제가 하나 있습니다. 터보레포는 바로 모노레포 빌드 시스템이라는 겁니다!모노레포, 즉 터보레포는 락파일을 전역으로 관리합니다. A, B 워크스페이스가 있을 때 B 워크스페이스에서 디펜던시가 변경된다면 그 사항이 그대로 락파일에 반영되는 것이죠. 이 때 디펜던시가 변경되지 않은 A 워크스페이스를 도커로 빌드한다면요? 아차차, 도커는 락파일이 변경된 것을 보고 새로 디펜던시들을 설치하기 시작합니다. 락파일! 락파일이 도커를 완벽하게 속여 먹인 겁니다!대체 이 문제를 어떻게 해결해야 할까요, 맞습니다. 가지치기가 등장할 시간입니다.가지치기된 모노레포터포레포도 진작에 이 문제를 파악하고 있었습니다:a. 터포레포는 락파일을 전역으로 관리하기 때문에 효율적인 도커 빌드가 불가능하다b. 그럼 빌드할 워크스페이스만 똑때서 락파일을 만든다음 도커에게 주면 되는 거 아닌가a. 헐b. ?이렇게 turbo prune가 탄생합니다. 그 이름에서도 보여지듯이 이 명령어의 기능은 간단합니다. 바로 특정 워크스페이스의 가지치기된 모노레포를 만들어 주는 것이죠!. ├── apps/ │ ├── a-workspace/ // example-package-1만 사용 │ │ ├── package.json │ │ └── src/ │ └── b-workspace/ // example-package-1, 2 둘 다 사용 │ ├── package.json │ └── src/ ├── packages/ │ ├── example-package-1/ │ │ ├── index.ts │ │ └── package.json │ └── example-package-2/ │ ├── index.ts │ └── package.json └── pnpm-lock.yaml예제 앱의 디렉토리 구조입니다. a-workspace는 example-package-1 패키지만 사용하고 있고 a-workspace는 1, 2 둘 다 사용하고 있네요. 그렇다면 turbo prune을 사용해 a-workspace만 빼내봅시다.turbo prune --scope="a-workspace"이 명령어를 실행한다면:. ├── apps/ │ └── a-workspaces/ // example-package-1만 사용 │ ├── package.json │ └── src/ ├── packages/ │ └── example-package-1/ │ ├── index.ts │ └── package.json └── pnpm-lock.yamlA 워크스페이스의 가지치기된 모노레포를 만들어 줍니다. 물론 가지치기된 락파일도 같이요! 이 락파일은 오직 A 워크스페이스의 디펜던시만을 담고 있어 B 워크스페이스의 디펜던시가 변경된다고 하더라도 영향을 받지 않습니다.이 가지치기된 모노레포를 도커로 빌드하게 된다면 더 이상 락파일에 머리를 감싸매지 않아도 됩니다!TMI도움이 됐나요?다행입니다! 오늘도 좋은 하루 보내세요!References[Best practices for writing Dockerfiles | Docker Docs](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#use-multi-stage-builds)[\`turbo prune\` – Turborepo](https://turbo.build/repo/docs/reference/command-line-reference/prune)[Deploying with Docker – Turborepo](https://turbo.build/repo/docs/handbook/deploying-with-docker)

프론트엔드turborepodocker

turbo prune는 도커하고 친해지고 싶어

서론turbo prune이 다시 돌아왔습니다! 저번에는 이 명령어가 터보레포의 특정 워크스페이스를 도커 이미지로 빌드할 때 왜 필요한지, 또한 기본적으로 어떻게 동작하는지 확인했습니다. 거기에 더해 하나를 더 알아볼 겁니다. 기존의 방법으로도 도커 이미지를 빌드하기에는 충분하지만 완벽하지는 않습니다. 해결하지 못한 작은 문제가 하나 있거든요.문제점질문입니다! 만약, 도커 이미지의 빌드에 사용될 파일들 중에 디펜던시와 관련이 없는 것이 변경된다면 어떻게 될까요? 이런 상황에서 도커는 기존의 캐시를 활용하지 않습니다! 디펜던시가 변하지 않았어도 계속 새로 설치가 된다는 괴담을 맞이하게 되는 겁니다. 정말 무서운 일입니다.이 질문은 turbo prune으로 생성된 가지치기된 모노레포를 그대로 가져와 빌드할 때 생기는 문제와도 이어져 있습니다. 사실 그 상황이 그대로 발생합니다! 해당 모노레포에 해당하는 워크스페이스의 파일이 하나라도 추가 혹은 변경된다면 디펜던시는 언제나 다시 처음부터 설치됩니다... 으아악!!해결방법?모든 문제는 디펜던시와 관련 없는 파일이 수정될 때 도커가 이를 감지하고 캐시를 깨트리면서 생겼습니다. 그렇다면 도커가 캐시를 깨트리지 않게 만들면 되겠군요! 아예 디펜던시와 관련있는 파일만 모아 설치를 진행하면 모든 문제가 해결될 것만 같습니다.도커에서 이런 일을 더욱 쉽게 만들어주는 기능이 무엇일까요? 바로 스테이지입니다! 각각의 스테이지는 독립된 캐시를 가지고, 파일을 입맛에 맞게 골라 사용할 수 있으니까요.그렇다면 우리는 디펜던시를 설치하는 스테이지와 빌드하는 스테이지를 나누고 설치 스테이지에 디펜던시와 관련있는 파일만 포함시켜야 합니다. 즉, 모노레포에서 락파일과 package.json만 쏙 빼서 설치 스테이지에 넣어주는 과정을 거쳐야 하죠. 이제 해결 방법에 좀 더 가까워진 느낌이 듭니다! 하지만 머릿 속 한켠에 "완전 귀찮아!" 같은 생각이 무럭무럭 피어오르지는 않나요..? 경험담입니다.괜찮습니다. 여러분들은 터보레포를 사용하고 있으니까요!--docker역시나! 터보레포는 이 문제도 이미 해결할 수단을 마련해 놓았습니다. --docker 옵션이 바로 그 것입니다.turbo prune --scope="a-workspace"이전에 우리는 다음과 같은 명령어로 가지치기된 모노레포를 만들었습니다. 그 모노레포의 구조는 다음과 같습니다:. ├── apps/ │ └── a-workspaces/ // example-package-1만 사용 │ ├── package.json │ └── src/ ├── packages/ │ └── example-package-1/ │ ├── index.ts │ └── package.json └── pnpm-lock.yaml언제나 보던 익숙한 구조입니다. 그렇다면 이번에는 --docker 옵션을 사용해 봅시다.turbo prune --scope="a-workspace" --docker기존의 명령어의 뒤에 --docker를 적어주면 됩니다. 이 것을 통해 만들어진 모노레포의 구조는 다음과 같습니다:. ├── full │ ├── apps/ │ │ └── a-workspaces/ // example-package-1만 사용 │ │ ├── package.json │ │ └── src/ │ └── packages/ │ └── example-package-1/ │ ├── index.ts │ └── package.json ├── json │ ├── apps/ │ │ └── a-workspaces/ │ │ └── package.json │ └── packages/ │ └── example-package-1/ │ └── package.json └── pnpm-lock.yaml전의 구조와 무언가 달라보입니다. 우리가 살펴봐야 할 부분은 나눠져 있는 full, json 디렉토리의 용도입니다.json 디렉토리는 이 옵션의 핵심입니다. full 디렉토리와 비교해 보세요. 무언가 빠진 부분이 있지 않나요? json 디렉토리는 바로 full 디렉토리에서 오직 package.json만을 가져와 만들어졌습니다. 그렇습니다, 바로 이겁니다! 우리가 원하던 거에요!--docker로 분리된 디렉토리 덕분에 설치, 빌드 스테이지를 분리해 줄 수 있습니다. 설치 스테이지에서는 락파일과 json 디렉토리를 가져와 오직 디펜던시의 설치만, 빌드 스테이지에서는 설치 스테이지에서 설치된 디펜던시들과 full 디렉토리에서 가져온 소스 파일들을 가져와 빌드만 하게 만드는 것이죠.이제 설치 스테이지는 디펜던시 설치에 반드시 필요한 락파일과 package.json만을 가질 수 있게 되었습니다. 더 이상 설치 스테이지에서 디펜던시와 관련되지 않은 다른 파일의 변경으로 캐시가 깨지는 일은 생기지 않습니다. 문제 해결입니다!TMI도움이 됐나요?다행입니다! 해피 해킹!References[\`turbo prune\` – Turborepo](https://turbo.build/repo/docs/reference/command-line-reference/prune)[Deploying with Docker – Turborepo](https://turbo.build/repo/docs/handbook/deploying-with-docker)[Optimizing builds with cache management | Docker Docs](https://docs.docker.com/build/cache/)

프론트엔드turborepo