태그
목차

태스크 의존성

생성일: 2024-01-18

수정일: 2024-01-18

터보레포(Turborepo)는 태스크(tasks)들이 서로 어떻게 연관되어 있는지 표현할 때 가장 강력하다. 이러한 관계를 "종속성"이라고 부르지만, 패키지 종속성은 package.json 파일에서 설치하는 패키지 종속성과는 다르다. 터보레포는 워크스페이스(workspaces)를 인식하지만, turbo.json에 명시적으로 dependOn 하지 않는 한 태스크 간의 관계를 자동으로 그리지는 않는다.

한 태스크가 다른 태스크에 종속되도록 만드는 방법에 대한 몇 가지 일반적인 패턴을 살펴보자.

동일한 워크스페이스에서

다른 태스크보다 먼저 실행해야 하는 태스크이 있을 수 있다. 예를 들어 배포 전에 빌드를 실행해야 할 수도 있다.

두 태스크가 동일한 워크스페이스에 있는 경우 다음과 같이 관계를 지정할 수 있다:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {},
    "deploy": {
      // 워크스페이스의 `deploy` 태스크는 같은 워크스페이스의 `build` 태스크에 의존한다.
      "dependsOn": ["build"]
    }
  }
}

즉, turbo deploy 가 실행될 때마다 build 도 동일한 워크스페이스 내에서 실행된다.

의존하는 워크스페이스에서

모노레포의 일반적인 패턴은 워크스페이스의 build 태스크가 종속된 모든 워크스페이스의 build 태스크의 완료된 후에만 실행되도록 선언하는 것이다.

태스크 종속성 vs 워크스페이스 종속성

이는 서로 다른 개념인 태스크 종속성(task dependencies)과 워크스페이스 종속성(workspace dependencies)을 모두 지칭하기 때문에 혼동될 수 있다. 워크스페이스 종속성은 package.jsondependenciesdevDependencies 인 반면, 태스크 종속성은 turbo.jsondependsOn 키를 말한다.

^ 기호("캐럿"이라고 함)는 태스크가 종속된 워크스페이스의 태스크에 종속되어 있음을 명시적으로 선언한다.

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      // 워크스페이스의 `build` 명령어는 해당 워크스페이스의 `dependencies` 와 `devDependencies`의 `build` 명령에 의존한다.
      "dependsOn": ["^build"]
    }
  }
}

위의 구성을 사용하면 앱에 다른 워크스페이스 패키지를 설치하는 경우 설치된 패키지의 build 스크립트가 항상 앱의 build 스크립트보다 먼저 실행된다.

임의의 워크스페이스에서

때로는 한 워크스페이스 태스크가 다른 워크스페이스 태스크에 종속되기를 원할 수도 있다. 이는 기본적으로 태스크가 별도의 단계로 실행되는 lerna 또는 rush 에서 마이그레이션하는 레포지토리에 특히 유용할 수 있다. 때때로 이러한 구성은 위에서 본 것처럼 단순한 pipeline 구성으로는 표현할 수 없는 가정을 전제로 한다. 또는 CI/CD에서 turbo 를 사용할 때 애플리케이션 또는 마이크로서비스 간의 작업 시퀀스를 표현하고 싶을 수도 있다.

이러한 경우 pipeline 구성에서 <workspace>#<task> 구문을 사용하여 이러한 관계를 표현할 수 있다. 아래 예제에서는 백엔드의 deployhealth-check 스크립트에 종속되는 프론트엔드 애플리케이션의 deploy 스크립트와 ui 워크스페이스의 test 스크립트에 대해 설명한다:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    // 워크스페이스-태스크가 의존하는 다른 워크스페이스-태스크를 명시적으로 선언한다.
    "frontend#deploy": {
      "dependsOn": ["ui#test", "backend#deploy", "backend#health-check"]
    }
  }
}

frontend#deploy 에 대한 위와 같은 설정은 testdeploy 태스크 설정과 충돌하는 것처럼 보일 수 있지만 그렇지 않다. testdeploy는 다른 워크스페이스(예: ^<task>)에 대한 종속성이 없으므로 해당 워크스페이스의 buildtest 스크립트가 완료된 후 언제든지 실행할 수 있다.

Warning

  1. <workspace>#<task> 구문은 유용한 이스케이프 해치이지만, 일반적으로 빌드-시간 종속성보다는 상태 확인(health checks)과 같은 배포 오케스트레이션 작업에 사용하는 것이 좋으므로 터보레포가 이러한 작업을 보다 효율적으로 최적화할 수 있다.
  2. Workspace-tasks는 캐시 구성을 상속하지 않는다. 현 시점에서 outputs을 다시 선언해야 한다.
  3. <workspace>는 워크스페이스의 package.json에 있는 name 키와 일치해야 하며, 그렇지 않으면 태스크가 무시된다.

종속성 없음

종속성 목록이 비어 있으면(dependsOn이 정의되지 않았거나 []) 이 태스크 전에 아무것도 실행할 필요가 없다는 뜻이다! 즉 종속성이 없다.

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    // 워크스페이스의 `lint` 명령은 종속성이 없으며 언제든지 실행할 수 있다.
    "lint": {}
  }
}

태스크 외부의 종속성

두 개의 앱, 즉 docsweb 에서 사용하는 공통 ui 패키지가 있다고 가정해 보자.

apps/
  docs/package.json # Depends on ui
  web/package.json  # Depends on ui
packages/
  ui/package.json   # No workspace dependencies
turbo.json
package.json

워크스페이스에 타입스크립트로 코드를 작성했으면 이제 tsc를 실행하여 타입을 확인할 차례다. 여기에는 두 가지 요구 사항이 있다:

이를 위해 그래프에 가짜 재귀 태스크를 생성하고 이에 의존한다:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "topo": {
      "dependsOn": ["^topo"]
    },
    "typecheck": {
      "dependsOn": ["topo"]
    }
  }
}

topo 태스크는 스크립트에 존재하지 않으므로 터보레포는 태스크를 "즉시" 완료한 다음 해당 워크스페이스에 종속되어 있던 모든 워크스페이스를 찾는다. 따라서 태스크 그래프에서 다른 워크스페이스와의 관계를 이해하면서 태스크가 병렬로 실행된다.

왜 이 방법이 효과가 있을까?

요구 사항을 거의 충족하는 파이프라인을 살펴봄으로써 이것이 작동하는 이유를 더 깊이 이해할 수 있다.

다음과 같이 작업 정의에서 dependsOn을 생략하면 태스크의 병렬 처리를 달성할 수 있다:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "typecheck": {} // 이것으로는 충분하지 않다!
  }
}

typecheck 태스크는 성공적으로 병렬로 실행되지만 워크스페이스 종속성에 대해서는 알 수 없다!

다음 단계를 통해 이를 시연해 볼 수 있다:

  1. turbo typecheck 실행
  2. ui 패키지에서 일부 소스 코드 변경
  3. turbo typecheck --filter=web 실행

이렇게 하면 3단계에서 캐시 히트에 도달하게 되지만 그렇게 하면 안 된다! web 워크스페이스에서 ui 패키지 코드의 변경으로 인해 유형 오류가 발생했을 수 있다. 3단계의 캐시 히트는 잘못된 것이므로 타입 오류를 발견할 수 없다.

이 문제를 해결하려면 build 태스크에서와 마찬가지로 토폴로지 종속성 그래프에 직접 종속되도록 선택할 수 있다:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "typecheck": {
      "dependsOn": ["^typecheck"] // 이것으로도 충분하지 않다!
    }
  }
}

이제 올바른 캐시 동작이 적용되어 ui 코드가 변경되면 web 에서 캐시가 miss 된다. 하지만 파이프라인을 매우 빠르게 실행할 수 있게 해주던 병렬 처리 기능이 사라졌다. 이제 ui 워크스페이스의 typecheck 태스크는 web 태스크가 시작되기 전에 완료되어야 한다.

종속된 워크스페이스에서 typecheck 명령을 훨씬 더 빨리 시작하여 "즉시 완료"되는 ui 태스크에 의존할 수 있다면 어떨까?

이것이 바로 '가짜' topo 태스크가 필요한 이유다:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "topo": {
      "dependsOn": ["^topo"]
    },
    "typecheck": {
      "dependsOn": ["topo"]
    }
  }
}

이 파이프라인에서는 topo라는 "synthetic" 태스크를 선언한다. package.json 파일에 topo 스크립트가 없으므로 turbo typecheck 파이프라인은 모든 typecheck 스크립트를 병렬로 실행하여 첫 번째 요구 사항을 충족한다.

하지만 이 topo 태스크는 web에서 ui로, 그리고 docs에서 ui로 "synthetic" 워크스페이스 종속성을 생성한다. 즉, ui에서 코드를 변경하면 webdocs의 워크스페이스에 대한 캐시 Miss가 발생하여 두 번째 요구 사항을 충족하게 된다.

파이프라인은 typechecktopo 태스크에 종속되고 topo^topo에 종속된다고 선언한다. 즉, 동일한 워크스페이스에서 topo 태스크가 모든 typecheck 태스크보다 먼저 실행되어야 하고 모든 패키지 종속성의 topo 태스크가 topo 태스크 자체보다 먼저 실행되어야 한다는 뜻이다.

typecheck^topo 직접 종속되지 않는 걸까? 워크스페이스가 합성 작업을 통해 패키지 종속성을 재귀적으로 연결하기를 원하기 때문이다. typecheck^topo에 종속되면 turbo가 첫 번째 종속성 수준 이후에는 그래프에 추가를 중지한다.