Turborepo 0.4.0

Jared Palmer
Name
Jared Palmer
X
@jaredpalmer

Turborepo v0.4.0 릴리스를 발표하게 되어 기쁩니다!

Go로 다시 작성됨

처음에는 TypeScript로 turbo의 프로토타입을 만들었지만, 로드맵의 특정 항목이 더 나은 성능을 요구한다는 것이 분명해졌습니다. 약 한 달간의 작업 끝에 드디어 Go 버전의 turbo CLI를 릴리스하게 되어 기쁩니다. 밀리초 단위로 부팅될 뿐만 아니라 새로운 Go 구현은 Node.js 구현보다 해싱에서 10배에서 100배 사이로 빠릅니다. 이 새로운 기반(그리고 곧 읽게 될 일부 기능)으로 Turborepo는 이제 Go의 놀라운 동시성 제어 덕분에 초은하계 크기의 프로젝트로 확장하면서도 놀랍도록 빠른 속도를 유지할 수 있습니다.

더 나은 해싱

v0.4.0에서는 해싱이 더 빠를 뿐만 아니라 훨씬 더 스마트합니다.

주요 변경 사항은 turbo가 더 이상 해셔(주어진 작업이 캐시에 존재하는지 또는 실행해야 하는지 판단하는 알고리즘)에 루트 잠금 파일 내용의 해시를 포함하지 않는다는 것입니다. 대신 turbo는 이제 루트 잠금 파일을 기반으로 패키지의 dependenciesdevDependencies의 해결된 버전 집합을 해시합니다.

이전 동작은 루트 잠금 파일이 어떤 방식으로든 변경될 때마다 캐시를 폭발시켰습니다. 이 새로운 동작으로 잠금 파일을 변경하면 추가/변경/제거된 종속성의 영향을 받는 패키지에 대해서만 캐시가 무효화됩니다. 복잡하게 들리지만, npm에서 종속성을 설치/제거/업데이트할 때 변경 사항의 영향을 실제로 받는 패키지만 다시 빌드하면 된다는 의미입니다.

실험적: 정리된 워크스페이스

고객의 가장 큰 문제점/요청 중 하나는 대규모 Yarn Workspaces(또는 실제로 모든 워크스페이스 구현)로 작업할 때 Docker 빌드 시간을 개선하는 것이었습니다. 핵심 문제는 워크스페이스의 최고 기능--모노레포를 단일 잠금 파일로 줄이는 것--이 Docker 레이어 캐싱과 관련하여 최악이기도 하다는 것입니다.

문제와 turbo가 이제 이를 해결하는 방법을 설명하기 위해 예를 살펴보겠습니다.

frontend, admin, ui, backend라는 패키지 세트를 포함하는 Yarn 워크스페이스가 있는 모노레포가 있다고 가정해 봅시다. 또한 frontendadmin이 동일한 내부 React 컴포넌트 라이브러리 패키지 ui에 모두 종속되는 Next.js 애플리케이션이라고 가정해 봅시다. 이제 backend에 모노레포의 다른 부분과 많은 코드를 공유하지 않는 Express TypeScript REST API가 포함되어 있다고 가정해 봅시다.

frontend Next.js 앱의 Dockerfile은 다음과 같습니다:

Dockerfile
FROM node:alpine AS base
RUN apk update
WORKDIR /app
 
# Add lockfile and package.jsons
FROM base AS builder
COPY *.json yarn.lock ./
COPY packages/ui/*.json ./packages/ui/
COPY packages/frontend/*.json ./packages/frontend/
RUN yarn install
 
# Copy source files
COPY packages/ui/ ./packages/ui/
COPY packages/frontend/ ./packages/frontend/
 
# Build
RUN yarn --cwd=packages/ui/ build
RUN yarn --cwd=packages/frontend/ build
 
# Start the Frontend Next.js application
EXPOSE 3000
RUN ['yarn', '--cwd', 'packages/frontend', 'start']

이것이 작동하는 동안 훨씬 더 나을 수 있는 몇 가지가 있습니다:

이 마지막 문제는 모노레포가 커질수록 특히 고통스러운데, 이 잠금 파일의 모든 변경이 앱이 실제로 새로운/변경된 종속성의 영향을 받는지 여부와 관계없이 거의 전체 재빌드를 트리거하기 때문입니다.

....지금까지는.

완전히 새로운 turbo prune 명령으로 이제 node_modules를 설치하지 않고도 대상 패키지에 대한 정리된 잠금 파일이 있는 희소/부분 모노레포를 결정적으로 생성하여 이 악몽을 해결할 수 있습니다.

Docker 내부에서 turbo prune을 사용하는 방법을 살펴보겠습니다.

Dockerfile
FROM node:alpine AS base
RUN apk update && apk add git
 
## Globally install `turbo`
RUN npm i -g turbo
 
# Prune the workspace for the `frontend` app
FROM base as pruner
WORKDIR /app
COPY . .
RUN turbo prune frontend --docker
 
# Add pruned lockfile and package.json's of the pruned subworkspace
FROM base AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/yarn.lock ./yarn.lock
# Install only the deps needed to build the target
RUN yarn install
 
# Copy source code of pruned subworkspace and build
FROM base AS builder
WORKDIR /app
COPY --from=pruner /app/.git ./.git
COPY --from=pruner /app/out/full/ .
COPY --from=installer /app/ .
RUN turbo run build frontend
 
# Start the app
FROM builder as runner
EXPOSE 3000
RUN ['yarn', '--cwd', 'packages/frontend', 'start']

그렇다면 turbo prune의 출력은 정확히 무엇일까요? 내부에 다음이 있는 out이라는 폴더입니다:

위의 덕분에 Docker는 이제 실제 이유가 있을 때만 각 애플리케이션을 다시 빌드하도록 설정할 수 있습니다. 따라서 frontend는 소스 또는 종속성(npm의 내부 또는 외부)이 실제로 변경된 경우에만 다시 빌드됩니다. adminbackend도 마찬가지입니다. ui의 소스 코드 또는 종속성에 대한 변경은 frontendadmin 모두의 재빌드를 트리거하지만 backend트리거하지 않습니다.

이 예제가 사소해 보이지만 각 앱을 빌드하고 배포하는 데 최대 20분이 걸린다고 상상해 보세요. 특히 대규모 팀에서 이러한 절약은 빠르게 누적되기 시작합니다.

파이프라인

Turborepo를 더욱 세밀하게 제어할 수 있도록 turbo의 구성에 pipeline을 추가했습니다. turbo 구성의 이 새 필드를 사용하면 모노레포의 npm 스크립트가 서로 어떻게 관련되어 있는지 및 일부 추가 작업별 옵션을 지정할 수 있습니다. 그런 다음 turbo는 이 정보를 사용하여 모노레포의 작업을 최적으로 예약하고 그렇지 않으면 존재할 폭포를 축소합니다.

작동 방식은 다음과 같습니다:

./package.json
{
  "turbo": {
    "pipeline": {
      "build": {
        // This `^` tells `turbo` that this pipeline target relies on a topological target being completed.
        // In english, this reads as: "this package's `build` command depends on its dependencies' or
        // devDependencies' `build` command being completed"
        "dependsOn": ["^build"]
      },
      "test": {
        //  `dependsOn` without `^` can be used to express the relationships between tasks at the package level.
        // In English, this reads as: "this package's `test` command depends on its `lint` and `build` command first being completed"
        "dependsOn": ["lint", "build"]
      },
      "lint": {},
      "dev": {}
    }
  }
}

위의 구성은 turbo에 의해 해석되어 실행을 최적으로 예약합니다.

실제로 무슨 의미일까요? 과거(Lerna 및 Nx와 같이)에는 turbo가 토폴로지 순서로만 작업을 실행할 수 있었습니다. 파이프라인을 추가하면 turbo는 이제 실제 종속성 그래프 외에도 토폴로지 "작업" 그래프를 구성하여 최대 동시성으로 작업을 실행해야 하는 순서를 결정하는 데 사용합니다. 최종 결과는 더 이상 작업이 완료되기를 기다리며 유휴 CPU 시간을 낭비하지 않는다는 것입니다(즉, 더 이상 폭포가 없습니다).

Turborepo scheduler

개선된 캐시 제어

pipeline 덕분에 이제 작업별로 turbo의 캐시 동작을 열 수 있는 좋은 장소가 생겼습니다.

위의 예를 기반으로 이제 다음과 같이 전체 모노레포에서 캐시 출력 규칙을 설정할 수 있습니다:

./package.json
{
  "turbo": {
    "pipeline": {
      "build": {
        // Cache anything in dist or .next directories emitted by a `build` command
        "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
        "dependsOn": ["^build"]
      },
      "test": {
        // Cache the test coverage report
        "outputs": ["coverage/**"],
        "dependsOn": ["lint", "build"]
      },
      "dev": {
        // Never cache the `dev` command
        "cache": false
      },
      "lint": {},
    }
  }
}

참고: 현재 pipeline은 프로젝트 수준에 존재하지만, 이후 릴리스에서는 패키지별로 재정의할 수 있습니다.

다음은 무엇인가요?

많은 내용이었지만 앞으로 더 많은 것이 있습니다. Turborepo 로드맵의 다음 내용은 다음과 같습니다.

크레딧